From a964d9de527066532ac0e2989db8a879003dd0ac Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Tue, 1 Oct 2024 19:37:12 +0200 Subject: [PATCH 1/8] some tests --- app/src/main/AndroidManifest.xml | 1 + .../de/jrpie/android/launcher/Application.kt | 21 ++- .../de/jrpie/android/launcher/Functions.kt | 5 +- .../LauncherPreferences$Config.java | 2 + .../serialization/PreferenceSerializers.kt | 19 +++ .../jrpie/android/launcher/ui/HomeActivity.kt | 20 +++ .../ui/widgets/WidgetContainerView.kt | 12 ++ .../android/launcher/widgets/WidgetInfo.kt | 28 ++++ .../jrpie/android/launcher/widgets/Widgets.kt | 123 ++++++++++++++++++ app/src/main/res/layout/home.xml | 9 ++ app/src/main/res/values/donottranslate.xml | 1 + 11 files changed, 239 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetContainerView.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/widgets/WidgetInfo.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a5f8831..4ab7065 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -8,6 +8,7 @@ tools:ignore="QueryAllPackagesPermission" /> + >() val privateSpaceLocked = MutableLiveData() + lateinit var appWidgetHost: AppWidgetHost + lateinit var appWidgetManager: AppWidgetManager private val profileAvailabilityBroadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { @@ -103,10 +111,15 @@ class Application : android.app.Application() { torchManager = TorchManager(this) } + appWidgetHost = AppWidgetHost(this.applicationContext, APP_WIDGET_HOST_ID) + appWidgetManager = AppWidgetManager.getInstance(this.applicationContext) + + appWidgetHost.startListening() + + val preferences = PreferenceManager.getDefaultSharedPreferences(this) LauncherPreferences.init(preferences, this.resources) - // Try to restore old preferences migratePreferencesToNewVersion(this) @@ -157,4 +170,10 @@ class Application : android.app.Application() { apps.postValue(getApps(packageManager, applicationContext)) } } + + override fun onTerminate() { + appWidgetHost.stopListening() + super.onTerminate() + + } } diff --git a/app/src/main/java/de/jrpie/android/launcher/Functions.kt b/app/src/main/java/de/jrpie/android/launcher/Functions.kt index afc2c31..9679ae5 100644 --- a/app/src/main/java/de/jrpie/android/launcher/Functions.kt +++ b/app/src/main/java/de/jrpie/android/launcher/Functions.kt @@ -6,6 +6,9 @@ import android.app.role.RoleManager import android.content.ActivityNotFoundException import android.content.ClipData import android.content.ClipboardManager +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProvider +import android.appwidget.AppWidgetProviderInfo import android.content.Context import android.content.Intent import android.content.pm.LauncherApps @@ -223,4 +226,4 @@ fun copyToClipboard(context: Context, text: String) { val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager val clipData = ClipData.newPlainText("Debug Info", text) clipboardManager.setPrimaryClip(clipData) -} \ No newline at end of file +} diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java b/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java index 85979fe..d8397a0 100644 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java @@ -8,6 +8,7 @@ import de.jrpie.android.launcher.actions.lock.LockMethod; import de.jrpie.android.launcher.preferences.serialization.MapAbstractAppInfoStringPreferenceSerializer; import de.jrpie.android.launcher.preferences.serialization.SetAbstractAppInfoPreferenceSerializer; import de.jrpie.android.launcher.preferences.serialization.SetPinnedShortcutInfoPreferenceSerializer; +import de.jrpie.android.launcher.preferences.serialization.SetWidgetInfoSerializer; import de.jrpie.android.launcher.preferences.theme.Background; import de.jrpie.android.launcher.preferences.theme.ColorTheme; import de.jrpie.android.launcher.preferences.theme.Font; @@ -26,6 +27,7 @@ import eu.jonahbauer.android.preference.annotations.Preferences; @Preference(name = "started_time", type = long.class), // see PREFERENCE_VERSION in de.jrpie.android.launcher.preferences.Preferences.kt @Preference(name = "version_code", type = int.class, defaultValue = "-1"), + @Preference(name = "widgets", type = Set.class, serializer = SetWidgetInfoSerializer.class) }), @PreferenceGroup(name = "apps", prefix = "settings_apps_", suffix = "_key", value = { @Preference(name = "favorites", type = Set.class, serializer = SetAbstractAppInfoPreferenceSerializer.class), diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/serialization/PreferenceSerializers.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/serialization/PreferenceSerializers.kt index 3e19daf..1746e8a 100644 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/serialization/PreferenceSerializers.kt +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/serialization/PreferenceSerializers.kt @@ -4,6 +4,7 @@ package de.jrpie.android.launcher.preferences.serialization import de.jrpie.android.launcher.apps.AbstractAppInfo import de.jrpie.android.launcher.apps.PinnedShortcutInfo +import de.jrpie.android.launcher.widgets.WidgetInfo import eu.jonahbauer.android.preference.annotations.serializer.PreferenceSerializationException import eu.jonahbauer.android.preference.annotations.serializer.PreferenceSerializer import kotlinx.serialization.Serializable @@ -28,6 +29,24 @@ class SetAbstractAppInfoPreferenceSerializer : } } + +@Suppress("UNCHECKED_CAST") +class SetWidgetInfoSerializer : + PreferenceSerializer?, java.util.Set?> { + @Throws(PreferenceSerializationException::class) + override fun serialize(value: java.util.Set?): java.util.Set { + return value?.map(WidgetInfo::serialize) + ?.toHashSet() as java.util.Set + } + + @Throws(PreferenceSerializationException::class) + override fun deserialize(value: java.util.Set?): java.util.Set? { + return value?.map(java.lang.String::toString)?.map(WidgetInfo::deserialize) + ?.toHashSet() as? java.util.Set + } +} + + @Suppress("UNCHECKED_CAST") class SetPinnedShortcutInfoPreferenceSerializer : PreferenceSerializer?, java.util.Set?> { diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt index 2ab5d9f..c7c017f 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt @@ -9,9 +9,12 @@ 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 import de.jrpie.android.launcher.actions.Gesture @@ -20,7 +23,13 @@ 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, @@ -73,6 +82,17 @@ 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) + } override fun onConfigurationChanged(newConfig: Configuration) { diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetContainerView.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetContainerView.kt new file mode 100644 index 0000000..47a1480 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetContainerView.kt @@ -0,0 +1,12 @@ +package de.jrpie.android.launcher.ui.widgets + +import android.content.Context +import android.util.AttributeSet +import android.widget.LinearLayout + +// TODO: implement layout logic instead of linear layout +class WidgetContainerView(context: Context, attrs: AttributeSet?): LinearLayout(context, attrs) { + init { + orientation = VERTICAL + } +} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/WidgetInfo.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/WidgetInfo.kt new file mode 100644 index 0000000..c3d9046 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/WidgetInfo.kt @@ -0,0 +1,28 @@ +package de.jrpie.android.launcher.widgets; + +import de.jrpie.android.launcher.apps.AbstractAppInfo +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) { + fun serialize(): String { + return Json.encodeToString(this) + } + + override fun hashCode(): Int { + return id + } + + override fun equals(other: Any?): Boolean { + return (other as? WidgetInfo)?.id == id + } + companion object { + fun deserialize(serialized: String): WidgetInfo { + return Json.decodeFromString(serialized) + } + } +} diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt new file mode 100644 index 0000000..ab4f40d --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt @@ -0,0 +1,123 @@ +package de.jrpie.android.launcher.widgets + +import android.app.Activity +import android.app.Service +import android.appwidget.AppWidgetHostView +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProviderInfo +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.os.UserManager +import android.util.Log +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 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + appWidgetHost.appWidgetIds.forEach { deleteAppWidget(activity, WidgetInfo(it, 0,0)) } + } +} + +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() + + Log.i("Launcher", "Binding new widget ${appWidgetId}") + if (!appWidgetManager.bindAppWidgetIdIfAllowed( + appWidgetId, + providerInfo.provider + ) + ) { + requestAppWidgetPermission(activity, appWidgetId, providerInfo) + return null + } + 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 +} + +fun deleteAppWidget(activity: Activity, widget: WidgetInfo) { + Log.i("Launcher", "Deleting widget ${widget.id}") + val appWidgetHost = (activity.application as Application).appWidgetHost + + appWidgetHost.deleteAppWidgetId(widget.id) + + LauncherPreferences.internal().widgets( + LauncherPreferences.internal().widgets()?.also { + it.remove(widget) + } + ) +} + +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 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) + } + appWidgetManager.updateAppWidgetOptions( + widget.id, + newOptions + ) + //view.minimumWidth = widget.width + //view.minimumHeight = widget.height + + return view +} + +fun getAppWidgetProviders(context: Context): List { + return appWidgetProviders(context, (context.applicationContext as Application).appWidgetManager) +} + +fun requestAppWidgetPermission(context: Activity, widgetId: Int, info: AppWidgetProviderInfo) { + 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) +} + +fun appWidgetProviders( + context: Context, + appWidgetManager: AppWidgetManager +): List { + 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 +} + diff --git a/app/src/main/res/layout/home.xml b/app/src/main/res/layout/home.xml index ecefdea..444bd4e 100644 --- a/app/src/main/res/layout/home.xml +++ b/app/src/main/res/layout/home.xml @@ -9,6 +9,15 @@ android:longClickable="false" android:fitsSystemWindows="true" tools:context=".ui.HomeActivity"> + + + internal.started_before internal.first_startup internal.version_code + internal.widgets apps.favorites apps.hidden apps.pinned_shortcuts From da35488ae0b5836b217cbc8e6de970cfd861cdbe Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Wed, 16 Apr 2025 15:31:45 +0200 Subject: [PATCH 2/8] add basic widget selection list --- app/build.gradle | 1 + app/src/main/AndroidManifest.xml | 6 +- .../launcher/actions/LauncherAction.kt | 7 +- .../jrpie/android/launcher/ui/HomeActivity.kt | 2 +- .../ui/widgets/SelectWidgetActivity.kt | 105 ++++++++++++++++++ .../res/layout/activity_select_widget.xml | 73 ++++++++++++ app/src/main/res/layout/list_widgets_row.xml | 46 ++++++++ app/src/main/res/values/strings.xml | 1 + 8 files changed, 237 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/de/jrpie/android/launcher/ui/widgets/SelectWidgetActivity.kt create mode 100644 app/src/main/res/layout/activity_select_widget.xml create mode 100644 app/src/main/res/layout/list_widgets_row.xml diff --git a/app/build.gradle b/app/build.gradle index 1a0a6fb..eaf97f7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -106,6 +106,7 @@ dependencies { implementation 'com.google.android.material:material:1.12.0' implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3") implementation "eu.jonahbauer:android-preference-annotations:1.1.2" + implementation 'androidx.activity:activity:1.10.1' annotationProcessor "eu.jonahbauer:android-preference-annotations:1.1.2" annotationProcessor "com.android.databinding:compiler:$android_plugin_version" testImplementation 'junit:junit:4.13.2' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4ab7065..579d5c1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -8,7 +8,6 @@ tools:ignore="QueryAllPackagesPermission" /> - + - + \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/actions/LauncherAction.kt b/app/src/main/java/de/jrpie/android/launcher/actions/LauncherAction.kt index 6ba467e..0cc3dd4 100644 --- a/app/src/main/java/de/jrpie/android/launcher/actions/LauncherAction.kt +++ b/app/src/main/java/de/jrpie/android/launcher/actions/LauncherAction.kt @@ -21,6 +21,8 @@ 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 kotlinx.serialization.KSerializer import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -61,7 +63,10 @@ enum class LauncherAction( "choose_from_favorites", R.string.list_other_list_favorites, R.drawable.baseline_favorite_24, - { context -> openAppsList(context, favorite = true) }, + { context -> + context.startActivity(Intent(context.applicationContext, SelectWidgetActivity::class.java)) + }, + //openAppsList(context, favorite = true) }, true ), CHOOSE_FROM_PRIVATE_SPACE( diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt index c7c017f..44947bb 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt @@ -83,7 +83,7 @@ class HomeActivity : UIObject, AppCompatActivity() { LauncherAction.SETTINGS.invoke(this) } - deleteAllWidgets(this) + // deleteAllWidgets(this) LauncherPreferences.internal().widgets().forEach { widget -> createAppWidgetView(this, widget)?.let { diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/SelectWidgetActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/SelectWidgetActivity.kt new file mode 100644 index 0000000..98dcbe7 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/SelectWidgetActivity.kt @@ -0,0 +1,105 @@ +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() { + + 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) + } +} diff --git a/app/src/main/res/layout/activity_select_widget.xml b/app/src/main/res/layout/activity_select_widget.xml new file mode 100644 index 0000000..463a317 --- /dev/null +++ b/app/src/main/res/layout/activity_select_widget.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_widgets_row.xml b/app/src/main/res/layout/list_widgets_row.xml new file mode 100644 index 0000000..120ea5b --- /dev/null +++ b/app/src/main/res/layout/list_widgets_row.xml @@ -0,0 +1,46 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 21f25f5..301ca5e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -387,5 +387,6 @@ Open Source Licenses No app found to handle search. Can\'t open URL: no browser found. + Choose Widget From 077bd1ce448808f969254e704681924143873da7 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Wed, 23 Apr 2025 01:51:01 +0200 Subject: [PATCH 3/8] add option to hide keyboard when scrolling (cf. #142) --- .../preferences/LauncherPreferences$Config.java | 1 + .../java/de/jrpie/android/launcher/ui/Helper.kt | 16 ++++++++++++---- .../launcher/ui/list/apps/ListFragmentApps.kt | 17 +++++++++++++++++ app/src/main/res/values/donottranslate.xml | 1 + app/src/main/res/values/strings.xml | 1 + app/src/main/res/xml/preferences.xml | 4 ++++ 6 files changed, 36 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java b/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java index 85979fe..4653910 100644 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java @@ -72,6 +72,7 @@ import eu.jonahbauer.android.preference.annotations.Preferences; @Preference(name = "search_auto_launch", type = boolean.class, defaultValue = "true"), @Preference(name = "search_web", type = boolean.class, description = "false"), @Preference(name = "search_auto_open_keyboard", type = boolean.class, defaultValue = "true"), + @Preference(name = "search_auto_close_keyboard", type = boolean.class, defaultValue = "false"), }), @PreferenceGroup(name = "enabled_gestures", prefix = "settings_enabled_gestures_", suffix = "_key", value = { @Preference(name = "double_swipe", type = boolean.class, defaultValue = "true"), diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/Helper.kt b/app/src/main/java/de/jrpie/android/launcher/ui/Helper.kt index 1ca4d2b..a863c67 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/Helper.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/Helper.kt @@ -1,5 +1,6 @@ package de.jrpie.android.launcher.ui +import android.app.Activity import android.content.Context import android.graphics.ColorMatrix import android.graphics.ColorMatrixColorFilter @@ -38,10 +39,17 @@ fun ImageView.transformGrayscale(grayscale: Boolean) { } -// Taken from https://stackoverflow.com/a/50743764/12787264 +// Taken from https://stackoverflow.com/a/50743764 fun View.openSoftKeyboard(context: Context) { this.requestFocus() - // open the soft keyboard - val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - imm.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT) + (context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager) + .showSoftInput(this, InputMethodManager.SHOW_IMPLICIT) } + +// https://stackoverflow.com/a/17789187 +fun closeSoftKeyboard(activity: Activity) { + activity.currentFocus?.let { focus -> + (activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager) + .hideSoftInputFromWindow( focus.windowToken, 0 ) + } +} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/list/apps/ListFragmentApps.kt b/app/src/main/java/de/jrpie/android/launcher/ui/list/apps/ListFragmentApps.kt index 1a55bbb..a8e59ba 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/list/apps/ListFragmentApps.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/list/apps/ListFragmentApps.kt @@ -11,13 +11,16 @@ import android.widget.Toast import androidx.fragment.app.Fragment import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import de.jrpie.android.launcher.R import de.jrpie.android.launcher.apps.AppFilter import de.jrpie.android.launcher.databinding.ListAppsBinding import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.ui.UIObject +import de.jrpie.android.launcher.ui.closeSoftKeyboard import de.jrpie.android.launcher.ui.list.ListActivity import de.jrpie.android.launcher.ui.openSoftKeyboard +import kotlin.math.absoluteValue /** @@ -90,6 +93,20 @@ class ListFragmentApps : Fragment(), UIObject { } } adapter = appsRecyclerAdapter + if (LauncherPreferences.functionality().searchAutoCloseKeyboard()) { + addOnScrollListener(object : RecyclerView.OnScrollListener() { + var totalDy: Int = 0 + var threshold = (resources.displayMetrics.density * 100).toInt() + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + totalDy += dy + + if (totalDy.absoluteValue > 100) { + totalDy = 0 + closeSoftKeyboard(requireActivity()) + } + } + }) + } } binding.listAppsSearchview.setOnQueryTextListener(object : diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index 89ec086..69c7f6a 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -147,6 +147,7 @@ functionality.search_auto_launch functionality.search_web functionality.search_auto_keyboard + functionality.search_auto_close_keyboard settings_action_lock_method diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 21f25f5..ed2bb66 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -167,6 +167,7 @@ Search the web Press return while searching the app list to launch a web search. Start keyboard for search + Close keyboard when scrolling Sensitivity diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 6ef5d07..7d906ff 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -104,6 +104,10 @@ android:key="@string/settings_functionality_search_auto_open_keyboard_key" android:defaultValue="true" android:title="@string/settings_functionality_auto_keyboard" /> + Date: Tue, 1 Oct 2024 19:37:12 +0200 Subject: [PATCH 4/8] some tests --- app/src/main/AndroidManifest.xml | 1 + .../de/jrpie/android/launcher/Application.kt | 21 ++- .../de/jrpie/android/launcher/Functions.kt | 5 +- .../LauncherPreferences$Config.java | 2 + .../serialization/PreferenceSerializers.kt | 19 +++ .../jrpie/android/launcher/ui/HomeActivity.kt | 20 +++ .../ui/widgets/WidgetContainerView.kt | 12 ++ .../android/launcher/widgets/WidgetInfo.kt | 28 ++++ .../jrpie/android/launcher/widgets/Widgets.kt | 123 ++++++++++++++++++ app/src/main/res/layout/home.xml | 9 ++ app/src/main/res/values/donottranslate.xml | 1 + 11 files changed, 239 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetContainerView.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/widgets/WidgetInfo.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a5f8831..4ab7065 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -8,6 +8,7 @@ tools:ignore="QueryAllPackagesPermission" /> + >() val privateSpaceLocked = MutableLiveData() + lateinit var appWidgetHost: AppWidgetHost + lateinit var appWidgetManager: AppWidgetManager private val profileAvailabilityBroadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { @@ -103,10 +111,15 @@ class Application : android.app.Application() { torchManager = TorchManager(this) } + appWidgetHost = AppWidgetHost(this.applicationContext, APP_WIDGET_HOST_ID) + appWidgetManager = AppWidgetManager.getInstance(this.applicationContext) + + appWidgetHost.startListening() + + val preferences = PreferenceManager.getDefaultSharedPreferences(this) LauncherPreferences.init(preferences, this.resources) - // Try to restore old preferences migratePreferencesToNewVersion(this) @@ -157,4 +170,10 @@ class Application : android.app.Application() { apps.postValue(getApps(packageManager, applicationContext)) } } + + override fun onTerminate() { + appWidgetHost.stopListening() + super.onTerminate() + + } } diff --git a/app/src/main/java/de/jrpie/android/launcher/Functions.kt b/app/src/main/java/de/jrpie/android/launcher/Functions.kt index afc2c31..9679ae5 100644 --- a/app/src/main/java/de/jrpie/android/launcher/Functions.kt +++ b/app/src/main/java/de/jrpie/android/launcher/Functions.kt @@ -6,6 +6,9 @@ import android.app.role.RoleManager import android.content.ActivityNotFoundException import android.content.ClipData import android.content.ClipboardManager +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProvider +import android.appwidget.AppWidgetProviderInfo import android.content.Context import android.content.Intent import android.content.pm.LauncherApps @@ -223,4 +226,4 @@ fun copyToClipboard(context: Context, text: String) { val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager val clipData = ClipData.newPlainText("Debug Info", text) clipboardManager.setPrimaryClip(clipData) -} \ No newline at end of file +} diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java b/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java index 4653910..17624fc 100644 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java @@ -8,6 +8,7 @@ import de.jrpie.android.launcher.actions.lock.LockMethod; import de.jrpie.android.launcher.preferences.serialization.MapAbstractAppInfoStringPreferenceSerializer; import de.jrpie.android.launcher.preferences.serialization.SetAbstractAppInfoPreferenceSerializer; import de.jrpie.android.launcher.preferences.serialization.SetPinnedShortcutInfoPreferenceSerializer; +import de.jrpie.android.launcher.preferences.serialization.SetWidgetInfoSerializer; import de.jrpie.android.launcher.preferences.theme.Background; import de.jrpie.android.launcher.preferences.theme.ColorTheme; import de.jrpie.android.launcher.preferences.theme.Font; @@ -26,6 +27,7 @@ import eu.jonahbauer.android.preference.annotations.Preferences; @Preference(name = "started_time", type = long.class), // see PREFERENCE_VERSION in de.jrpie.android.launcher.preferences.Preferences.kt @Preference(name = "version_code", type = int.class, defaultValue = "-1"), + @Preference(name = "widgets", type = Set.class, serializer = SetWidgetInfoSerializer.class) }), @PreferenceGroup(name = "apps", prefix = "settings_apps_", suffix = "_key", value = { @Preference(name = "favorites", type = Set.class, serializer = SetAbstractAppInfoPreferenceSerializer.class), diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/serialization/PreferenceSerializers.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/serialization/PreferenceSerializers.kt index 3e19daf..1746e8a 100644 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/serialization/PreferenceSerializers.kt +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/serialization/PreferenceSerializers.kt @@ -4,6 +4,7 @@ package de.jrpie.android.launcher.preferences.serialization import de.jrpie.android.launcher.apps.AbstractAppInfo import de.jrpie.android.launcher.apps.PinnedShortcutInfo +import de.jrpie.android.launcher.widgets.WidgetInfo import eu.jonahbauer.android.preference.annotations.serializer.PreferenceSerializationException import eu.jonahbauer.android.preference.annotations.serializer.PreferenceSerializer import kotlinx.serialization.Serializable @@ -28,6 +29,24 @@ class SetAbstractAppInfoPreferenceSerializer : } } + +@Suppress("UNCHECKED_CAST") +class SetWidgetInfoSerializer : + PreferenceSerializer?, java.util.Set?> { + @Throws(PreferenceSerializationException::class) + override fun serialize(value: java.util.Set?): java.util.Set { + return value?.map(WidgetInfo::serialize) + ?.toHashSet() as java.util.Set + } + + @Throws(PreferenceSerializationException::class) + override fun deserialize(value: java.util.Set?): java.util.Set? { + return value?.map(java.lang.String::toString)?.map(WidgetInfo::deserialize) + ?.toHashSet() as? java.util.Set + } +} + + @Suppress("UNCHECKED_CAST") class SetPinnedShortcutInfoPreferenceSerializer : PreferenceSerializer?, java.util.Set?> { diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt index 2ab5d9f..c7c017f 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt @@ -9,9 +9,12 @@ 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 import de.jrpie.android.launcher.actions.Gesture @@ -20,7 +23,13 @@ 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, @@ -73,6 +82,17 @@ 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) + } override fun onConfigurationChanged(newConfig: Configuration) { diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetContainerView.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetContainerView.kt new file mode 100644 index 0000000..47a1480 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetContainerView.kt @@ -0,0 +1,12 @@ +package de.jrpie.android.launcher.ui.widgets + +import android.content.Context +import android.util.AttributeSet +import android.widget.LinearLayout + +// TODO: implement layout logic instead of linear layout +class WidgetContainerView(context: Context, attrs: AttributeSet?): LinearLayout(context, attrs) { + init { + orientation = VERTICAL + } +} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/WidgetInfo.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/WidgetInfo.kt new file mode 100644 index 0000000..c3d9046 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/WidgetInfo.kt @@ -0,0 +1,28 @@ +package de.jrpie.android.launcher.widgets; + +import de.jrpie.android.launcher.apps.AbstractAppInfo +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) { + fun serialize(): String { + return Json.encodeToString(this) + } + + override fun hashCode(): Int { + return id + } + + override fun equals(other: Any?): Boolean { + return (other as? WidgetInfo)?.id == id + } + companion object { + fun deserialize(serialized: String): WidgetInfo { + return Json.decodeFromString(serialized) + } + } +} diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt new file mode 100644 index 0000000..ab4f40d --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt @@ -0,0 +1,123 @@ +package de.jrpie.android.launcher.widgets + +import android.app.Activity +import android.app.Service +import android.appwidget.AppWidgetHostView +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProviderInfo +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.os.UserManager +import android.util.Log +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 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + appWidgetHost.appWidgetIds.forEach { deleteAppWidget(activity, WidgetInfo(it, 0,0)) } + } +} + +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() + + Log.i("Launcher", "Binding new widget ${appWidgetId}") + if (!appWidgetManager.bindAppWidgetIdIfAllowed( + appWidgetId, + providerInfo.provider + ) + ) { + requestAppWidgetPermission(activity, appWidgetId, providerInfo) + return null + } + 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 +} + +fun deleteAppWidget(activity: Activity, widget: WidgetInfo) { + Log.i("Launcher", "Deleting widget ${widget.id}") + val appWidgetHost = (activity.application as Application).appWidgetHost + + appWidgetHost.deleteAppWidgetId(widget.id) + + LauncherPreferences.internal().widgets( + LauncherPreferences.internal().widgets()?.also { + it.remove(widget) + } + ) +} + +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 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) + } + appWidgetManager.updateAppWidgetOptions( + widget.id, + newOptions + ) + //view.minimumWidth = widget.width + //view.minimumHeight = widget.height + + return view +} + +fun getAppWidgetProviders(context: Context): List { + return appWidgetProviders(context, (context.applicationContext as Application).appWidgetManager) +} + +fun requestAppWidgetPermission(context: Activity, widgetId: Int, info: AppWidgetProviderInfo) { + 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) +} + +fun appWidgetProviders( + context: Context, + appWidgetManager: AppWidgetManager +): List { + 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 +} + diff --git a/app/src/main/res/layout/home.xml b/app/src/main/res/layout/home.xml index ecefdea..444bd4e 100644 --- a/app/src/main/res/layout/home.xml +++ b/app/src/main/res/layout/home.xml @@ -9,6 +9,15 @@ android:longClickable="false" android:fitsSystemWindows="true" tools:context=".ui.HomeActivity"> + + + internal.started_before internal.first_startup internal.version_code + internal.widgets apps.favorites apps.hidden apps.pinned_shortcuts From f025ac12c19332c33a71b657121f68dda3d9e5ab Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Wed, 16 Apr 2025 15:31:45 +0200 Subject: [PATCH 5/8] add basic widget selection list --- app/build.gradle | 1 + app/src/main/AndroidManifest.xml | 6 +- .../launcher/actions/LauncherAction.kt | 7 +- .../jrpie/android/launcher/ui/HomeActivity.kt | 2 +- .../ui/widgets/SelectWidgetActivity.kt | 105 ++++++++++++++++++ .../res/layout/activity_select_widget.xml | 73 ++++++++++++ app/src/main/res/layout/list_widgets_row.xml | 46 ++++++++ app/src/main/res/values/strings.xml | 1 + 8 files changed, 237 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/de/jrpie/android/launcher/ui/widgets/SelectWidgetActivity.kt create mode 100644 app/src/main/res/layout/activity_select_widget.xml create mode 100644 app/src/main/res/layout/list_widgets_row.xml diff --git a/app/build.gradle b/app/build.gradle index 1a0a6fb..eaf97f7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -106,6 +106,7 @@ dependencies { implementation 'com.google.android.material:material:1.12.0' implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3") implementation "eu.jonahbauer:android-preference-annotations:1.1.2" + implementation 'androidx.activity:activity:1.10.1' annotationProcessor "eu.jonahbauer:android-preference-annotations:1.1.2" annotationProcessor "com.android.databinding:compiler:$android_plugin_version" testImplementation 'junit:junit:4.13.2' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4ab7065..579d5c1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -8,7 +8,6 @@ tools:ignore="QueryAllPackagesPermission" /> - + - + \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/actions/LauncherAction.kt b/app/src/main/java/de/jrpie/android/launcher/actions/LauncherAction.kt index 6ba467e..0cc3dd4 100644 --- a/app/src/main/java/de/jrpie/android/launcher/actions/LauncherAction.kt +++ b/app/src/main/java/de/jrpie/android/launcher/actions/LauncherAction.kt @@ -21,6 +21,8 @@ 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 kotlinx.serialization.KSerializer import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -61,7 +63,10 @@ enum class LauncherAction( "choose_from_favorites", R.string.list_other_list_favorites, R.drawable.baseline_favorite_24, - { context -> openAppsList(context, favorite = true) }, + { context -> + context.startActivity(Intent(context.applicationContext, SelectWidgetActivity::class.java)) + }, + //openAppsList(context, favorite = true) }, true ), CHOOSE_FROM_PRIVATE_SPACE( diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt index c7c017f..44947bb 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt @@ -83,7 +83,7 @@ class HomeActivity : UIObject, AppCompatActivity() { LauncherAction.SETTINGS.invoke(this) } - deleteAllWidgets(this) + // deleteAllWidgets(this) LauncherPreferences.internal().widgets().forEach { widget -> createAppWidgetView(this, widget)?.let { diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/SelectWidgetActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/SelectWidgetActivity.kt new file mode 100644 index 0000000..98dcbe7 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/SelectWidgetActivity.kt @@ -0,0 +1,105 @@ +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() { + + 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) + } +} diff --git a/app/src/main/res/layout/activity_select_widget.xml b/app/src/main/res/layout/activity_select_widget.xml new file mode 100644 index 0000000..463a317 --- /dev/null +++ b/app/src/main/res/layout/activity_select_widget.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_widgets_row.xml b/app/src/main/res/layout/list_widgets_row.xml new file mode 100644 index 0000000..120ea5b --- /dev/null +++ b/app/src/main/res/layout/list_widgets_row.xml @@ -0,0 +1,46 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ed2bb66..e225558 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -388,5 +388,6 @@ Open Source Licenses No app found to handle search. Can\'t open URL: no browser found. + Choose Widget From e1daa8d9be017c6364a3d1ba111a3d4548c8726c Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Tue, 22 Apr 2025 14:00:36 +0200 Subject: [PATCH 6/8] some progress --- app/src/main/AndroidManifest.xml | 7 +- .../launcher/actions/LauncherAction.kt | 12 +- .../launcher/preferences/Preferences.kt | 2 + .../jrpie/android/launcher/ui/HomeActivity.kt | 53 +++--- .../launcher/ui/PinShortcutActivity.kt | 16 +- .../ui/widgets/SelectWidgetActivity.kt | 105 ----------- .../ui/widgets/WidgetContainerView.kt | 126 ++++++++++++- .../widgets/manage/ManageWidgetsActivity.kt | 173 +++++++++++++++++ .../ui/widgets/manage/SelectWidgetActivity.kt | 151 +++++++++++++++ .../ui/widgets/manage/WidgetManagerView.kt | 176 ++++++++++++++++++ .../ui/widgets/manage/WidgetOverlayView.kt | 133 +++++++++++++ .../android/launcher/widgets/WidgetInfo.kt | 58 +++++- .../android/launcher/widgets/WidgetPanel.kt | 15 ++ .../launcher/widgets/WidgetPosition.kt | 58 ++++++ .../jrpie/android/launcher/widgets/Widgets.kt | 125 ++++++------- app/src/main/res/drawable/baseline_add_24.xml | 11 ++ .../res/layout/activity_manage_widgets.xml | 25 +++ .../res/layout/activity_select_widget.xml | 9 +- app/src/main/res/layout/home.xml | 9 +- .../main/res/layout/list_widgets_header.xml | 35 ++++ app/src/main/res/layout/list_widgets_row.xml | 46 +++-- .../res/layout/sample_widget_overlay_view.xml | 16 ++ app/src/main/res/values-night/styles.xml | 7 + .../res/values/attrs_widget_overlay_view.xml | 8 + app/src/main/res/values/colors.xml | 4 + app/src/main/res/values/styles.xml | 15 +- 26 files changed, 1152 insertions(+), 243 deletions(-) delete mode 100644 app/src/main/java/de/jrpie/android/launcher/ui/widgets/SelectWidgetActivity.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetsActivity.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/SelectWidgetActivity.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetManagerView.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetOverlayView.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/widgets/WidgetPanel.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/widgets/WidgetPosition.kt create mode 100644 app/src/main/res/drawable/baseline_add_24.xml create mode 100644 app/src/main/res/layout/activity_manage_widgets.xml create mode 100644 app/src/main/res/layout/list_widgets_header.xml create mode 100644 app/src/main/res/layout/sample_widget_overlay_view.xml create mode 100644 app/src/main/res/values-night/styles.xml create mode 100644 app/src/main/res/values/attrs_widget_overlay_view.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 579d5c1..841c9bd 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -8,6 +8,7 @@ tools:ignore="QueryAllPackagesPermission" /> + + - 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() } diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt index 59ecc7a..90afdfe 100644 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt @@ -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 = mutableSetOf() diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt index 44947bb..242593a 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt @@ -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.onCreate(savedInstanceState) + super.onCreate(savedInstanceState) super.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.onStart() - + super.onStart() super.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 } -} +} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/PinShortcutActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/PinShortcutActivity.kt index 71908ba..3dbdda8 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/PinShortcutActivity.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/PinShortcutActivity.kt @@ -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 } diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/SelectWidgetActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/SelectWidgetActivity.kt deleted file mode 100644 index 98dcbe7..0000000 --- a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/SelectWidgetActivity.kt +++ /dev/null @@ -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() { - - 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) - } -} diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetContainerView.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetContainerView.kt index 47a1480..e1e0462 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetContainerView.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetContainerView.kt @@ -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.. + 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.. + if (prefKey?.startsWith("internal.widgets") == true) { + findViewById(R.id.manage_widgets_container).updateWidgets(this) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + super.onCreate() + setContentView(R.layout.activity_manage_widgets) + findViewById(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(R.id.manage_widgets_container).updateWidgets(this) + } + + override fun onStart() { + super.onStart() + super.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(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 + } +} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/SelectWidgetActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/SelectWidgetActivity.kt new file mode 100644 index 0000000..036d3ca --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/SelectWidgetActivity.kt @@ -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.onStart() + super.onStart() + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + super.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() { + + 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) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetManagerView.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetManagerView.kt new file mode 100644 index 0000000..056394a --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetManagerView.kt @@ -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 + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetOverlayView.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetOverlayView.kt new file mode 100644 index 0000000..b9662f2 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetOverlayView.kt @@ -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 { + return listOf( + 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) + } + +} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/WidgetInfo.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/WidgetInfo.kt index c3d9046..7340ca8 100644 --- a/app/src/main/java/de/jrpie/android/launcher/widgets/WidgetInfo.kt +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/WidgetInfo.kt @@ -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)" + } } diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/WidgetPanel.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/WidgetPanel.kt new file mode 100644 index 0000000..c5fbb7b --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/WidgetPanel.kt @@ -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") + } +} + + + diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/WidgetPosition.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/WidgetPosition.kt new file mode 100644 index 0000000..b575665 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/WidgetPosition.kt @@ -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 + ) + + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt index ab4f40d..f518f3d 100644 --- a/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt @@ -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 { - 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 { - 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 { + 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 +} diff --git a/app/src/main/res/drawable/baseline_add_24.xml b/app/src/main/res/drawable/baseline_add_24.xml new file mode 100644 index 0000000..13267ce --- /dev/null +++ b/app/src/main/res/drawable/baseline_add_24.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/layout/activity_manage_widgets.xml b/app/src/main/res/layout/activity_manage_widgets.xml new file mode 100644 index 0000000..c77f0e3 --- /dev/null +++ b/app/src/main/res/layout/activity_manage_widgets.xml @@ -0,0 +1,25 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_select_widget.xml b/app/src/main/res/layout/activity_select_widget.xml index 463a317..82db94d 100644 --- a/app/src/main/res/layout/activity_select_widget.xml +++ b/app/src/main/res/layout/activity_select_widget.xml @@ -2,10 +2,11 @@ + android:fitsSystemWindows="true" + tools:context=".ui.widgets.manage.SelectWidgetActivity"> + app:layout_constraintTop_toBottomOf="@+id/select_widget_appbar" /> \ No newline at end of file diff --git a/app/src/main/res/layout/home.xml b/app/src/main/res/layout/home.xml index 444bd4e..0b8930c 100644 --- a/app/src/main/res/layout/home.xml +++ b/app/src/main/res/layout/home.xml @@ -9,15 +9,12 @@ android:longClickable="false" android:fitsSystemWindows="true" tools:context=".ui.HomeActivity"> - + + - + android:layout_height="match_parent" /> + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_widgets_row.xml b/app/src/main/res/layout/list_widgets_row.xml index 120ea5b..878aaad 100644 --- a/app/src/main/res/layout/list_widgets_row.xml +++ b/app/src/main/res/layout/list_widgets_row.xml @@ -8,23 +8,13 @@ android:layout_height="wrap_content" android:layout_margin="15sp"> - + @@ -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" /> + + + \ No newline at end of file diff --git a/app/src/main/res/layout/sample_widget_overlay_view.xml b/app/src/main/res/layout/sample_widget_overlay_view.xml new file mode 100644 index 0000000..b2dcb52 --- /dev/null +++ b/app/src/main/res/layout/sample_widget_overlay_view.xml @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-night/styles.xml b/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..a4f15b9 --- /dev/null +++ b/app/src/main/res/values-night/styles.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/attrs_widget_overlay_view.xml b/app/src/main/res/values/attrs_widget_overlay_view.xml new file mode 100644 index 0000000..b340016 --- /dev/null +++ b/app/src/main/res/values/attrs_widget_overlay_view.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 1e4d12b..2712036 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -11,5 +11,9 @@ #fff #9999ff #000 + #FF29B6F6 + #FF039BE5 + #FFBDBDBD + #FF757575 diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 20ccb67..84207b6 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -66,12 +66,12 @@ 0 2 + - - + + + + + + @@ -126,4 +130,9 @@ @android:anim/fade_in @android:anim/fade_out + + From 13c88122b229b415a91deb6552c96a59de7c214b Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Thu, 24 Apr 2025 04:05:22 +0200 Subject: [PATCH 7/8] turn clock into a widget --- .../LauncherPreferences$Config.java | 4 +- .../launcher/preferences/Preferences.kt | 8 ++ .../serialization/PreferenceSerializers.kt | 18 +-- .../jrpie/android/launcher/ui/HomeActivity.kt | 62 ---------- .../android/launcher/ui/widgets/ClockView.kt | 80 +++++++++++++ .../ui/widgets/WidgetContainerView.kt | 17 +-- .../widgets/manage/ManageWidgetsActivity.kt | 7 +- .../ui/widgets/manage/SelectWidgetActivity.kt | 47 ++++++-- .../ui/widgets/manage/WidgetManagerView.kt | 19 +-- .../ui/widgets/manage/WidgetOverlayView.kt | 27 ++--- .../android/launcher/widgets/AppWidget.kt | 108 ++++++++++++++++++ .../android/launcher/widgets/ClockWidget.kt | 31 +++++ .../widgets/LauncherWidgetProvider.kt | 52 +++++++++ .../jrpie/android/launcher/widgets/Widget.kt | 55 +++++++++ .../android/launcher/widgets/WidgetInfo.kt | 78 ------------- .../jrpie/android/launcher/widgets/Widgets.kt | 78 +++++-------- app/src/main/res/layout/clock.xml | 35 ++++++ app/src/main/res/layout/home.xml | 25 ---- .../res/layout/sample_widget_overlay_view.xml | 16 --- app/src/main/res/values-night/styles.xml | 7 -- .../res/values/attrs_widget_overlay_view.xml | 8 -- app/src/main/res/values/styles.xml | 5 - 22 files changed, 462 insertions(+), 325 deletions(-) create mode 100644 app/src/main/java/de/jrpie/android/launcher/ui/widgets/ClockView.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/widgets/AppWidget.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/widgets/ClockWidget.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/widgets/LauncherWidgetProvider.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/widgets/Widget.kt delete mode 100644 app/src/main/java/de/jrpie/android/launcher/widgets/WidgetInfo.kt create mode 100644 app/src/main/res/layout/clock.xml delete mode 100644 app/src/main/res/layout/sample_widget_overlay_view.xml delete mode 100644 app/src/main/res/values-night/styles.xml delete mode 100644 app/src/main/res/values/attrs_widget_overlay_view.xml diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java b/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java index 17624fc..5e51b52 100644 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java @@ -8,7 +8,7 @@ import de.jrpie.android.launcher.actions.lock.LockMethod; import de.jrpie.android.launcher.preferences.serialization.MapAbstractAppInfoStringPreferenceSerializer; import de.jrpie.android.launcher.preferences.serialization.SetAbstractAppInfoPreferenceSerializer; import de.jrpie.android.launcher.preferences.serialization.SetPinnedShortcutInfoPreferenceSerializer; -import de.jrpie.android.launcher.preferences.serialization.SetWidgetInfoSerializer; +import de.jrpie.android.launcher.preferences.serialization.SetWidgetSerializer; import de.jrpie.android.launcher.preferences.theme.Background; import de.jrpie.android.launcher.preferences.theme.ColorTheme; import de.jrpie.android.launcher.preferences.theme.Font; @@ -27,7 +27,7 @@ import eu.jonahbauer.android.preference.annotations.Preferences; @Preference(name = "started_time", type = long.class), // see PREFERENCE_VERSION in de.jrpie.android.launcher.preferences.Preferences.kt @Preference(name = "version_code", type = int.class, defaultValue = "-1"), - @Preference(name = "widgets", type = Set.class, serializer = SetWidgetInfoSerializer.class) + @Preference(name = "widgets", type = Set.class, serializer = SetWidgetSerializer.class) }), @PreferenceGroup(name = "apps", prefix = "settings_apps_", suffix = "_key", value = { @Preference(name = "favorites", type = Set.class, serializer = SetAbstractAppInfoPreferenceSerializer.class), diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt index 90afdfe..dd0155b 100644 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt @@ -13,6 +13,8 @@ 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.ClockWidget +import de.jrpie.android.launcher.widgets.WidgetPosition import de.jrpie.android.launcher.widgets.deleteAllWidgets /* Current version of the structure of preferences. @@ -74,6 +76,12 @@ fun resetPreferences(context: Context) { LauncherPreferences.internal().versionCode(PREFERENCE_VERSION) deleteAllWidgets(context) + LauncherPreferences.internal().widgets( + setOf( + ClockWidget(-500, WidgetPosition(1,4,10,3)) + ) + ) + val hidden: MutableSet = mutableSetOf() val launcher = DetailedAppInfo.fromAppInfo( diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/serialization/PreferenceSerializers.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/serialization/PreferenceSerializers.kt index 1746e8a..a2749ae 100644 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/serialization/PreferenceSerializers.kt +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/serialization/PreferenceSerializers.kt @@ -4,7 +4,7 @@ package de.jrpie.android.launcher.preferences.serialization import de.jrpie.android.launcher.apps.AbstractAppInfo import de.jrpie.android.launcher.apps.PinnedShortcutInfo -import de.jrpie.android.launcher.widgets.WidgetInfo +import de.jrpie.android.launcher.widgets.Widget import eu.jonahbauer.android.preference.annotations.serializer.PreferenceSerializationException import eu.jonahbauer.android.preference.annotations.serializer.PreferenceSerializer import kotlinx.serialization.Serializable @@ -31,18 +31,18 @@ class SetAbstractAppInfoPreferenceSerializer : @Suppress("UNCHECKED_CAST") -class SetWidgetInfoSerializer : - PreferenceSerializer?, java.util.Set?> { +class SetWidgetSerializer : + PreferenceSerializer?, java.util.Set?> { @Throws(PreferenceSerializationException::class) - override fun serialize(value: java.util.Set?): java.util.Set { - return value?.map(WidgetInfo::serialize) - ?.toHashSet() as java.util.Set + override fun serialize(value: java.util.Set?): java.util.Set? { + return value?.map(Widget::serialize) + ?.toHashSet() as? java.util.Set } @Throws(PreferenceSerializationException::class) - override fun deserialize(value: java.util.Set?): java.util.Set? { - return value?.map(java.lang.String::toString)?.map(WidgetInfo::deserialize) - ?.toHashSet() as? java.util.Set + override fun deserialize(value: java.util.Set?): java.util.Set? { + return value?.map(java.lang.String::toString)?.map(Widget::deserialize) + ?.toHashSet() as? java.util.Set } } diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt index 242593a..2900b0a 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt @@ -11,7 +11,6 @@ import android.view.KeyEvent import android.view.MotionEvent import android.view.View import android.window.OnBackInvokedDispatcher -import androidx.core.view.isVisible import de.jrpie.android.launcher.Application import de.jrpie.android.launcher.R import de.jrpie.android.launcher.actions.Action @@ -21,7 +20,6 @@ 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 java.util.Locale /** * [HomeActivity] is the actual application Launcher, @@ -134,44 +132,6 @@ class HomeActivity : UIObject, Activity() { } } - private fun initClock() { - val locale = Locale.getDefault() - val dateVisible = LauncherPreferences.clock().dateVisible() - val timeVisible = LauncherPreferences.clock().timeVisible() - - var dateFMT = "yyyy-MM-dd" - var timeFMT = "HH:mm" - if (LauncherPreferences.clock().showSeconds()) { - timeFMT += ":ss" - } - - if (LauncherPreferences.clock().localized()) { - dateFMT = android.text.format.DateFormat.getBestDateTimePattern(locale, dateFMT) - timeFMT = android.text.format.DateFormat.getBestDateTimePattern(locale, timeFMT) - } - - var upperFormat = dateFMT - var lowerFormat = timeFMT - var upperVisible = dateVisible - var lowerVisible = timeVisible - - if (LauncherPreferences.clock().flipDateTime()) { - upperFormat = lowerFormat.also { lowerFormat = upperFormat } - upperVisible = lowerVisible.also { lowerVisible = upperVisible } - } - - binding.homeUpperView.isVisible = upperVisible - binding.homeLowerView.isVisible = lowerVisible - - binding.homeUpperView.setTextColor(LauncherPreferences.clock().color()) - binding.homeLowerView.setTextColor(LauncherPreferences.clock().color()) - - binding.homeLowerView.format24Hour = lowerFormat - binding.homeUpperView.format24Hour = upperFormat - binding.homeLowerView.format12Hour = lowerFormat - binding.homeUpperView.format12Hour = upperFormat - } - override fun getTheme(): Resources.Theme { val mTheme = modifyTheme(super.getTheme()) mTheme.applyStyle(R.style.backgroundWallpaper, true) @@ -209,8 +169,6 @@ class HomeActivity : UIObject, Activity() { windowInsets } } - - initClock() updateSettingsFallbackButtonVisibility() } @@ -260,26 +218,6 @@ class HomeActivity : UIObject, Activity() { return true } - override fun setOnClicks() { - - binding.homeUpperView.setOnClickListener { - if (LauncherPreferences.clock().flipDateTime()) { - Gesture.TIME(this) - } else { - Gesture.DATE(this) - } - } - - binding.homeLowerView.setOnClickListener { - if (LauncherPreferences.clock().flipDateTime()) { - Gesture.DATE(this) - } else { - Gesture.TIME(this) - } - } - } - - private fun handleBack() { Gesture.BACK(this) } diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/ClockView.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/ClockView.kt new file mode 100644 index 0000000..33c4888 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/ClockView.kt @@ -0,0 +1,80 @@ +package de.jrpie.android.launcher.ui.widgets + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.isVisible +import de.jrpie.android.launcher.actions.Gesture +import de.jrpie.android.launcher.databinding.ClockBinding +import de.jrpie.android.launcher.preferences.LauncherPreferences +import java.util.Locale + +class ClockView(context: Context, attrs: AttributeSet? = null, val appWidgetId: Int): ConstraintLayout(context, attrs) { + + val binding: ClockBinding = ClockBinding.inflate(LayoutInflater.from(context), this, true) + init { + initClock() + setOnClicks() + } + + + private fun initClock() { + val locale = Locale.getDefault() + val dateVisible = LauncherPreferences.clock().dateVisible() + val timeVisible = LauncherPreferences.clock().timeVisible() + + var dateFMT = "yyyy-MM-dd" + var timeFMT = "HH:mm" + if (LauncherPreferences.clock().showSeconds()) { + timeFMT += ":ss" + } + + if (LauncherPreferences.clock().localized()) { + dateFMT = android.text.format.DateFormat.getBestDateTimePattern(locale, dateFMT) + timeFMT = android.text.format.DateFormat.getBestDateTimePattern(locale, timeFMT) + } + + var upperFormat = dateFMT + var lowerFormat = timeFMT + var upperVisible = dateVisible + var lowerVisible = timeVisible + + if (LauncherPreferences.clock().flipDateTime()) { + upperFormat = lowerFormat.also { lowerFormat = upperFormat } + upperVisible = lowerVisible.also { lowerVisible = upperVisible } + } + + binding.clockUpperView.isVisible = upperVisible + binding.clockLowerView.isVisible = lowerVisible + + binding.clockUpperView.setTextColor(LauncherPreferences.clock().color()) + binding.clockLowerView.setTextColor(LauncherPreferences.clock().color()) + + binding.clockLowerView.format24Hour = lowerFormat + binding.clockUpperView.format24Hour = upperFormat + binding.clockLowerView.format12Hour = lowerFormat + binding.clockUpperView.format12Hour = upperFormat + } + + fun setOnClicks() { + binding.clockUpperView.setOnClickListener { + if (LauncherPreferences.clock().flipDateTime()) { + Gesture.TIME(context) + } else { + Gesture.DATE(context) + } + } + + binding.clockLowerView.setOnClickListener { + if (LauncherPreferences.clock().flipDateTime()) { + Gesture.DATE(context) + } else { + Gesture.TIME(context) + } + } + } + + + +} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetContainerView.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetContainerView.kt index e1e0462..5371c63 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetContainerView.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetContainerView.kt @@ -12,7 +12,6 @@ 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 @@ -25,22 +24,8 @@ open class WidgetContainerView(context: Context, attrs: AttributeSet? = null) : open fun updateWidgets(activity: Activity) { Log.i("WidgetContainer", "updating ${activity.localClassName}") (0.. - 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}") - } + widget.createView(activity)?.let { addView(it, WidgetContainerView.Companion.LayoutParams(widget.position)) } } diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetsActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetsActivity.kt index 73c81ff..b81e685 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetsActivity.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetsActivity.kt @@ -17,9 +17,8 @@ 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.AppWidget import de.jrpie.android.launcher.widgets.WidgetPosition -import de.jrpie.android.launcher.widgets.deleteAppWidget import kotlin.math.min @@ -111,7 +110,7 @@ class ManageWidgetsActivity : Activity(), UIObject { display.height ) - val widget = WidgetInfo(appWidgetId, provider, position) + val widget = AppWidget(appWidgetId, provider, position) LauncherPreferences.internal().widgets( (LauncherPreferences.internal().widgets() ?: HashSet()).also { it.add(widget) @@ -158,7 +157,7 @@ class ManageWidgetsActivity : Activity(), UIObject { val appWidgetId = data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1) if (appWidgetId != -1) { - deleteAppWidget(this, WidgetInfo(appWidgetId)) + AppWidget(appWidgetId).delete(this) } } } diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/SelectWidgetActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/SelectWidgetActivity.kt index 036d3ca..0a2bee8 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/SelectWidgetActivity.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/SelectWidgetActivity.kt @@ -23,8 +23,16 @@ 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.ClockWidget +import de.jrpie.android.launcher.widgets.LauncherAppWidgetProvider +import de.jrpie.android.launcher.widgets.LauncherClockWidgetProvider +import de.jrpie.android.launcher.widgets.LauncherWidgetProvider +import de.jrpie.android.launcher.widgets.Widget +import de.jrpie.android.launcher.widgets.WidgetPosition import de.jrpie.android.launcher.widgets.bindAppWidgetOrRequestPermission +import de.jrpie.android.launcher.widgets.getAppWidgetHost import de.jrpie.android.launcher.widgets.getAppWidgetProviders +import de.jrpie.android.launcher.widgets.updateWidget private const val REQUEST_WIDGET_PERMISSION = 29 @@ -38,15 +46,29 @@ 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) + private fun tryBindWidget(info: LauncherWidgetProvider) { + when (info) { + is LauncherAppWidgetProvider -> { + if (bindAppWidgetOrRequestPermission( + this, + info.info, + widgetId, + REQUEST_WIDGET_PERMISSION + ) + ) { + setResult( + RESULT_OK, + Intent().also { + it.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId) + } + ) + finish() } - ) - finish() + } + is LauncherClockWidgetProvider -> { + updateWidget(ClockWidget(widgetId, WidgetPosition(0,4,12,3))) + finish() + } } } @@ -64,6 +86,9 @@ class SelectWidgetActivity : AppCompatActivity(), UIObject { widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1) + if (widgetId == -1) { + widgetId = getAppWidgetHost().allocateAppWidgetId() + } val viewManager = LinearLayoutManager(this) val viewAdapter = SelectWidgetRecyclerAdapter() @@ -87,7 +112,7 @@ class SelectWidgetActivity : AppCompatActivity(), UIObject { data ?: return Log.i("SelectWidget", "permission granted") val provider = (data.getSerializableExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER) as? AppWidgetProviderInfo) ?: return - tryBindWidget(provider) + tryBindWidget(LauncherAppWidgetProvider(provider)) } } @@ -121,9 +146,9 @@ class SelectWidgetActivity : AppCompatActivity(), UIObject { "" } val preview = - widgets[i].loadPreviewImage(this@SelectWidgetActivity, DisplayMetrics.DENSITY_DEFAULT ) + widgets[i].loadPreviewImage(this@SelectWidgetActivity) val icon = - widgets[i].loadIcon(this@SelectWidgetActivity, DisplayMetrics.DENSITY_DEFAULT) + widgets[i].loadIcon(this@SelectWidgetActivity) viewHolder.textView.text = label viewHolder.descriptionView.text = description diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetManagerView.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetManagerView.kt index 056394a..3ed86b0 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetManagerView.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetManagerView.kt @@ -20,8 +20,8 @@ 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.Widget 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 @@ -91,9 +91,7 @@ class WidgetManagerView(context: Context, attrs: AttributeSet? = null) : 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 + selectedWidgetView = Widget.byId(view.widgetId)?.findView(children) ?: return true startWidgetPosition = position val positionInView = start.minus(Point(position.left, position.top)) @@ -127,17 +125,18 @@ class WidgetManagerView(context: Context, attrs: AttributeSet? = null) : ) if (newPosition != lastPosition) { lastPosition = absoluteNewPosition + (view.layoutParams as Companion.LayoutParams).position = newPosition + (selectedWidgetView?.layoutParams as? Companion.LayoutParams)?.position = newPosition + requestLayout() 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 + val widget = Widget.byId(id) ?: return true widget.position = newPosition endInteraction() updateWidget(widget) @@ -154,12 +153,6 @@ class WidgetManagerView(context: Context, attrs: AttributeSet? = null) : 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) { diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetOverlayView.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetOverlayView.kt index b9662f2..1b5eddb 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetOverlayView.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetOverlayView.kt @@ -2,20 +2,14 @@ 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 +import de.jrpie.android.launcher.widgets.Widget /** * An overlay to show configuration options for a widget. @@ -33,27 +27,22 @@ class WidgetOverlayView : View { class Handle(val mode: WidgetManagerView.EditMode, val position: Rect) init { handlePaint.style = Paint.Style.STROKE - handlePaint.setARGB(100, 255, 255, 255) + handlePaint.setARGB(255, 255, 255, 255) selectedHandlePaint.style = Paint.Style.FILL_AND_STROKE - selectedHandlePaint.setARGB(255, 255, 255, 255) + selectedHandlePaint.setARGB(100, 255, 255, 255) paint.style = Paint.Style.STROKE - paint.setARGB(50, 255, 255, 255) + paint.setARGB(255, 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) + preview = Widget.byId(widgetId)?.getPreview(context) } constructor(context: Context) : super(context) { @@ -91,8 +80,8 @@ class WidgetOverlayView : View { return } - preview?.bounds = bounds - preview?.draw(canvas) + //preview?.bounds = bounds + //preview?.draw(canvas) } @@ -101,7 +90,7 @@ class WidgetOverlayView : View { val menu = PopupMenu(context, this) menu.menu.let { it.add("Remove").setOnMenuItemClickListener { _ -> - deleteAppWidget(context, WidgetInfo(widgetId)) + Widget.byId(widgetId)?.delete(context) return@setOnMenuItemClickListener true } it.add("Allow Interaction").setOnMenuItemClickListener { _ -> diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/AppWidget.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/AppWidget.kt new file mode 100644 index 0000000..fc816cc --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/AppWidget.kt @@ -0,0 +1,108 @@ +package de.jrpie.android.launcher.widgets; + +import android.app.Activity +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.graphics.drawable.Drawable +import android.os.Build +import android.os.Bundle +import android.util.AttributeSet +import android.util.DisplayMetrics +import android.util.Log +import android.util.SizeF +import android.view.View +import de.jrpie.android.launcher.Application +import de.jrpie.android.launcher.preferences.LauncherPreferences +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +@Serializable +@SerialName("widget:app") +class AppWidget( + override val id: Int, + override 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 +): Widget() { + + + constructor(id: Int, widgetProviderInfo: AppWidgetProviderInfo, position: WidgetPosition) : + this( + id, position, + widgetProviderInfo.provider.packageName, + widgetProviderInfo.provider.className, + widgetProviderInfo.profile.hashCode() + ) + + /** + * Get the [AppWidgetProviderInfo] by [id]. + * If the widget is not installed, use [restoreAppWidgetProviderInfo] instead. + */ + fun getAppWidgetProviderInfo(context: Context): AppWidgetProviderInfo? { + if (id < 0) { + return null + } + 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)" + } + + override fun createView(activity: Activity): AppWidgetHostView? { + val providerInfo = activity.getAppWidgetManager().getAppWidgetInfo(id) ?: return null + val view = activity.getAppWidgetHost() + .createView(activity, this.id, providerInfo) + + val dp = activity.resources.displayMetrics.density + val screenWidth = activity.resources.displayMetrics.widthPixels + val screenHeight = activity.resources.displayMetrics.heightPixels + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val absolutePosition = position.getAbsoluteRect(screenWidth, screenHeight) + view.updateAppWidgetSize(Bundle.EMPTY, + listOf(SizeF( + absolutePosition.width() / dp, + absolutePosition.height() / dp + ))) + } + view.setPadding(0,0,0,0) + return view + } + + override fun findView(views: Sequence): AppWidgetHostView? { + return views.mapNotNull { it as? AppWidgetHostView }.firstOrNull { it.appWidgetId == id } + } + + override fun getIcon(context: Context): Drawable? { + return context.getAppWidgetManager().getAppWidgetInfo(id)?.loadIcon(context, DisplayMetrics.DENSITY_HIGH) + } + + override fun getPreview(context: Context): Drawable? { + return context.getAppWidgetManager().getAppWidgetInfo(id)?.loadPreviewImage(context, DisplayMetrics.DENSITY_HIGH) + } +} diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/ClockWidget.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/ClockWidget.kt new file mode 100644 index 0000000..7accfe4 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/ClockWidget.kt @@ -0,0 +1,31 @@ +package de.jrpie.android.launcher.widgets + +import android.app.Activity +import android.content.Context +import android.graphics.drawable.Drawable +import android.view.View +import de.jrpie.android.launcher.ui.widgets.ClockView +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + + +@Serializable +@SerialName("widget:clock") +class ClockWidget(override val id: Int, override var position: WidgetPosition): Widget() { + + override fun createView(activity: Activity): View? { + return ClockView(activity, null, id) + } + + override fun findView(views: Sequence): ClockView? { + return views.mapNotNull { it as? ClockView }.firstOrNull { it.appWidgetId == id } + } + + override fun getPreview(context: Context): Drawable? { + return null + } + + override fun getIcon(context: Context): Drawable? { + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/LauncherWidgetProvider.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/LauncherWidgetProvider.kt new file mode 100644 index 0000000..2755fe8 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/LauncherWidgetProvider.kt @@ -0,0 +1,52 @@ +package de.jrpie.android.launcher.widgets + +import android.appwidget.AppWidgetProviderInfo +import android.content.Context +import android.graphics.drawable.Drawable +import android.os.Build +import android.util.DisplayMetrics + +sealed class LauncherWidgetProvider { + abstract val label: String? + + abstract fun loadPreviewImage(context: Context): Drawable? + abstract fun loadIcon(context: Context): Drawable? + abstract fun loadDescription(context: Context): CharSequence? +} + +class LauncherAppWidgetProvider(val info: AppWidgetProviderInfo) : LauncherWidgetProvider() { + override val label: String? = info.label + override fun loadPreviewImage(context: Context): Drawable? { + return info.loadPreviewImage(context, DisplayMetrics.DENSITY_DEFAULT) + } + + override fun loadIcon(context: Context): Drawable? { + return info.loadIcon(context, DisplayMetrics.DENSITY_DEFAULT) + } + + override fun loadDescription(context: Context): CharSequence? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + info.loadDescription(context) + } else { + null + } + } + +} +class LauncherClockWidgetProvider : LauncherWidgetProvider() { + override val label: String? + get() = "Clock" + + override fun loadPreviewImage(context: Context): Drawable? { + return null + } + + override fun loadIcon(context: Context): Drawable? { + return null + } + + override fun loadDescription(context: Context): CharSequence? { + return null + } +} + diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/Widget.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/Widget.kt new file mode 100644 index 0000000..e177baf --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/Widget.kt @@ -0,0 +1,55 @@ +package de.jrpie.android.launcher.widgets + +import android.app.Activity +import android.content.Context +import android.graphics.drawable.Drawable +import android.view.View +import de.jrpie.android.launcher.preferences.LauncherPreferences +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json + + +@Serializable +sealed class Widget { + abstract val id: Int + abstract var position: WidgetPosition + + /** + * @param activity The activity where the view will be used. Must not be an AppCompatActivity. + */ + abstract fun createView(activity: Activity): View? + abstract fun findView(views: Sequence): View? + abstract fun getPreview(context: Context): Drawable? + abstract fun getIcon(context: Context): Drawable? + + fun delete(context: Context) { + context.getAppWidgetHost().deleteAppWidgetId(id) + + LauncherPreferences.internal().widgets( + LauncherPreferences.internal().widgets()?.also { + it.remove(this) + } + ) + } + + override fun hashCode(): Int { + return id + } + + override fun equals(other: Any?): Boolean { + return (other as? Widget)?.id == id + } + + fun serialize(): String { + return Json.encodeToString(serializer(), this) + } + companion object { + fun deserialize(serialized: String): Widget { + return Json.decodeFromString(serialized) + } + fun byId(id: Int): Widget? { + return (LauncherPreferences.internal().widgets() ?: setOf()) + .firstOrNull { it.id == id } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/WidgetInfo.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/WidgetInfo.kt deleted file mode 100644 index 7340ca8..0000000 --- a/app/src/main/java/de/jrpie/android/launcher/widgets/WidgetInfo.kt +++ /dev/null @@ -1,78 +0,0 @@ -package de.jrpie.android.launcher.widgets; - -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, - 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) - } - - override fun hashCode(): Int { - return id - } - - 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)" - } -} diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt index f518f3d..485045f 100644 --- a/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt @@ -19,10 +19,19 @@ import de.jrpie.android.launcher.preferences.LauncherPreferences fun deleteAllWidgets(context: Context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - context.getAppWidgetHost().appWidgetIds.forEach { deleteAppWidget(context, WidgetInfo(it)) } + context.getAppWidgetHost().appWidgetIds.forEach { AppWidget(it).delete(context) } } } +/** + * Tries to bind [providerInfo] to the id [id]. + * @param providerInfo The widget to be bound. + * @param id The id to bind the widget to. If -1 is provided, a new id is allocated. + * @param + * @param requestCode Used to start an activity to request permission to bind the widget. + * + * @return true iff the app widget was bound successfully. + */ fun bindAppWidgetOrRequestPermission(activity: Activity, providerInfo: AppWidgetProviderInfo, id: Int, requestCode: Int? = null): Boolean { val appWidgetId = if(id == -1) { activity.getAppWidgetHost().allocateAppWidgetId() @@ -34,50 +43,20 @@ fun bindAppWidgetOrRequestPermission(activity: Activity, providerInfo: AppWidget providerInfo.provider ) ) { - Log.e("Launcher", "not allowed to bind widget") - requestAppWidgetPermission(activity, appWidgetId, providerInfo, requestCode) + Log.i("Widgets", "requesting permission for widget") + val intent = Intent(AppWidgetManager.ACTION_APPWIDGET_BIND).apply { + putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,appWidgetId) + putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER, providerInfo.provider) + } + activity.startActivityForResult(intent, requestCode ?: 0) return false } return true } -fun deleteAppWidget(context: Context, widget: WidgetInfo) { - Log.i("Launcher", "Deleting widget ${widget.id}") - val appWidgetHost = (context.applicationContext as Application).appWidgetHost - appWidgetHost.deleteAppWidgetId(widget.id) - - LauncherPreferences.internal().widgets( - LauncherPreferences.internal().widgets()?.also { - it.remove(widget) - } - ) -} - -fun createAppWidgetView(activity: Activity, widget: WidgetInfo): AppWidgetHostView? { - val providerInfo = activity.getAppWidgetManager().getAppWidgetInfo(widget.id) ?: return null - - val dp = activity.resources.displayMetrics.density - - 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))) - } - view.setPadding(0,0,0,0) - return view -} - -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, requestCode ?: 0) -} - -fun getAppWidgetProviders( context: Context ): List { +fun getAppWidgetProviders( context: Context ): List { + val list = mutableListOf(LauncherClockWidgetProvider()) val appWidgetManager = context.getAppWidgetManager() val profiles = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @@ -85,28 +64,27 @@ fun getAppWidgetProviders( context: Context ): List { } else { (context.getSystemService(Service.USER_SERVICE) as UserManager).userProfiles } - Log.i("Widgets", "profiles: ${profiles.size}, $profiles") - - return profiles.map { + list.addAll( + profiles.map { appWidgetManager.getInstalledProvidersForProfile(it) + .map { LauncherAppWidgetProvider(it) } }.flatten() + ) + + + return list } -fun getWidgetById(id: Int): WidgetInfo? { - return (LauncherPreferences.internal().widgets() ?: setOf()).firstOrNull { - it.id == id - } -} -fun updateWidget(widget: WidgetInfo) { +fun updateWidget(widget: Widget) { var widgets = LauncherPreferences.internal().widgets() ?: setOf() widgets = widgets.minus(widget).plus(widget) LauncherPreferences.internal().widgets(widgets) } -private fun Context.getAppWidgetHost(): AppWidgetHost { +fun Context.getAppWidgetHost(): AppWidgetHost { return (this.applicationContext as Application).appWidgetHost } -private fun Context.getAppWidgetManager(): AppWidgetManager { +fun Context.getAppWidgetManager(): AppWidgetManager { return (this.applicationContext as Application).appWidgetManager } diff --git a/app/src/main/res/layout/clock.xml b/app/src/main/res/layout/clock.xml new file mode 100644 index 0000000..d81fc5f --- /dev/null +++ b/app/src/main/res/layout/clock.xml @@ -0,0 +1,35 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/home.xml b/app/src/main/res/layout/home.xml index 0b8930c..717151f 100644 --- a/app/src/main/res/layout/home.xml +++ b/app/src/main/res/layout/home.xml @@ -10,36 +10,11 @@ android:fitsSystemWindows="true" tools:context=".ui.HomeActivity"> - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values-night/styles.xml b/app/src/main/res/values-night/styles.xml deleted file mode 100644 index a4f15b9..0000000 --- a/app/src/main/res/values-night/styles.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/values/attrs_widget_overlay_view.xml b/app/src/main/res/values/attrs_widget_overlay_view.xml deleted file mode 100644 index b340016..0000000 --- a/app/src/main/res/values/attrs_widget_overlay_view.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 84207b6..c5b7252 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -130,9 +130,4 @@ @android:anim/fade_in @android:anim/fade_out - - From 72f77c8294964a68ee1fedeb76a63a887ed0e651 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Thu, 24 Apr 2025 14:34:27 +0200 Subject: [PATCH 8/8] add option to enable / disable interaction with widgets --- .../de/jrpie/android/launcher/Application.kt | 5 ++ .../launcher/actions/LauncherAction.kt | 13 ++--- .../LauncherPreferences$Config.java | 4 +- .../launcher/preferences/Preferences.kt | 2 +- .../jrpie/android/launcher/ui/HomeActivity.kt | 26 ++++------ .../launcher/SettingsFragmentLauncher.kt | 9 ++++ .../ui/widgets/WidgetContainerView.kt | 50 +++++++++++++------ .../widgets/manage/ManageWidgetsActivity.kt | 45 +++++++++-------- .../ui/widgets/manage/SelectWidgetActivity.kt | 10 +--- .../ui/widgets/manage/WidgetManagerView.kt | 21 ++++---- .../ui/widgets/manage/WidgetOverlayView.kt | 29 +++++++---- .../android/launcher/widgets/AppWidget.kt | 30 +++++++---- .../android/launcher/widgets/ClockWidget.kt | 12 ++++- .../widgets/LauncherWidgetProvider.kt | 26 ++++++---- .../jrpie/android/launcher/widgets/Widget.kt | 17 ++++--- .../jrpie/android/launcher/widgets/Widgets.kt | 7 +-- .../main/res/drawable/baseline_clock_24.xml | 15 ++++++ app/src/main/res/values/donottranslate.xml | 2 +- app/src/main/res/values/strings.xml | 11 ++++ app/src/main/res/xml/preferences.xml | 4 ++ 20 files changed, 214 insertions(+), 124 deletions(-) create mode 100644 app/src/main/res/drawable/baseline_clock_24.xml diff --git a/app/src/main/java/de/jrpie/android/launcher/Application.kt b/app/src/main/java/de/jrpie/android/launcher/Application.kt index 3c2e3bc..775621c 100644 --- a/app/src/main/java/de/jrpie/android/launcher/Application.kt +++ b/app/src/main/java/de/jrpie/android/launcher/Application.kt @@ -22,6 +22,8 @@ import de.jrpie.android.launcher.apps.isPrivateSpaceLocked import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.preferences.migratePreferencesToNewVersion import de.jrpie.android.launcher.preferences.resetPreferences +import de.jrpie.android.launcher.widgets.LauncherWidgetProvider +import de.jrpie.android.launcher.widgets.Widget import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -32,6 +34,7 @@ const val APP_WIDGET_HOST_ID = 42; class Application : android.app.Application() { val apps = MutableLiveData>() + val widgets = MutableLiveData>() val privateSpaceLocked = MutableLiveData() lateinit var appWidgetHost: AppWidgetHost lateinit var appWidgetManager: AppWidgetManager @@ -98,6 +101,8 @@ class Application : android.app.Application() { customAppNames = LauncherPreferences.apps().customNames() } else if (pref == LauncherPreferences.apps().keys().pinnedShortcuts()) { loadApps() + } else if (pref == LauncherPreferences.widgets().keys().widgets()) { + widgets.postValue(LauncherPreferences.widgets().widgets() ?: setOf()) } } diff --git a/app/src/main/java/de/jrpie/android/launcher/actions/LauncherAction.kt b/app/src/main/java/de/jrpie/android/launcher/actions/LauncherAction.kt index aa758a2..6ba467e 100644 --- a/app/src/main/java/de/jrpie/android/launcher/actions/LauncherAction.kt +++ b/app/src/main/java/de/jrpie/android/launcher/actions/LauncherAction.kt @@ -15,13 +15,12 @@ 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.widgets.manage.ManageWidgetsActivity -import de.jrpie.android.launcher.ui.widgets.manage.SelectWidgetActivity import kotlinx.serialization.KSerializer import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -62,10 +61,7 @@ enum class LauncherAction( "choose_from_favorites", R.string.list_other_list_favorites, R.drawable.baseline_favorite_24, - { context -> - context.startActivity(Intent(context.applicationContext, ManageWidgetsActivity::class.java)) - }, - //openAppsList(context, favorite = true) }, + { context -> openAppsList(context, favorite = true) }, true ), CHOOSE_FROM_PRIVATE_SPACE( @@ -73,15 +69,12 @@ 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() } diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java b/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java index 5e51b52..575346a 100644 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java @@ -27,7 +27,6 @@ import eu.jonahbauer.android.preference.annotations.Preferences; @Preference(name = "started_time", type = long.class), // see PREFERENCE_VERSION in de.jrpie.android.launcher.preferences.Preferences.kt @Preference(name = "version_code", type = int.class, defaultValue = "-1"), - @Preference(name = "widgets", type = Set.class, serializer = SetWidgetSerializer.class) }), @PreferenceGroup(name = "apps", prefix = "settings_apps_", suffix = "_key", value = { @Preference(name = "favorites", type = Set.class, serializer = SetAbstractAppInfoPreferenceSerializer.class), @@ -84,5 +83,8 @@ import eu.jonahbauer.android.preference.annotations.Preferences; @PreferenceGroup(name = "actions", prefix = "settings_actions_", suffix = "_key", value = { @Preference(name = "lock_method", type = LockMethod.class, defaultValue = "DEVICE_ADMIN"), }), + @PreferenceGroup(name = "widgets", prefix = "settings_widgets_", suffix= "_key", value = { + @Preference(name = "widgets", type = Set.class, serializer = SetWidgetSerializer.class) + }), }) public final class LauncherPreferences$Config {} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt index dd0155b..332f4df 100644 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt @@ -76,7 +76,7 @@ fun resetPreferences(context: Context) { LauncherPreferences.internal().versionCode(PREFERENCE_VERSION) deleteAllWidgets(context) - LauncherPreferences.internal().widgets( + LauncherPreferences.widgets().widgets( setOf( ClockWidget(-500, WidgetPosition(1,4,10,3)) ) diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt index 2900b0a..3f1e497 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt @@ -44,16 +44,14 @@ class HomeActivity : UIObject, Activity() { prefKey?.startsWith("display.") == true ) { recreate() - } - - if (prefKey?.startsWith("action.") == true) { + } else if (prefKey?.startsWith("action.") == true) { updateSettingsFallbackButtonVisibility() + } else if (prefKey == LauncherPreferences.widgets().keys().widgets()) { + binding.homeWidgetContainer.updateWidgets(this@HomeActivity, + LauncherPreferences.widgets().widgets() + ) } - if (prefKey?.startsWith("internal.widgets") == true) { - binding.homeWidgetContainer.updateWidgets(this) - - } } override fun onCreate(savedInstanceState: Bundle?) { @@ -77,7 +75,6 @@ class HomeActivity : UIObject, Activity() { binding.buttonFallbackSettings.setOnClickListener { LauncherAction.SETTINGS.invoke(this) } - binding.homeWidgetContainer.updateWidgets(this) } override fun onConfigurationChanged(newConfig: Configuration) { @@ -98,7 +95,6 @@ class HomeActivity : UIObject, Activity() { .registerOnSharedPreferenceChangeListener(sharedPreferencesListener) (application as Application).appWidgetHost.startListening() - binding.homeWidgetContainer.updateWidgets(this) } @@ -170,6 +166,10 @@ class HomeActivity : UIObject, Activity() { } } updateSettingsFallbackButtonVisibility() + + binding.homeWidgetContainer.updateWidgets(this@HomeActivity, + LauncherPreferences.widgets().widgets() + ) } override fun onDestroy() { @@ -206,12 +206,6 @@ class HomeActivity : UIObject, Activity() { 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) @@ -225,4 +219,4 @@ class HomeActivity : UIObject, Activity() { override fun isHomeScreen(): Boolean { return true } -} \ No newline at end of file +} diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/settings/launcher/SettingsFragmentLauncher.kt b/app/src/main/java/de/jrpie/android/launcher/ui/settings/launcher/SettingsFragmentLauncher.kt index a8efb43..8907f04 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/settings/launcher/SettingsFragmentLauncher.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/settings/launcher/SettingsFragmentLauncher.kt @@ -11,6 +11,7 @@ import de.jrpie.android.launcher.actions.openAppsList import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.preferences.theme.ColorTheme import de.jrpie.android.launcher.setDefaultHomeScreen +import de.jrpie.android.launcher.ui.widgets.manage.ManageWidgetsActivity /** @@ -81,6 +82,14 @@ class SettingsFragmentLauncher : PreferenceFragmentCompat() { true } + val manageWidgets = findPreference( + LauncherPreferences.widgets().keys().widgets() + ) + manageWidgets?.setOnPreferenceClickListener { + startActivity(Intent(requireActivity(), ManageWidgetsActivity::class.java)) + true + } + val hiddenApps = findPreference( LauncherPreferences.apps().keys().hidden() ) diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetContainerView.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetContainerView.kt index 5371c63..04668ca 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetContainerView.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetContainerView.kt @@ -2,34 +2,60 @@ 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.graphics.PointF +import android.graphics.RectF import android.util.AttributeSet import android.util.Log -import android.util.SizeF +import android.view.MotionEvent +import android.view.View import android.view.View.MeasureSpec.makeMeasureSpec import android.view.ViewGroup +import androidx.core.graphics.contains import androidx.core.view.size -import de.jrpie.android.launcher.preferences.LauncherPreferences +import de.jrpie.android.launcher.widgets.Widget import de.jrpie.android.launcher.widgets.WidgetPosition import kotlin.math.max -// TODO: implement layout logic instead of linear layout /** * This only works in an Activity, not AppCompatActivity */ open class WidgetContainerView(context: Context, attrs: AttributeSet? = null) : ViewGroup(context, attrs) { - open fun updateWidgets(activity: Activity) { + var widgetViewById = HashMap() + + open fun updateWidgets(activity: Activity, widgets: Set?) { + if (widgets == null) { + return + } Log.i("WidgetContainer", "updating ${activity.localClassName}") + widgetViewById.clear() (0.. + widgets.forEach { widget -> widget.createView(activity)?.let { addView(it, WidgetContainerView.Companion.LayoutParams(widget.position)) + widgetViewById.put(widget.id, it) } } } + + override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { + if (ev == null) { + return false + } + val position = PointF(ev.x, ev.y) + + return widgetViewById.filter { + RectF( + it.value.x, + it.value.y, + it.value.x + it.value.width, + it.value.y + it.value.height + ).contains(position) == true + }.any { + Widget.byId(context, it.key)?.allowInteraction == false + } + } override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { var maxHeight = suggestedMinimumHeight @@ -70,15 +96,11 @@ open class WidgetContainerView(context: Context, attrs: AttributeSet? = null) : override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { for (i in 0.. - if (prefKey?.startsWith("internal.widgets") == true) { - findViewById(R.id.manage_widgets_container).updateWidgets(this) + if (prefKey == LauncherPreferences.widgets().keys().widgets()) { + // We can't observe the livedata because this is not an AppCompatActivity + findViewById(R.id.manage_widgets_container).updateWidgets(this, + LauncherPreferences.widgets().widgets() + ) } } @@ -50,7 +54,9 @@ class ManageWidgetsActivity : Activity(), UIObject { insets } - findViewById(R.id.manage_widgets_container).updateWidgets(this) + findViewById(R.id.manage_widgets_container).updateWidgets(this, + (application as Application).widgets.value + ) } override fun onStart() { @@ -60,6 +66,14 @@ class ManageWidgetsActivity : Activity(), UIObject { LauncherPreferences.getSharedPreferences() .registerOnSharedPreferenceChangeListener(sharedPreferencesListener) + } + + override fun onResume() { + super.onResume() + findViewById(R.id.manage_widgets_container).updateWidgets(this, + LauncherPreferences.widgets().widgets() + ) + } override fun getTheme(): Resources.Theme { val mTheme = modifyTheme(super.getTheme()) @@ -111,30 +125,19 @@ class ManageWidgetsActivity : Activity(), UIObject { ) val widget = AppWidget(appWidgetId, provider, position) - LauncherPreferences.internal().widgets( - (LauncherPreferences.internal().widgets() ?: HashSet()).also { + LauncherPreferences.widgets().widgets( + (LauncherPreferences.widgets().widgets() ?: HashSet()).also { it.add(widget) } ) - - findViewById(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 - ) - + val widget = AppWidget(appWidgetId) + if (widget.isConfigurable(this)) { + widget.configure(this, REQUEST_CREATE_APPWIDGET) } else { createWidget(data) } @@ -146,14 +149,12 @@ class ManageWidgetsActivity : Activity(), UIObject { ) { 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) { @@ -169,4 +170,4 @@ class ManageWidgetsActivity : Activity(), UIObject { override fun isHomeScreen(): Boolean { return true } -} \ No newline at end of file +} diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/SelectWidgetActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/SelectWidgetActivity.kt index 0a2bee8..c414db6 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/SelectWidgetActivity.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/SelectWidgetActivity.kt @@ -6,28 +6,21 @@ 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.ClockWidget import de.jrpie.android.launcher.widgets.LauncherAppWidgetProvider import de.jrpie.android.launcher.widgets.LauncherClockWidgetProvider import de.jrpie.android.launcher.widgets.LauncherWidgetProvider -import de.jrpie.android.launcher.widgets.Widget import de.jrpie.android.launcher.widgets.WidgetPosition import de.jrpie.android.launcher.widgets.bindAppWidgetOrRequestPermission import de.jrpie.android.launcher.widgets.getAppWidgetHost @@ -110,7 +103,6 @@ class SelectWidgetActivity : AppCompatActivity(), UIObject { 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(LauncherAppWidgetProvider(provider)) } @@ -139,7 +131,7 @@ class SelectWidgetActivity : AppCompatActivity(), UIObject { } override fun onBindViewHolder(viewHolder: ViewHolder, i: Int) { - val label = widgets[i].label + val label = widgets[i].loadLabel(this@SelectWidgetActivity) val description = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { widgets[i].loadDescription(this@SelectWidgetActivity) } else { diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetManagerView.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetManagerView.kt index 3ed86b0..2d41e13 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetManagerView.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetManagerView.kt @@ -1,7 +1,7 @@ package de.jrpie.android.launcher.ui.widgets.manage +import android.annotation.SuppressLint import android.app.Activity -import android.appwidget.AppWidgetHostView import android.content.Context import android.graphics.Point import android.graphics.Rect @@ -18,7 +18,6 @@ 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.Widget import de.jrpie.android.launcher.widgets.WidgetPosition @@ -72,11 +71,12 @@ class WidgetManagerView(context: Context, attrs: AttributeSet? = null) : private val longPressHandler = Handler(Looper.getMainLooper()) - override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { - onTouchEvent(ev) + + override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { return true } + @SuppressLint("ClickableViewAccessibility") override fun onTouchEvent(event: MotionEvent?): Boolean { if (event == null) { return false @@ -91,7 +91,7 @@ class WidgetManagerView(context: Context, attrs: AttributeSet? = null) : val position = (view.layoutParams as Companion.LayoutParams).position.getAbsoluteRect(width, height) selectedWidgetOverlayView = view - selectedWidgetView = Widget.byId(view.widgetId)?.findView(children) ?: return true + selectedWidgetView = widgetViewById.get(view.widgetId) ?: return true startWidgetPosition = position val positionInView = start.minus(Point(position.left, position.top)) @@ -136,7 +136,7 @@ class WidgetManagerView(context: Context, attrs: AttributeSet? = null) : if (event.actionMasked == MotionEvent.ACTION_UP) { longPressHandler.removeCallbacksAndMessages(null) val id = selectedWidgetOverlayView?.widgetId ?: return true - val widget = Widget.byId(id) ?: return true + val widget = Widget.byId(context, id) ?: return true widget.position = newPosition endInteraction() updateWidget(widget) @@ -155,10 +155,13 @@ class WidgetManagerView(context: Context, attrs: AttributeSet? = null) : selectedWidgetOverlayView?.mode = null } - override fun updateWidgets(activity: Activity) { - super.updateWidgets(activity) + override fun updateWidgets(activity: Activity, widgets: Set?) { + super.updateWidgets(activity, widgets) + if (widgets == null) { + return + } - LauncherPreferences.internal().widgets()?.forEach { widget -> + widgets.forEach { widget -> WidgetOverlayView(activity).let { addView(it) it.widgetId = widget.id diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetOverlayView.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetOverlayView.kt index 1b5eddb..0ce789f 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetOverlayView.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetOverlayView.kt @@ -9,7 +9,9 @@ import android.util.AttributeSet import android.view.View import android.widget.PopupMenu import androidx.core.graphics.toRectF +import de.jrpie.android.launcher.R import de.jrpie.android.launcher.widgets.Widget +import de.jrpie.android.launcher.widgets.updateWidget /** * An overlay to show configuration options for a widget. @@ -29,11 +31,9 @@ class WidgetOverlayView : View { handlePaint.style = Paint.Style.STROKE handlePaint.setARGB(255, 255, 255, 255) - selectedHandlePaint.style = Paint.Style.FILL_AND_STROKE selectedHandlePaint.setARGB(100, 255, 255, 255) - paint.style = Paint.Style.STROKE paint.setARGB(255, 255, 255, 255) } @@ -42,7 +42,7 @@ class WidgetOverlayView : View { var widgetId: Int = -1 set(newId) { field = newId - preview = Widget.byId(widgetId)?.getPreview(context) + preview = Widget.byId(context, widgetId)?.getPreview(context) } constructor(context: Context) : super(context) { @@ -74,7 +74,7 @@ class WidgetOverlayView : View { } } val bounds = getBounds() - canvas.drawRect(bounds, paint) + canvas.drawRoundRect(bounds.toRectF(), 5f, 5f, paint) if (mode == null) { return @@ -87,16 +87,26 @@ class WidgetOverlayView : View { } fun showPopupMenu() { + val widget = Widget.byId(context, widgetId)?: return val menu = PopupMenu(context, this) menu.menu.let { - it.add("Remove").setOnMenuItemClickListener { _ -> - Widget.byId(widgetId)?.delete(context) + it.add( + context.getString(R.string.widget_menu_remove) + ).setOnMenuItemClickListener { _ -> + Widget.byId(context, widgetId)?.delete(context) return@setOnMenuItemClickListener true } - it.add("Allow Interaction").setOnMenuItemClickListener { _ -> - return@setOnMenuItemClickListener true + it.add( + if (widget.allowInteraction) { + context.getString(R.string.widget_menu_disable_interaction) + } else { + context.getString(R.string.widget_menu_enable_interaction) } - it.add("Add Padding") + ).setOnMenuItemClickListener { _ -> + widget.allowInteraction = !widget.allowInteraction + updateWidget(widget) + return@setOnMenuItemClickListener true + } } menu.show() } @@ -118,5 +128,4 @@ class WidgetOverlayView : View { private fun getBounds(): Rect { return Rect(0,0, width, height) } - } \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/AppWidget.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/AppWidget.kt index fc816cc..3e9a2eb 100644 --- a/app/src/main/java/de/jrpie/android/launcher/widgets/AppWidget.kt +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/AppWidget.kt @@ -1,32 +1,26 @@ package de.jrpie.android.launcher.widgets; import android.app.Activity -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.graphics.drawable.Drawable import android.os.Build import android.os.Bundle -import android.util.AttributeSet import android.util.DisplayMetrics -import android.util.Log import android.util.SizeF import android.view.View import de.jrpie.android.launcher.Application -import de.jrpie.android.launcher.preferences.LauncherPreferences +import de.jrpie.android.launcher.ui.HomeActivity import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json @Serializable @SerialName("widget:app") class AppWidget( override val id: Int, override var position: WidgetPosition = WidgetPosition(0,0,1,1), + override var allowInteraction: Boolean = false, // 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) @@ -39,7 +33,9 @@ class AppWidget( constructor(id: Int, widgetProviderInfo: AppWidgetProviderInfo, position: WidgetPosition) : this( - id, position, + id, + position, + false, widgetProviderInfo.provider.packageName, widgetProviderInfo.provider.className, widgetProviderInfo.profile.hashCode() @@ -105,4 +101,20 @@ class AppWidget( override fun getPreview(context: Context): Drawable? { return context.getAppWidgetManager().getAppWidgetInfo(id)?.loadPreviewImage(context, DisplayMetrics.DENSITY_HIGH) } + + override fun isConfigurable(context: Context): Boolean { + return context.getAppWidgetManager().getAppWidgetInfo(id)?.configure != null + } + override fun configure(activity: Activity, requestCode: Int) { + if (!isConfigurable(activity)) { + return + } + activity.getAppWidgetHost().startAppWidgetConfigureActivityForResult( + activity, + id, + 0, + requestCode, + null + ) + } } diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/ClockWidget.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/ClockWidget.kt index 7accfe4..d819538 100644 --- a/app/src/main/java/de/jrpie/android/launcher/widgets/ClockWidget.kt +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/ClockWidget.kt @@ -11,7 +11,11 @@ import kotlinx.serialization.Serializable @Serializable @SerialName("widget:clock") -class ClockWidget(override val id: Int, override var position: WidgetPosition): Widget() { +class ClockWidget( + override val id: Int, + override var position: WidgetPosition, + override var allowInteraction: Boolean = true +) : Widget() { override fun createView(activity: Activity): View? { return ClockView(activity, null, id) @@ -28,4 +32,10 @@ class ClockWidget(override val id: Int, override var position: WidgetPosition): override fun getIcon(context: Context): Drawable? { return null } + + override fun isConfigurable(context: Context): Boolean { + return false + } + + override fun configure(activity: Activity, requestCode: Int) { } } \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/LauncherWidgetProvider.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/LauncherWidgetProvider.kt index 2755fe8..018b29b 100644 --- a/app/src/main/java/de/jrpie/android/launcher/widgets/LauncherWidgetProvider.kt +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/LauncherWidgetProvider.kt @@ -5,17 +5,21 @@ import android.content.Context import android.graphics.drawable.Drawable import android.os.Build import android.util.DisplayMetrics +import androidx.appcompat.content.res.AppCompatResources +import de.jrpie.android.launcher.R sealed class LauncherWidgetProvider { - abstract val label: String? - + abstract fun loadLabel(context: Context): CharSequence? abstract fun loadPreviewImage(context: Context): Drawable? abstract fun loadIcon(context: Context): Drawable? abstract fun loadDescription(context: Context): CharSequence? } class LauncherAppWidgetProvider(val info: AppWidgetProviderInfo) : LauncherWidgetProvider() { - override val label: String? = info.label + + override fun loadLabel(context: Context): CharSequence? { + return info.loadLabel(context.packageManager) + } override fun loadPreviewImage(context: Context): Drawable? { return info.loadPreviewImage(context, DisplayMetrics.DENSITY_DEFAULT) } @@ -34,19 +38,21 @@ class LauncherAppWidgetProvider(val info: AppWidgetProviderInfo) : LauncherWidge } class LauncherClockWidgetProvider : LauncherWidgetProvider() { - override val label: String? - get() = "Clock" + + override fun loadLabel(context: Context): CharSequence? { + return context.getString(R.string.widget_clock_label) + } + + override fun loadDescription(context: Context): CharSequence? { + return context.getString(R.string.widget_clock_description) + } override fun loadPreviewImage(context: Context): Drawable? { return null } override fun loadIcon(context: Context): Drawable? { - return null - } - - override fun loadDescription(context: Context): CharSequence? { - return null + return AppCompatResources.getDrawable(context, R.drawable.baseline_clock_24) } } diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/Widget.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/Widget.kt index e177baf..d3610dd 100644 --- a/app/src/main/java/de/jrpie/android/launcher/widgets/Widget.kt +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/Widget.kt @@ -4,6 +4,7 @@ import android.app.Activity import android.content.Context import android.graphics.drawable.Drawable import android.view.View +import de.jrpie.android.launcher.Application import de.jrpie.android.launcher.preferences.LauncherPreferences import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json @@ -13,6 +14,7 @@ import kotlinx.serialization.json.Json sealed class Widget { abstract val id: Int abstract var position: WidgetPosition + abstract var allowInteraction: Boolean /** * @param activity The activity where the view will be used. Must not be an AppCompatActivity. @@ -21,12 +23,14 @@ sealed class Widget { abstract fun findView(views: Sequence): View? abstract fun getPreview(context: Context): Drawable? abstract fun getIcon(context: Context): Drawable? + abstract fun isConfigurable(context: Context): Boolean + abstract fun configure(activity: Activity, requestCode: Int) fun delete(context: Context) { context.getAppWidgetHost().deleteAppWidgetId(id) - LauncherPreferences.internal().widgets( - LauncherPreferences.internal().widgets()?.also { + LauncherPreferences.widgets().widgets( + LauncherPreferences.widgets().widgets()?.also { it.remove(this) } ) @@ -47,9 +51,10 @@ sealed class Widget { fun deserialize(serialized: String): Widget { return Json.decodeFromString(serialized) } - fun byId(id: Int): Widget? { - return (LauncherPreferences.internal().widgets() ?: setOf()) - .firstOrNull { it.id == id } + fun byId(context: Context, id: Int): Widget? { + return (context.applicationContext as Application).widgets.value?.firstOrNull { + it.id == id + } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt index 485045f..cd4ef29 100644 --- a/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt @@ -3,17 +3,14 @@ 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 @@ -77,9 +74,9 @@ fun getAppWidgetProviders( context: Context ): List { fun updateWidget(widget: Widget) { - var widgets = LauncherPreferences.internal().widgets() ?: setOf() + var widgets = LauncherPreferences.widgets().widgets() ?: setOf() widgets = widgets.minus(widget).plus(widget) - LauncherPreferences.internal().widgets(widgets) + LauncherPreferences.widgets().widgets(widgets) } fun Context.getAppWidgetHost(): AppWidgetHost { diff --git a/app/src/main/res/drawable/baseline_clock_24.xml b/app/src/main/res/drawable/baseline_clock_24.xml new file mode 100644 index 0000000..7968998 --- /dev/null +++ b/app/src/main/res/drawable/baseline_clock_24.xml @@ -0,0 +1,15 @@ + + + + + + + diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index 9a99167..30e4cda 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -9,7 +9,7 @@ internal.started_before internal.first_startup internal.version_code - internal.widgets + widgets.widgets apps.favorites apps.hidden apps.pinned_shortcuts diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e225558..a8813f8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -98,6 +98,8 @@ Time Click on time + Manage widgets + Choose App @@ -390,4 +392,13 @@ Can\'t open URL: no browser found. Choose Widget + Remove + Configure + Enable Interaction + Disable Interaction + + + Clock + The default clock of μLauncher + diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 7d906ff..b4bc5f0 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -6,6 +6,10 @@ + +