some progress

This commit is contained in:
Josia Pietsch 2025-04-22 14:00:36 +02:00
parent f025ac12c1
commit e1daa8d9be
Signed by: jrpie
GPG key ID: E70B571D66986A2D
26 changed files with 1152 additions and 243 deletions

View file

@ -8,6 +8,7 @@
tools:ignore="QueryAllPackagesPermission" />
<uses-permission android:name="android.permission.ACCESS_HIDDEN_PROFILES" />
<uses-permission android:name="android.permission.EXPAND_STATUS_BAR" />
<uses-permission android:name="android.permission.BIND_APPWIDGET" />
<application
android:name=".Application"
@ -20,7 +21,11 @@
android:theme="@style/launcherBaseTheme"
tools:ignore="UnusedAttribute">
<activity
android:name=".ui.widgets.SelectWidgetActivity"
android:name=".ui.widgets.manage.ManageWidgetsActivity"
android:theme="@style/launcherHomeTheme"
android:exported="false" />
<activity
android:name=".ui.widgets.manage.SelectWidgetActivity"
android:exported="false" />
<activity
android:name=".ui.PinShortcutActivity"

View file

@ -15,14 +15,13 @@ import de.jrpie.android.launcher.BuildConfig
import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.actions.lock.LauncherAccessibilityService
import de.jrpie.android.launcher.apps.AppFilter
import de.jrpie.android.launcher.apps.hidePrivateSpaceWhenLocked
import de.jrpie.android.launcher.apps.isPrivateSpaceSupported
import de.jrpie.android.launcher.apps.togglePrivateSpaceLock
import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.ui.list.ListActivity
import de.jrpie.android.launcher.ui.settings.SettingsActivity
import de.jrpie.android.launcher.ui.tutorial.TutorialActivity
import de.jrpie.android.launcher.ui.widgets.SelectWidgetActivity
import de.jrpie.android.launcher.ui.widgets.manage.ManageWidgetsActivity
import de.jrpie.android.launcher.ui.widgets.manage.SelectWidgetActivity
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@ -64,7 +63,7 @@ enum class LauncherAction(
R.string.list_other_list_favorites,
R.drawable.baseline_favorite_24,
{ context ->
context.startActivity(Intent(context.applicationContext, SelectWidgetActivity::class.java))
context.startActivity(Intent(context.applicationContext, ManageWidgetsActivity::class.java))
},
//openAppsList(context, favorite = true) },
true
@ -74,12 +73,15 @@ enum class LauncherAction(
R.string.list_other_list_private_space,
R.drawable.baseline_security_24,
{ context ->
context.startActivity(Intent(context.applicationContext, SelectWidgetActivity::class.java))
},
/*{ context ->
if ((context.applicationContext as Application).privateSpaceLocked.value != true
|| !hidePrivateSpaceWhenLocked(context)
) {
openAppsList(context, private = true)
}
},
}, */
available = { _ ->
isPrivateSpaceSupported()
}

View file

@ -13,6 +13,7 @@ import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersio
import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersion3
import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersionUnknown
import de.jrpie.android.launcher.ui.HomeActivity
import de.jrpie.android.launcher.widgets.deleteAllWidgets
/* Current version of the structure of preferences.
* Increase when breaking changes are introduced and write an appropriate case in
@ -71,6 +72,7 @@ fun resetPreferences(context: Context) {
Log.i(TAG, "Resetting preferences")
LauncherPreferences.clear()
LauncherPreferences.internal().versionCode(PREFERENCE_VERSION)
deleteAllWidgets(context)
val hidden: MutableSet<AbstractAppInfo> = mutableSetOf()

View file

@ -1,6 +1,7 @@
package de.jrpie.android.launcher.ui
import android.annotation.SuppressLint
import android.app.Activity
import android.content.SharedPreferences
import android.content.res.Configuration
import android.content.res.Resources
@ -9,11 +10,8 @@ import android.os.Bundle
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.window.OnBackInvokedDispatcher
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import androidx.core.view.marginTop
import de.jrpie.android.launcher.Application
import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.actions.Action
@ -23,13 +21,7 @@ import de.jrpie.android.launcher.databinding.HomeBinding
import de.jrpie.android.launcher.openTutorial
import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.ui.tutorial.TutorialActivity
import de.jrpie.android.launcher.widgets.bindAppWidget
import de.jrpie.android.launcher.widgets.createAppWidgetView
import de.jrpie.android.launcher.widgets.deleteAllWidgets
import de.jrpie.android.launcher.widgets.getAppWidgetProviders
import java.util.Locale
import kotlin.math.absoluteValue
import kotlin.random.Random
/**
* [HomeActivity] is the actual application Launcher,
@ -43,7 +35,7 @@ import kotlin.random.Random
* - Setting global variables (preferences etc.)
* - Opening the [TutorialActivity] on new installations
*/
class HomeActivity : UIObject, AppCompatActivity() {
class HomeActivity : UIObject, Activity() {
private lateinit var binding: HomeBinding
private var touchGestureDetector: TouchGestureDetector? = null
@ -59,10 +51,15 @@ class HomeActivity : UIObject, AppCompatActivity() {
if (prefKey?.startsWith("action.") == true) {
updateSettingsFallbackButtonVisibility()
}
if (prefKey?.startsWith("internal.widgets") == true) {
binding.homeWidgetContainer.updateWidgets(this)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super<AppCompatActivity>.onCreate(savedInstanceState)
super<Activity>.onCreate(savedInstanceState)
super<UIObject>.onCreate()
@ -82,17 +79,7 @@ class HomeActivity : UIObject, AppCompatActivity() {
binding.buttonFallbackSettings.setOnClickListener {
LauncherAction.SETTINGS.invoke(this)
}
// deleteAllWidgets(this)
LauncherPreferences.internal().widgets().forEach { widget ->
createAppWidgetView(this, widget)?.let {
binding.homeWidgetContainer.addView(it)
}
}
// TODO: appWidgetHost.deleteAppWidgetId(appWidgetId)
binding.homeWidgetContainer.updateWidgets(this)
}
override fun onConfigurationChanged(newConfig: Configuration) {
@ -101,8 +88,7 @@ class HomeActivity : UIObject, AppCompatActivity() {
}
override fun onStart() {
super<AppCompatActivity>.onStart()
super<Activity>.onStart()
super<UIObject>.onStart()
// If the tutorial was not finished, start it
@ -113,6 +99,16 @@ class HomeActivity : UIObject, AppCompatActivity() {
LauncherPreferences.getSharedPreferences()
.registerOnSharedPreferenceChangeListener(sharedPreferencesListener)
(application as Application).appWidgetHost.startListening()
binding.homeWidgetContainer.updateWidgets(this)
}
override fun onStop() {
(application as Application).appWidgetHost.stopListening()
super.onStop()
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
@ -252,7 +248,14 @@ class HomeActivity : UIObject, AppCompatActivity() {
return true
}
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
// TODO: fix!
touchGestureDetector?.onTouchEvent(event)
return false
}
override fun onTouchEvent(event: MotionEvent): Boolean {
android.util.Log.e("Launcher", "on touch")
touchGestureDetector?.onTouchEvent(event)
return true
}
@ -284,4 +287,4 @@ class HomeActivity : UIObject, AppCompatActivity() {
override fun isHomeScreen(): Boolean {
return true
}
}
}

View file

@ -49,7 +49,21 @@ class PinShortcutActivity : AppCompatActivity(), UIObject {
val request = launcherApps.getPinItemRequest(intent)
this.request = request
if (request == null || request.requestType != PinItemRequest.REQUEST_TYPE_SHORTCUT) {
if (request == null) {
finish()
return
}
if (request.requestType == PinItemRequest.REQUEST_TYPE_APPWIDGET) {
// TODO
request.getAppWidgetProviderInfo(this)
// startActivity()
finish()
return
}
if (request.requestType != PinItemRequest.REQUEST_TYPE_SHORTCUT) {
finish()
return
}

View file

@ -1,105 +0,0 @@
package de.jrpie.android.launcher.ui.widgets
import android.app.Activity
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.actions.Action
import de.jrpie.android.launcher.actions.Gesture
import de.jrpie.android.launcher.actions.LauncherAction
import de.jrpie.android.launcher.databinding.ActivitySelectWidgetBinding
import de.jrpie.android.launcher.databinding.HomeBinding
import de.jrpie.android.launcher.ui.list.ListActivity
import de.jrpie.android.launcher.ui.list.other.OtherRecyclerAdapter
import de.jrpie.android.launcher.widgets.bindAppWidget
import de.jrpie.android.launcher.widgets.getAppWidgetProviders
class SelectWidgetActivity : AppCompatActivity() {
lateinit var binding: ActivitySelectWidgetBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
// Initialise layout
binding = ActivitySelectWidgetBinding.inflate(layoutInflater)
setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
val viewManager = LinearLayoutManager(this)
val viewAdapter = SelectWidgetRecyclerAdapter(this)
binding.selectWidgetRecycler.apply {
// improve performance (since content changes don't change the layout size)
setHasFixedSize(true)
layoutManager = viewManager
adapter = viewAdapter
}
}
}
class SelectWidgetRecyclerAdapter(val activity: Activity) :
RecyclerView.Adapter<SelectWidgetRecyclerAdapter.ViewHolder>() {
private val widgets = getAppWidgetProviders(activity).toTypedArray()
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView),
View.OnClickListener {
var textView: TextView = itemView.findViewById(R.id.list_widgets_row_name)
var iconView: ImageView = itemView.findViewById(R.id.list_widgets_row_icon)
var previewView: ImageView = itemView.findViewById(R.id.list_widgets_row_preview)
override fun onClick(v: View) {
val pos = bindingAdapterPosition
val content = widgets[pos]
bindAppWidget(activity, content)
activity.finish()
}
init {
itemView.setOnClickListener(this)
}
}
override fun onBindViewHolder(viewHolder: ViewHolder, i: Int) {
val label = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
"${widgets[i].activityInfo.loadLabel(activity.packageManager)} ${widgets[i].loadDescription(activity)}"
} else {
widgets[i].label
}
val preview = widgets[i].loadPreviewImage(activity, 100)
val icon = widgets[i].loadIcon(activity, 100)
viewHolder.textView.text = label
viewHolder.iconView.setImageDrawable(icon)
viewHolder.previewView.setImageDrawable(preview)
}
override fun getItemCount(): Int {
return widgets.size
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(parent.context)
val view: View = inflater.inflate(R.layout.list_widgets_row, parent, false)
return ViewHolder(view)
}
}

View file

@ -1,12 +1,130 @@
package de.jrpie.android.launcher.ui.widgets
import android.app.Activity
import android.content.Context
import android.os.Build
import android.os.Bundle
import android.util.AttributeSet
import android.widget.LinearLayout
import android.util.Log
import android.util.SizeF
import android.view.View.MeasureSpec.makeMeasureSpec
import android.view.ViewGroup
import androidx.core.view.size
import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.widgets.WidgetPosition
import de.jrpie.android.launcher.widgets.createAppWidgetView
import kotlin.math.max
// TODO: implement layout logic instead of linear layout
class WidgetContainerView(context: Context, attrs: AttributeSet?): LinearLayout(context, attrs) {
init {
orientation = VERTICAL
/**
* This only works in an Activity, not AppCompatActivity
*/
open class WidgetContainerView(context: Context, attrs: AttributeSet? = null) : ViewGroup(context, attrs) {
open fun updateWidgets(activity: Activity) {
Log.i("WidgetContainer", "updating ${activity.localClassName}")
(0..<size).forEach { removeViewAt(0) }
val dp = activity.resources.displayMetrics.density
val screenWidth = activity.resources.displayMetrics.widthPixels
val screenHeight = activity.resources.displayMetrics.heightPixels
LauncherPreferences.internal().widgets()?.forEach { widget ->
createAppWidgetView(activity, widget)?.let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val absolutePosition = widget.position.getAbsoluteRect(screenWidth, screenHeight)
it.updateAppWidgetSize(Bundle.EMPTY,
listOf(SizeF(
absolutePosition.width() / dp,
absolutePosition.height() / dp
)))
Log.i("WidgetContainer", "Adding widget ${widget.id} at ${widget.position} ($absolutePosition)")
} else {
Log.i("WidgetContainer", "Adding widget ${widget.id} at ${widget.position}")
}
addView(it, WidgetContainerView.Companion.LayoutParams(widget.position))
}
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
var maxHeight = suggestedMinimumHeight
var maxWidth = suggestedMinimumWidth
val mWidth = MeasureSpec.getSize(widthMeasureSpec)
val mHeight = MeasureSpec.getSize(heightMeasureSpec)
(0..<size).map { getChildAt(it) }.forEach {
val position = (it.layoutParams as LayoutParams).position.getAbsoluteRect(mWidth, mHeight)
it.measure(makeMeasureSpec(position.width(), MeasureSpec.EXACTLY), makeMeasureSpec(position.height(), MeasureSpec.EXACTLY))
Log.e("measure", "$position")
}
// Find rightmost and bottom-most child
(0..<size).map { getChildAt(it) }.filter { it.visibility != GONE }.forEach {
val position = (it.layoutParams as LayoutParams).position.getAbsoluteRect(mWidth, mHeight)
maxWidth = max(maxWidth, position.left + it.measuredWidth)
maxHeight = max(maxHeight, position.top + it.measuredHeight)
}
setMeasuredDimension(
resolveSizeAndState(maxWidth.toInt(), widthMeasureSpec, 0),
resolveSizeAndState(maxHeight.toInt(), heightMeasureSpec, 0)
)
}
/**
* Returns a set of layout parameters with a width of
* [ViewGroup.LayoutParams.WRAP_CONTENT],
* a height of [ViewGroup.LayoutParams.WRAP_CONTENT]
* and with the coordinates (0, 0).
*/
override fun generateDefaultLayoutParams(): ViewGroup.LayoutParams {
return LayoutParams(WidgetPosition(0,0,1,1))
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
for (i in 0..<size) {
val child = getChildAt(i)
//if (child.visibility != GONE) {
val lp = child.layoutParams as LayoutParams
val position = lp.position.getAbsoluteRect(r - l, b - t)
Log.e("onLayout", "$l, $t, $r, $b, absolute rect: $position")
child.layout(position.left, position.top, position.right, position.bottom)
child.layoutParams.width = position.width()
child.layoutParams.height = position.height()
//}
}
}
override fun generateLayoutParams(attrs: AttributeSet?): ViewGroup.LayoutParams {
return LayoutParams(context, attrs)
}
// Override to allow type-checking of LayoutParams.
override fun checkLayoutParams(p: ViewGroup.LayoutParams?): Boolean {
return p is LayoutParams
}
override fun generateLayoutParams(p: ViewGroup.LayoutParams?): ViewGroup.LayoutParams {
return LayoutParams(p)
}
override fun shouldDelayChildPressedState(): Boolean {
return false
}
companion object {
class LayoutParams : ViewGroup.LayoutParams {
var position = WidgetPosition(0,0,4,4)
constructor(position: WidgetPosition) : super(WRAP_CONTENT, WRAP_CONTENT) {
this.position = position
}
constructor(c: Context, attrs: AttributeSet?) : super(c, attrs)
constructor(source: ViewGroup.LayoutParams?) : super(source)
}
}
}

View file

@ -0,0 +1,173 @@
package de.jrpie.android.launcher.ui.widgets.manage
import android.app.Activity
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProviderInfo
import android.content.Intent
import android.content.SharedPreferences
import android.content.res.Resources
import android.graphics.Rect
import android.os.Bundle
import android.util.Log
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.google.android.material.floatingactionbutton.FloatingActionButton
import de.jrpie.android.launcher.Application
import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.ui.UIObject
import de.jrpie.android.launcher.ui.widgets.WidgetContainerView
import de.jrpie.android.launcher.widgets.WidgetInfo
import de.jrpie.android.launcher.widgets.WidgetPosition
import de.jrpie.android.launcher.widgets.deleteAppWidget
import kotlin.math.min
// http://coderender.blogspot.com/2012/01/hosting-android-widgets-my.html
const val REQUEST_CREATE_APPWIDGET = 1
const val REQUEST_PICK_APPWIDGET = 2
class ManageWidgetsActivity : Activity(), UIObject {
private var sharedPreferencesListener =
SharedPreferences.OnSharedPreferenceChangeListener { _, prefKey ->
if (prefKey?.startsWith("internal.widgets") == true) {
findViewById<WidgetContainerView>(R.id.manage_widgets_container).updateWidgets(this)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super<Activity>.onCreate(savedInstanceState)
super<UIObject>.onCreate()
setContentView(R.layout.activity_manage_widgets)
findViewById<FloatingActionButton>(R.id.manage_widgets_button_add).setOnClickListener {
selectWidget()
}
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
findViewById<WidgetContainerView>(R.id.manage_widgets_container).updateWidgets(this)
}
override fun onStart() {
super<Activity>.onStart()
super<UIObject>.onStart()
LauncherPreferences.getSharedPreferences()
.registerOnSharedPreferenceChangeListener(sharedPreferencesListener)
}
override fun getTheme(): Resources.Theme {
val mTheme = modifyTheme(super.getTheme())
mTheme.applyStyle(R.style.backgroundWallpaper, true)
LauncherPreferences.clock().font().applyToTheme(mTheme)
LauncherPreferences.theme().colorTheme().applyToTheme(
mTheme,
LauncherPreferences.theme().textShadow()
)
return mTheme
}
override fun onDestroy() {
LauncherPreferences.getSharedPreferences()
.unregisterOnSharedPreferenceChangeListener(sharedPreferencesListener)
super.onDestroy()
}
fun selectWidget() {
val appWidgetHost = (application as Application).appWidgetHost
startActivityForResult(
Intent(this, SelectWidgetActivity::class.java).also {
it.putExtra(
AppWidgetManager.EXTRA_APPWIDGET_ID,
appWidgetHost.allocateAppWidgetId()
)
}, REQUEST_PICK_APPWIDGET
)
}
fun createWidget(data: Intent) {
Log.i("Launcher", "creating widget")
val appWidgetManager = (application as Application).appWidgetManager
val appWidgetId = data.extras?.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID) ?: return
val provider = appWidgetManager.getAppWidgetInfo(appWidgetId)
val display = windowManager.defaultDisplay
val position = WidgetPosition.fromAbsoluteRect(
Rect(0,0,
min(400, appWidgetManager.getAppWidgetInfo(appWidgetId).minWidth),
min(400, appWidgetManager.getAppWidgetInfo(appWidgetId).minHeight)
),
display.width,
display.height
)
val widget = WidgetInfo(appWidgetId, provider, position)
LauncherPreferences.internal().widgets(
(LauncherPreferences.internal().widgets() ?: HashSet()).also {
it.add(widget)
}
)
findViewById<WidgetContainerView>(R.id.manage_widgets_container).updateWidgets(this)
}
private fun configureWidget(data: Intent) {
val extras = data.extras
val appWidgetId = extras!!.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)
val appWidgetHost = (application as Application).appWidgetHost
val appWidgetInfo: AppWidgetProviderInfo =
(application as Application).appWidgetManager.getAppWidgetInfo(appWidgetId) ?: return
if (appWidgetInfo.configure != null) {
appWidgetHost.startAppWidgetConfigureActivityForResult(
this,
appWidgetId,
0,
REQUEST_CREATE_APPWIDGET,
null
)
} else {
createWidget(data)
}
}
override fun onActivityResult(
requestCode: Int, resultCode: Int,
data: Intent?
) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == RESULT_OK) {
Log.i("Manage Widgets", "result ok")
if (requestCode == REQUEST_PICK_APPWIDGET) {
configureWidget(data!!)
} else if (requestCode == REQUEST_CREATE_APPWIDGET) {
createWidget(data!!)
}
} else if (resultCode == RESULT_CANCELED && data != null) {
Log.i("Manage Widgets", "result canceled")
val appWidgetId =
data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)
if (appWidgetId != -1) {
deleteAppWidget(this, WidgetInfo(appWidgetId))
}
}
}
/**
* For a better preview, [ManageWidgetsActivity] should behave exactly like [HomeActivity]
*/
override fun isHomeScreen(): Boolean {
return true
}
}

View file

@ -0,0 +1,151 @@
package de.jrpie.android.launcher.ui.widgets.manage
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProviderInfo
import android.content.Intent
import android.content.res.Resources
import android.os.Build
import android.os.Bundle
import android.util.DisplayMetrics
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.databinding.ActivitySelectWidgetBinding
import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.ui.UIObject
import de.jrpie.android.launcher.widgets.bindAppWidgetOrRequestPermission
import de.jrpie.android.launcher.widgets.getAppWidgetProviders
private const val REQUEST_WIDGET_PERMISSION = 29
/**
* This activity lets the user pick an app widget to add.
* It provides an interface similar to [android.appwidget.AppWidgetManager.ACTION_APPWIDGET_PICK],
* but shows more information and also shows widgets from other user profiles.
*/
class SelectWidgetActivity : AppCompatActivity(), UIObject {
lateinit var binding: ActivitySelectWidgetBinding
var widgetId: Int = -1
private fun tryBindWidget(info: AppWidgetProviderInfo) {
if(bindAppWidgetOrRequestPermission(this, info, widgetId, REQUEST_WIDGET_PERMISSION)) {
setResult(
RESULT_OK,
Intent().also {
it.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId)
}
)
finish()
}
}
override fun onStart() {
super<AppCompatActivity>.onStart()
super<UIObject>.onStart()
}
override fun onCreate(savedInstanceState: Bundle?) {
super<AppCompatActivity>.onCreate(savedInstanceState)
super<UIObject>.onCreate()
binding = ActivitySelectWidgetBinding.inflate(layoutInflater)
setContentView(binding.root)
widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)
val viewManager = LinearLayoutManager(this)
val viewAdapter = SelectWidgetRecyclerAdapter()
binding.selectWidgetRecycler.apply {
setHasFixedSize(false)
layoutManager = viewManager
adapter = viewAdapter
}
}
override fun getTheme(): Resources.Theme {
return modifyTheme(super.getTheme())
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_WIDGET_PERMISSION && resultCode == RESULT_OK) {
data ?: return
Log.i("SelectWidget", "permission granted")
val provider = (data.getSerializableExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER) as? AppWidgetProviderInfo) ?: return
tryBindWidget(provider)
}
}
inner class SelectWidgetRecyclerAdapter() :
RecyclerView.Adapter<SelectWidgetRecyclerAdapter.ViewHolder>() {
private val widgets = getAppWidgetProviders(this@SelectWidgetActivity).toTypedArray()
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView),
View.OnClickListener {
var textView: TextView = itemView.findViewById(R.id.list_widgets_row_name)
var descriptionView: TextView = itemView.findViewById(R.id.list_widgets_row_description)
var iconView: ImageView = itemView.findViewById(R.id.list_widgets_row_icon)
var previewView: ImageView = itemView.findViewById(R.id.list_widgets_row_preview)
override fun onClick(v: View) {
tryBindWidget(widgets[bindingAdapterPosition])
}
init {
itemView.setOnClickListener(this)
}
}
override fun onBindViewHolder(viewHolder: ViewHolder, i: Int) {
val label = widgets[i].label
val description = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
widgets[i].loadDescription(this@SelectWidgetActivity)
} else {
""
}
val preview =
widgets[i].loadPreviewImage(this@SelectWidgetActivity, DisplayMetrics.DENSITY_DEFAULT )
val icon =
widgets[i].loadIcon(this@SelectWidgetActivity, DisplayMetrics.DENSITY_DEFAULT)
viewHolder.textView.text = label
viewHolder.descriptionView.text = description
viewHolder.descriptionView.visibility =
if (description?.isEmpty() == false) { View.VISIBLE } else { View.GONE }
viewHolder.iconView.setImageDrawable(icon)
viewHolder.previewView.setImageDrawable(preview)
viewHolder.previewView.visibility =
if (preview != null) { View.VISIBLE } else { View.GONE }
viewHolder.previewView.requestLayout()
}
override fun getItemCount(): Int {
return widgets.size
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(parent.context)
val view: View = inflater.inflate(R.layout.list_widgets_row, parent, false)
return ViewHolder(view)
}
}
}

View file

@ -0,0 +1,176 @@
package de.jrpie.android.launcher.ui.widgets.manage
import android.app.Activity
import android.appwidget.AppWidgetHostView
import android.content.Context
import android.graphics.Point
import android.graphics.Rect
import android.graphics.RectF
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.AttributeSet
import android.view.HapticFeedbackConstants
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import androidx.core.graphics.contains
import androidx.core.graphics.minus
import androidx.core.graphics.toRect
import androidx.core.view.children
import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.ui.widgets.WidgetContainerView
import de.jrpie.android.launcher.widgets.WidgetPosition
import de.jrpie.android.launcher.widgets.getWidgetById
import de.jrpie.android.launcher.widgets.updateWidget
import kotlin.math.max
import kotlin.math.min
/**
* A variant of the [WidgetContainerView] which allows to manage widgets.
*/
class WidgetManagerView(context: Context, attrs: AttributeSet? = null) :
WidgetContainerView(context, attrs) {
val TOUCH_SLOP: Int
val TOUCH_SLOP_SQUARE: Int
val LONG_PRESS_TIMEOUT: Long
init {
val configuration = ViewConfiguration.get(context)
TOUCH_SLOP = configuration.scaledTouchSlop
TOUCH_SLOP_SQUARE = TOUCH_SLOP * TOUCH_SLOP
LONG_PRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout().toLong()
}
enum class EditMode(val resize: (dx: Int, dy: Int, rect: Rect) -> Rect) {
MOVE({ dx, dy, rect ->
Rect(rect.left + dx, rect.top + dy, rect.right + dx, rect.bottom + dy)
}),
TOP({ dx, dy, rect ->
Rect(rect.left, min(rect.top + dy, rect.bottom - 200), rect.right, rect.bottom)
}),
BOTTOM({ dx, dy, rect ->
Rect(rect.left, rect.top, rect.right, max(rect.top + 200, rect.bottom + dy))
}),
LEFT({ dx, dy, rect ->
Rect(min(rect.left + dx, rect.right - 200), rect.top, rect.right, rect.bottom)
}),
RIGHT({ dx, dy, rect ->
Rect(rect.left, rect.top, max(rect.left + 200, rect.right + dx), rect.bottom)
}),
}
var selectedWidgetOverlayView: WidgetOverlayView? = null
var selectedWidgetView: View? = null
var currentGestureStart: Point? = null
var startWidgetPosition: Rect? = null
var lastPosition = Rect()
private val longPressHandler = Handler(Looper.getMainLooper())
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
onTouchEvent(ev)
return true
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
if (event == null) {
return false
}
synchronized(this) {
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
val start = Point(event.x.toInt(), event.y.toInt())
currentGestureStart = start
val view = children.mapNotNull { it as? WidgetOverlayView }.firstOrNull {
RectF(it.x, it.y, it.x + it.width, it.y + it.height).toRect().contains(start) == true
} ?: return false
val position = (view.layoutParams as Companion.LayoutParams).position.getAbsoluteRect(width, height)
selectedWidgetOverlayView = view
val widgetView = getWidgetViewById(view.widgetId)
selectedWidgetView = widgetView ?: return true
widgetView.visibility = GONE
startWidgetPosition = position
val positionInView = start.minus(Point(position.left, position.top))
view.mode = view.getHandles().firstOrNull { it.position.contains(positionInView) }?.mode ?: EditMode.MOVE
longPressHandler.postDelayed({
synchronized(this@WidgetManagerView) {
view.showPopupMenu()
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
endInteraction()
}
}, LONG_PRESS_TIMEOUT)
}
if (event.actionMasked == MotionEvent.ACTION_MOVE ||
event.actionMasked == MotionEvent.ACTION_UP
) {
val distanceX = event.x - (currentGestureStart?.x ?: return true)
val distanceY = event.y - (currentGestureStart?.y ?: return true)
if (distanceX * distanceX + distanceY * distanceY > TOUCH_SLOP_SQUARE) {
longPressHandler.removeCallbacksAndMessages(null)
}
val view = selectedWidgetOverlayView ?: return true
val start = startWidgetPosition ?: return true
val absoluteNewPosition = view.mode?.resize(
distanceX.toInt(),
distanceY.toInt(),
start
) ?: return true
val newPosition = WidgetPosition.fromAbsoluteRect(
absoluteNewPosition, width, height
)
if (newPosition != lastPosition) {
lastPosition = absoluteNewPosition
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_PRESS)
}
}
(view.layoutParams as Companion.LayoutParams).position = newPosition
requestLayout()
if (event.actionMasked == MotionEvent.ACTION_UP) {
longPressHandler.removeCallbacksAndMessages(null)
val id = selectedWidgetOverlayView?.widgetId ?: return true
val widget = getWidgetById(id) ?: return true
widget.position = newPosition
endInteraction()
updateWidget(widget)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
view.performHapticFeedback(HapticFeedbackConstants.GESTURE_END)
}
}
}
}
return true
}
private fun endInteraction() {
startWidgetPosition = null
selectedWidgetOverlayView?.mode = null
selectedWidgetView?.visibility = VISIBLE
}
fun getWidgetViewById(id: Int): AppWidgetHostView? {
return children.mapNotNull { it as? AppWidgetHostView }.firstOrNull {
it.appWidgetId == id
}
}
override fun updateWidgets(activity: Activity) {
super.updateWidgets(activity)
LauncherPreferences.internal().widgets()?.forEach { widget ->
WidgetOverlayView(activity).let {
addView(it)
it.widgetId = widget.id
(it.layoutParams as Companion.LayoutParams).position = widget.position
}
}
}
}

View file

@ -0,0 +1,133 @@
package de.jrpie.android.launcher.ui.widgets.manage
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.util.DisplayMetrics
import android.view.ContextMenu
import android.view.View
import android.widget.PopupMenu
import androidx.core.graphics.toRectF
import de.jrpie.android.launcher.Application
import de.jrpie.android.launcher.widgets.WidgetInfo
import de.jrpie.android.launcher.widgets.WidgetPosition
import de.jrpie.android.launcher.widgets.deleteAppWidget
/**
* An overlay to show configuration options for a widget.
*/
private const val HANDLE_SIZE = 100
private const val HANDLE_EDGE_SIZE = (1.2 * HANDLE_SIZE).toInt()
class WidgetOverlayView : View {
val paint = Paint()
val handlePaint = Paint()
val selectedHandlePaint = Paint()
var mode: WidgetManagerView.EditMode? = null
class Handle(val mode: WidgetManagerView.EditMode, val position: Rect)
init {
handlePaint.style = Paint.Style.STROKE
handlePaint.setARGB(100, 255, 255, 255)
selectedHandlePaint.style = Paint.Style.FILL_AND_STROKE
selectedHandlePaint.setARGB(255, 255, 255, 255)
paint.style = Paint.Style.STROKE
paint.setARGB(50, 255, 255, 255)
}
private var preview: Drawable? = null
var widgetId: Int = -1
get() = field
set(newId) {
field = newId
val appWidgetManager= (context.applicationContext as Application).appWidgetManager
preview =
appWidgetManager.getAppWidgetInfo(newId).loadPreviewImage(context, DisplayMetrics.DENSITY_HIGH) ?:
appWidgetManager.getAppWidgetInfo(newId).loadIcon(context, DisplayMetrics.DENSITY_HIGH)
}
constructor(context: Context) : super(context) {
init(null, 0)
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
init(attrs, 0)
}
constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(
context,
attrs,
defStyle
) {
init(attrs, defStyle)
}
private fun init(attrs: AttributeSet?, defStyle: Int) { }
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
getHandles().forEach {
if (it.mode == mode) {
canvas.drawRoundRect(it.position.toRectF(), 5f, 5f, selectedHandlePaint)
} else {
canvas.drawRoundRect(it.position.toRectF(), 5f, 5f, handlePaint)
}
}
val bounds = getBounds()
canvas.drawRect(bounds, paint)
if (mode == null) {
return
}
preview?.bounds = bounds
preview?.draw(canvas)
}
fun showPopupMenu() {
val menu = PopupMenu(context, this)
menu.menu.let {
it.add("Remove").setOnMenuItemClickListener { _ ->
deleteAppWidget(context, WidgetInfo(widgetId))
return@setOnMenuItemClickListener true
}
it.add("Allow Interaction").setOnMenuItemClickListener { _ ->
return@setOnMenuItemClickListener true
}
it.add("Add Padding")
}
menu.show()
}
fun getHandles(): List<Handle> {
return listOf<Handle>(
Handle(WidgetManagerView.EditMode.TOP,
Rect(HANDLE_EDGE_SIZE, 0, width - HANDLE_EDGE_SIZE, HANDLE_SIZE)),
Handle(WidgetManagerView.EditMode.BOTTOM,
Rect(HANDLE_EDGE_SIZE, height - HANDLE_SIZE, width - HANDLE_EDGE_SIZE, height)),
Handle(WidgetManagerView.EditMode.LEFT,
Rect(0, HANDLE_EDGE_SIZE, HANDLE_SIZE, height - HANDLE_EDGE_SIZE)),
Handle(WidgetManagerView.EditMode.RIGHT,
Rect(width - HANDLE_SIZE, HANDLE_EDGE_SIZE, width, height - HANDLE_EDGE_SIZE))
)
}
private fun getBounds(): Rect {
return Rect(0,0, width, height)
}
}

View file

@ -1,14 +1,36 @@
package de.jrpie.android.launcher.widgets;
import de.jrpie.android.launcher.apps.AbstractAppInfo
import kotlinx.serialization.SerialName;
import kotlinx.serialization.Serializable;
import android.appwidget.AppWidgetProviderInfo
import android.content.Context
import de.jrpie.android.launcher.Application
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@Serializable
@SerialName("widget")
class WidgetInfo(val id: Int, val width: Int, val height: Int) {
class WidgetInfo(
val id: Int,
var position: WidgetPosition = WidgetPosition(0,0,1,1),
// We keep track of packageName, className and user to make it possible to restore the widget
// on a new device when restoring settings (currently not implemented)
// In normal operation only id and position are used.
val packageName: String? = null,
val className: String? = null,
val user: Int? = null
) {
constructor(id: Int, widgetProviderInfo: AppWidgetProviderInfo, position: WidgetPosition) :
this(
id, position,
widgetProviderInfo.provider.packageName,
widgetProviderInfo.provider.className,
widgetProviderInfo.profile.hashCode()
)
fun serialize(): String {
return Json.encodeToString(this)
}
@ -20,9 +42,37 @@ class WidgetInfo(val id: Int, val width: Int, val height: Int) {
override fun equals(other: Any?): Boolean {
return (other as? WidgetInfo)?.id == id
}
companion object {
fun deserialize(serialized: String): WidgetInfo {
return Json.decodeFromString(serialized)
}
}
/**
* Get the [AppWidgetProviderInfo] by [id].
* If the widget is not installed, use [restoreAppWidgetProviderInfo] instead.
*/
fun getAppWidgetProviderInfo(context: Context): AppWidgetProviderInfo? {
return (context.applicationContext as Application).appWidgetManager
.getAppWidgetInfo(id)
}
/**
* Restore the AppWidgetProviderInfo from [user], [packageName] and [className].
* Only use this when the widget is not installed,
* in normal operation use [getAppWidgetProviderInfo] instead.
*/
fun restoreAppWidgetProviderInfo(context: Context): AppWidgetProviderInfo? {
return getAppWidgetProviders(context).firstOrNull {
it.profile.hashCode() == user
&& it.provider.packageName == packageName
&& it.provider.className == className
}
}
override fun toString(): String {
return "WidgetInfo(id=$id, position=$position, packageName=$packageName, className=$className, user=$user)"
}
}

View file

@ -0,0 +1,15 @@
package de.jrpie.android.launcher.widgets
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
@SerialName("panel")
data class WidgetPanel(val id: Int, val label: String) {
companion object {
val DEFAULT = WidgetPanel(0, "home")
}
}

View file

@ -0,0 +1,58 @@
package de.jrpie.android.launcher.widgets
import android.graphics.Rect
import kotlinx.serialization.Serializable
import kotlin.math.ceil
import kotlin.math.roundToInt
import kotlin.math.max
const val GRID_SIZE: Short = 12
@Serializable
data class WidgetPosition(var x: Short, var y: Short, var width: Short, var height: Short) {
fun getAbsoluteRect(screenWidth: Int, screenHeight: Int): Rect {
val gridWidth = screenWidth / GRID_SIZE.toFloat()
val gridHeight= screenHeight / GRID_SIZE.toFloat()
return Rect(
(x * gridWidth).toInt(),
(y * gridHeight).toInt(),
((x + width) * gridWidth).toInt(),
((y + height) * gridHeight).toInt()
)
}
companion object {
fun fromAbsoluteRect(absolute: Rect, screenWidth: Int, screenHeight: Int): WidgetPosition {
val gridWidth = screenWidth / GRID_SIZE.toFloat()
val gridHeight= screenHeight / GRID_SIZE.toFloat()
val x = (absolute.left / gridWidth).roundToInt().toShort().coerceIn(0, (GRID_SIZE-1).toShort())
val y = (absolute.top / gridHeight).roundToInt().toShort().coerceIn(0, (GRID_SIZE-1).toShort())
val w = max(2, ((absolute.right - absolute.left) / gridWidth).roundToInt()).toShort()
val h = max(2, ((absolute.bottom - absolute.top) / gridHeight).roundToInt()).toShort()
return WidgetPosition(x,y,w,h)
}
fun center(minWidth: Int, minHeight: Int, screenWidth: Int, screenHeight: Int): WidgetPosition {
val gridWidth = screenWidth / GRID_SIZE.toFloat()
val gridHeight= screenHeight / GRID_SIZE.toFloat()
val cellsWidth = ceil(minWidth / gridWidth).toInt().toShort()
val cellsHeight = ceil(minHeight / gridHeight).toInt().toShort()
return WidgetPosition(
((GRID_SIZE - cellsWidth) / 2).toShort(),
((GRID_SIZE - cellsHeight) / 2).toShort(),
cellsWidth,
cellsHeight
)
}
}
}

View file

@ -2,62 +2,48 @@ package de.jrpie.android.launcher.widgets
import android.app.Activity
import android.app.Service
import android.appwidget.AppWidgetHost
import android.appwidget.AppWidgetHostView
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProviderInfo
import android.content.Context
import android.content.Intent
import android.content.pm.LauncherApps
import android.os.Build
import android.os.Bundle
import android.os.UserManager
import android.util.Log
import android.util.SizeF
import de.jrpie.android.launcher.Application
import de.jrpie.android.launcher.preferences.LauncherPreferences
import kotlin.math.absoluteValue
import kotlin.random.Random
fun deleteAllWidgets(activity: Activity) {
val appWidgetHost = (activity.application as Application).appWidgetHost
fun deleteAllWidgets(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
appWidgetHost.appWidgetIds.forEach { deleteAppWidget(activity, WidgetInfo(it, 0,0)) }
context.getAppWidgetHost().appWidgetIds.forEach { deleteAppWidget(context, WidgetInfo(it)) }
}
}
fun bindAppWidget(activity: Activity, providerInfo: AppWidgetProviderInfo): WidgetInfo? {
val appWidgetHost = (activity.application as Application).appWidgetHost
val appWidgetManager = (activity.application as Application).appWidgetManager
val appWidgetId = appWidgetHost.allocateAppWidgetId()
fun bindAppWidgetOrRequestPermission(activity: Activity, providerInfo: AppWidgetProviderInfo, id: Int, requestCode: Int? = null): Boolean {
val appWidgetId = if(id == -1) {
activity.getAppWidgetHost().allocateAppWidgetId()
} else { id }
Log.i("Launcher", "Binding new widget ${appWidgetId}")
if (!appWidgetManager.bindAppWidgetIdIfAllowed(
if (!activity.getAppWidgetManager().bindAppWidgetIdIfAllowed(
appWidgetId,
providerInfo.provider
)
) {
requestAppWidgetPermission(activity, appWidgetId, providerInfo)
return null
Log.e("Launcher", "not allowed to bind widget")
requestAppWidgetPermission(activity, appWidgetId, providerInfo, requestCode)
return false
}
try {
Log.e("widgets", "configure widget")
appWidgetHost.startAppWidgetConfigureActivityForResult(activity, appWidgetId, 0, 1, null)
} catch (e: Exception) {
e.printStackTrace()
}
val widget = WidgetInfo(appWidgetId, 500, 500)
LauncherPreferences.internal().widgets(
(LauncherPreferences.internal().widgets() ?: HashSet()).also {
it.add(widget)
}
)
return widget
return true
}
fun deleteAppWidget(activity: Activity, widget: WidgetInfo) {
fun deleteAppWidget(context: Context, widget: WidgetInfo) {
Log.i("Launcher", "Deleting widget ${widget.id}")
val appWidgetHost = (activity.application as Application).appWidgetHost
val appWidgetHost = (context.applicationContext as Application).appWidgetHost
appWidgetHost.deleteAppWidgetId(widget.id)
@ -69,55 +55,58 @@ fun deleteAppWidget(activity: Activity, widget: WidgetInfo) {
}
fun createAppWidgetView(activity: Activity, widget: WidgetInfo): AppWidgetHostView? {
val appWidgetHost = (activity.application as Application).appWidgetHost
val appWidgetManager = (activity.application as Application).appWidgetManager
val providerInfo = appWidgetManager.getAppWidgetInfo(widget.id) ?: return null
val view = appWidgetHost.createView(activity, widget.id, providerInfo)
.apply {
setAppWidget(appWidgetId, appWidgetInfo)
}
val providerInfo = activity.getAppWidgetManager().getAppWidgetInfo(widget.id) ?: return null
val dp = activity.resources.displayMetrics.density
val newOptions = Bundle().apply {
putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, widget.width)
putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, widget.width)
putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, widget.height)
putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, widget.height)
val view = activity.getAppWidgetHost()
.createView(activity, widget.id, providerInfo)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
view.updateAppWidgetSize(Bundle.EMPTY, listOf(SizeF(widget.position.width / dp, widget.position.height / dp)))
}
appWidgetManager.updateAppWidgetOptions(
widget.id,
newOptions
)
//view.minimumWidth = widget.width
//view.minimumHeight = widget.height
view.setPadding(0,0,0,0)
return view
}
fun getAppWidgetProviders(context: Context): List<AppWidgetProviderInfo> {
return appWidgetProviders(context, (context.applicationContext as Application).appWidgetManager)
}
fun requestAppWidgetPermission(context: Activity, widgetId: Int, info: AppWidgetProviderInfo) {
fun requestAppWidgetPermission(context: Activity, widgetId: Int, info: AppWidgetProviderInfo, requestCode: Int?) {
Log.i("Widgets", "requesting permission for widget")
val intent = Intent(AppWidgetManager.ACTION_APPWIDGET_BIND).apply {
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId)
putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER, info.provider)
}
context.startActivityForResult(intent, 0)//REQUEST_CODE_BIND_WIDGET)
context.startActivityForResult(intent, requestCode ?: 0)
}
fun appWidgetProviders(
context: Context,
appWidgetManager: AppWidgetManager
): List<AppWidgetProviderInfo> {
val userManager = context.getSystemService(Service.USER_SERVICE) as UserManager
return userManager.userProfiles.map {
appWidgetManager.getInstalledProvidersForProfile(it)
}.flatten()
}
fun Activity.bindRandomWidget() {
val selectedWidget =
getAppWidgetProviders(this).let { it.get(Random.nextInt().absoluteValue % it.size) }
bindAppWidget(this, selectedWidget) ?: return
fun getAppWidgetProviders( context: Context ): List<AppWidgetProviderInfo> {
val appWidgetManager = context.getAppWidgetManager()
val profiles =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
(context.getSystemService(Service.LAUNCHER_APPS_SERVICE) as LauncherApps).profiles
} else {
(context.getSystemService(Service.USER_SERVICE) as UserManager).userProfiles
}
Log.i("Widgets", "profiles: ${profiles.size}, $profiles")
return profiles.map {
appWidgetManager.getInstalledProvidersForProfile(it)
}.flatten()
}
fun getWidgetById(id: Int): WidgetInfo? {
return (LauncherPreferences.internal().widgets() ?: setOf()).firstOrNull {
it.id == id
}
}
fun updateWidget(widget: WidgetInfo) {
var widgets = LauncherPreferences.internal().widgets() ?: setOf()
widgets = widgets.minus(widget).plus(widget)
LauncherPreferences.internal().widgets(widgets)
}
private fun Context.getAppWidgetHost(): AppWidgetHost {
return (this.applicationContext as Application).appWidgetHost
}
private fun Context.getAppWidgetManager(): AppWidgetManager {
return (this.applicationContext as Application).appWidgetManager
}

View file

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?android:textColor"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
</vector>

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".ui.widgets.manage.ManageWidgetsActivity">
<de.jrpie.android.launcher.ui.widgets.manage.WidgetManagerView
android:id="@+id/manage_widgets_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/manage_widgets_button_add"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:clickable="true"
android:focusable="true"
android:src="@drawable/baseline_add_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -2,10 +2,11 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:id="@+id/select_widget_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.widgets.SelectWidgetActivity">
android:fitsSystemWindows="true"
tools:context=".ui.widgets.manage.SelectWidgetActivity">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/select_widget_appbar"
@ -61,13 +62,11 @@
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:scrollbars="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toBottomOf="@+id/select_widget_appbar" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -9,15 +9,12 @@
android:longClickable="false"
android:fitsSystemWindows="true"
tools:context=".ui.HomeActivity">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent" >
<de.jrpie.android.launcher.ui.widgets.WidgetContainerView
android:id="@+id/home_widget_container"
android:paddingLeft="20dp"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</ScrollView>
android:layout_height="match_parent" />
<TextClock
android:id="@+id/home_upper_view"

View file

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/list_apps_row_container"
android:background="@color/cardview_dark_background"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="15sp">
<ImageView
android:id="@+id/list_widgets_header_icon"
android:layout_width="40dp"
android:layout_height="40dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:src="@mipmap/ic_launcher_round"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/list_widgets_header_app_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="20sp"
android:gravity="start"
android:text=""
android:textSize="20sp"
tools:text="some widget"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/list_widgets_header_icon"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -8,23 +8,13 @@
android:layout_height="wrap_content"
android:layout_margin="15sp">
<ImageView
android:id="@+id/list_widgets_row_preview"
android:layout_width="0dp"
android:maxWidth="500dp"
android:maxHeight="200dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@mipmap/ic_launcher_round"
tools:ignore="ContentDescription" />
<ImageView
android:id="@+id/list_widgets_row_icon"
android:layout_width="40dp"
android:layout_height="40dp"
app:layout_constraintBottom_toBottomOf="@id/list_widgets_row_preview"
app:layout_constraintStart_toStartOf="@id/list_widgets_row_preview"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:src="@mipmap/ic_launcher_round"
tools:ignore="ContentDescription" />
@ -33,14 +23,38 @@
android:id="@+id/list_widgets_row_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="20sp"
android:layout_marginStart="10sp"
android:layout_marginEnd="10sp"
android:gravity="start"
android:text=""
android:textSize="20sp"
tools:text="some widget"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/list_widgets_row_preview"
app:layout_constraintStart_toEndOf="@id/list_widgets_row_icon"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/list_widgets_row_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="10sp"
android:gravity="start"
android:text=""
android:textSize="12sp"
app:layout_constraintStart_toStartOf="@+id/list_widgets_row_name"
app:layout_constraintTop_toBottomOf="@+id/list_widgets_row_name"
tools:text="a longer description of the widget" />
<ImageView
android:id="@+id/list_widgets_row_preview"
android:layout_width="0dp"
android:maxHeight="100dp"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:layout_height="100dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/list_widgets_row_description"
tools:src="@mipmap/ic_launcher_round"
tools:ignore="ContentDescription" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,16 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<de.jrpie.android.launcher.ui.widgets.WidgetOverlayView
style="@style/Widget.launcherBaseTheme.MyView"
android:layout_width="300dp"
android:layout_height="300dp"
android:paddingLeft="20dp"
android:paddingBottom="40dp"
app:exampleDimension="24sp"
app:exampleDrawable="@android:drawable/ic_menu_add"
app:exampleString="Hello, WidgetOverlayView" />
</FrameLayout>

View file

@ -0,0 +1,7 @@
<resources>
<style name="Widget.launcherBaseTheme.MyView" parent="">
<item name="android:background">@color/gray_600</item>
<item name="exampleColor">@color/light_blue_600</item>
</style>
</resources>

View file

@ -0,0 +1,8 @@
<resources>
<declare-styleable name="WidgetOverlayView">
<attr name="exampleString" format="string" />
<attr name="exampleDimension" format="dimension" />
<attr name="exampleColor" format="color" />
<attr name="exampleDrawable" format="color|reference" />
</declare-styleable>
</resources>

View file

@ -11,5 +11,9 @@
<color name="lightTheme_background_color">#fff</color>
<color name="lightTheme_accent_color">#9999ff</color>
<color name="lightTheme_text_color">#000</color>
<color name="light_blue_400">#FF29B6F6</color>
<color name="light_blue_600">#FF039BE5</color>
<color name="gray_400">#FFBDBDBD</color>
<color name="gray_600">#FF757575</color>
</resources>

View file

@ -66,12 +66,12 @@
<item name="android:shadowDy">0</item>
<item name="android:shadowRadius">2</item>
</style>
<style name="textShadowLight" parent="textShadow">
<item name="android:shadowColor">#aaa</item>
</style>
<style name="backgroundWallpaper">
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
@ -81,26 +81,30 @@
<item name="android:colorBackgroundCacheHint">@null</item>
</style>
<style name="backgroundSolid">
</style>
<style name="backgroundSolid"></style>
<style name="fontSystemDefault">
<!--<item name="android:textSize">18sp</item>-->
</style>
<style name="fontHack">
<item name="android:fontFamily">@font/hack</item>
<!--<item name="android:textSize">18sp</item>-->
</style>
<style name="fontMonospace">
<item name="android:fontFamily">monospace</item>
</style>
<style name="fontSerifMonospace">
<item name="android:fontFamily">serif-monospace</item>
</style>
<style name="fontSansSerif">
<item name="android:fontFamily">sans-serif</item>
</style>
<style name="fontSerif" tools:keep="@style/fontSerif">
<item name="android:fontFamily">serif</item>
</style>
@ -126,4 +130,9 @@
<item name="android:windowEnterAnimation">@android:anim/fade_in</item>
<item name="android:windowExitAnimation">@android:anim/fade_out</item>
</style>
<style name="Widget.launcherBaseTheme.MyView" parent="">
<item name="android:background">@color/gray_400</item>
<item name="exampleColor">@color/light_blue_400</item>
</style>
</resources>