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 a5f8831..5a1d5a0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -8,6 +8,7 @@ tools:ignore="QueryAllPackagesPermission" /> + + + + + - + \ No newline at end of file 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 e6cce23..775621c 100644 --- a/app/src/main/java/de/jrpie/android/launcher/Application.kt +++ b/app/src/main/java/de/jrpie/android/launcher/Application.kt @@ -12,6 +12,8 @@ import android.os.Build.VERSION_CODES import android.os.UserHandle import androidx.core.content.ContextCompat import androidx.lifecycle.MutableLiveData +import android.appwidget.AppWidgetHost +import android.appwidget.AppWidgetManager import androidx.preference.PreferenceManager import de.jrpie.android.launcher.actions.TorchManager import de.jrpie.android.launcher.apps.AbstractAppInfo @@ -20,13 +22,22 @@ 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 + +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 private val profileAvailabilityBroadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { @@ -90,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()) } } @@ -103,10 +116,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 +175,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/actions/Action.kt b/app/src/main/java/de/jrpie/android/launcher/actions/Action.kt index 9a2dc62..a883922 100644 --- a/app/src/main/java/de/jrpie/android/launcher/actions/Action.kt +++ b/app/src/main/java/de/jrpie/android/launcher/actions/Action.kt @@ -6,14 +6,18 @@ import android.content.SharedPreferences.Editor import android.graphics.Rect import android.graphics.drawable.Drawable import android.widget.Toast +import androidx.core.content.edit import de.jrpie.android.launcher.R import de.jrpie.android.launcher.preferences.LauncherPreferences import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json -import androidx.core.content.edit +/** + * Represents an action that can be bound to a [Gesture]. + * There are four types of actions: [AppAction], [ShortcutAction], [LauncherAction] and [WidgetPanelAction] + */ @Serializable sealed interface Action { fun invoke(context: Context, rect: Rect? = null): Boolean @@ -21,6 +25,10 @@ sealed interface Action { fun getIcon(context: Context): Drawable? fun isAvailable(context: Context): Boolean + fun showConfigurationDialog(context: Context, onSuccess: (Action) -> Unit) { + onSuccess(this) + } + // Can the action be used to reach µLauncher settings? fun canReachSettings(): Boolean diff --git a/app/src/main/java/de/jrpie/android/launcher/actions/WidgetPanelAction.kt b/app/src/main/java/de/jrpie/android/launcher/actions/WidgetPanelAction.kt new file mode 100644 index 0000000..d7829a6 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/actions/WidgetPanelAction.kt @@ -0,0 +1,83 @@ +package de.jrpie.android.launcher.actions + +import android.content.Context +import android.content.Intent +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.view.View +import android.widget.TextView +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import androidx.core.content.res.ResourcesCompat +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import de.jrpie.android.launcher.R +import de.jrpie.android.launcher.ui.widgets.WidgetPanelActivity +import de.jrpie.android.launcher.ui.widgets.manage.EXTRA_PANEL_ID +import de.jrpie.android.launcher.ui.widgets.manage.WidgetPanelsRecyclerAdapter +import de.jrpie.android.launcher.widgets.WidgetPanel +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +@SerialName("action:panel") +class WidgetPanelAction(val widgetPanelId: Int) : Action { + + override fun invoke(context: Context, rect: Rect?): Boolean { + + if (WidgetPanel.byId(widgetPanelId) == null) { + Toast.makeText(context, R.string.alert_widget_panel_not_found, Toast.LENGTH_LONG).show() + } else { + context.startActivity(Intent(context, WidgetPanelActivity::class.java).also { + it.putExtra(EXTRA_PANEL_ID, widgetPanelId) + }) + } + return true + } + + override fun label(context: Context): String { + return WidgetPanel.byId(widgetPanelId)?.label + ?: context.getString(R.string.list_other_open_widget_panel) + } + + override fun isAvailable(context: Context): Boolean { + return true + } + + override fun canReachSettings(): Boolean { + return false + } + + override fun getIcon(context: Context): Drawable? { + return ResourcesCompat.getDrawable( + context.resources, + R.drawable.baseline_widgets_24, + context.theme + ) + } + + override fun showConfigurationDialog(context: Context, onSuccess: (Action) -> Unit) { + AlertDialog.Builder(context, R.style.AlertDialogCustom).apply { + setTitle(R.string.dialog_select_widget_panel_title) + setNegativeButton(R.string.dialog_cancel) { _, _ -> } + setView(R.layout.dialog_select_widget_panel) + }.create().also { it.show() }.also { alertDialog -> + val infoTextView = + alertDialog.findViewById(R.id.dialog_select_widget_panel_info) + alertDialog.findViewById(R.id.dialog_select_widget_panel_recycler) + ?.apply { + setHasFixedSize(true) + layoutManager = LinearLayoutManager(alertDialog.context) + adapter = + WidgetPanelsRecyclerAdapter(alertDialog.context, false) { widgetPanel -> + onSuccess(WidgetPanelAction(widgetPanel.id)) + alertDialog.dismiss() + } + if (adapter?.itemCount == 0) { + infoTextView?.visibility = View.VISIBLE + } + } + } + true + } +} 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..d509ef2 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,8 @@ 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.SetWidgetPanelSerializer; +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; @@ -72,6 +74,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"), @@ -81,5 +84,9 @@ 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), + @Preference(name = "custom_panels", type = Set.class, serializer = SetWidgetPanelSerializer.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 59ecc7a..e8e717e 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 @@ -2,23 +2,29 @@ package de.jrpie.android.launcher.preferences import android.content.Context import android.util.Log +import de.jrpie.android.launcher.Application import de.jrpie.android.launcher.BuildConfig import de.jrpie.android.launcher.actions.Action -import de.jrpie.android.launcher.apps.AppInfo import de.jrpie.android.launcher.apps.AbstractAppInfo import de.jrpie.android.launcher.apps.AbstractAppInfo.Companion.INVALID_USER +import de.jrpie.android.launcher.apps.AppInfo import de.jrpie.android.launcher.apps.DetailedAppInfo import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersion1 import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersion2 import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersion3 +import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersion4 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.WidgetPanel +import de.jrpie.android.launcher.widgets.WidgetPosition +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 * `migratePreferencesToNewVersion` */ -const val PREFERENCE_VERSION = 4 +const val PREFERENCE_VERSION = 5 const val UNKNOWN_PREFERENCE_VERSION = -1 private const val TAG = "Launcher - Preferences" @@ -40,18 +46,23 @@ fun migratePreferencesToNewVersion(context: Context) { } 1 -> { - migratePreferencesFromVersion1() + migratePreferencesFromVersion1(context) Log.i(TAG, "migration of preferences complete (1 -> ${PREFERENCE_VERSION}).") } 2 -> { - migratePreferencesFromVersion2() + migratePreferencesFromVersion2(context) Log.i(TAG, "migration of preferences complete (2 -> ${PREFERENCE_VERSION}).") } 3 -> { - migratePreferencesFromVersion3() + migratePreferencesFromVersion3(context) Log.i(TAG, "migration of preferences complete (3 -> ${PREFERENCE_VERSION}).") } + 4 -> { + migratePreferencesFromVersion4(context) + Log.i(TAG, "migration of preferences complete (4 -> ${PREFERENCE_VERSION}).") + } + else -> { Log.w( TAG, @@ -71,6 +82,17 @@ fun resetPreferences(context: Context) { Log.i(TAG, "Resetting preferences") LauncherPreferences.clear() LauncherPreferences.internal().versionCode(PREFERENCE_VERSION) + deleteAllWidgets(context) + + LauncherPreferences.widgets().widgets( + setOf( + ClockWidget( + (context.applicationContext as Application).appWidgetHost.allocateAppWidgetId(), + WidgetPosition(1, 3, 10, 4), + WidgetPanel.HOME.id + ) + ) + ) val hidden: MutableSet = mutableSetOf() diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version1.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version1.kt index a1cb022..6cd9819 100644 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version1.kt +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version1.kt @@ -1,11 +1,13 @@ package de.jrpie.android.launcher.preferences.legacy +import android.content.Context +import androidx.core.content.edit import de.jrpie.android.launcher.actions.Action import de.jrpie.android.launcher.actions.AppAction import de.jrpie.android.launcher.actions.Gesture import de.jrpie.android.launcher.actions.LauncherAction -import de.jrpie.android.launcher.apps.AppInfo import de.jrpie.android.launcher.apps.AbstractAppInfo.Companion.INVALID_USER +import de.jrpie.android.launcher.apps.AppInfo import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.preferences.PREFERENCE_VERSION import kotlinx.serialization.Serializable @@ -13,7 +15,6 @@ import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import org.json.JSONException import org.json.JSONObject -import androidx.core.content.edit @Serializable @@ -129,7 +130,7 @@ private fun migrateAction(key: String) { * Migrate preferences from version 1 (used until version j-0.0.18) to the current format * (see [PREFERENCE_VERSION]) */ -fun migratePreferencesFromVersion1() { +fun migratePreferencesFromVersion1(context: Context) { assert(LauncherPreferences.internal().versionCode() == 1) Gesture.entries.forEach { g -> migrateAction(g.id) } migrateAppInfoSet(LauncherPreferences.apps().keys().hidden()) @@ -137,5 +138,5 @@ fun migratePreferencesFromVersion1() { migrateAppInfoStringMap(LauncherPreferences.apps().keys().customNames()) LauncherPreferences.internal().versionCode(2) - migratePreferencesFromVersion2() + migratePreferencesFromVersion2(context) } \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version2.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version2.kt index 4e6eae1..9714359 100644 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version2.kt +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version2.kt @@ -1,5 +1,6 @@ package de.jrpie.android.launcher.preferences.legacy +import android.content.Context import de.jrpie.android.launcher.actions.Action import de.jrpie.android.launcher.actions.Gesture import de.jrpie.android.launcher.actions.LauncherAction @@ -11,10 +12,10 @@ import de.jrpie.android.launcher.preferences.PREFERENCE_VERSION * Migrate preferences from version 2 (used until version 0.0.21) to the current format * (see [PREFERENCE_VERSION]) */ -fun migratePreferencesFromVersion2() { +fun migratePreferencesFromVersion2(context: Context) { assert(LauncherPreferences.internal().versionCode() == 2) // previously there was no setting for this Action.setActionForGesture(Gesture.BACK, LauncherAction.CHOOSE) LauncherPreferences.internal().versionCode(3) - migratePreferencesFromVersion3() + migratePreferencesFromVersion3(context) } \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version3.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version3.kt index 4a9241f..e0a8447 100644 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version3.kt +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version3.kt @@ -1,17 +1,17 @@ package de.jrpie.android.launcher.preferences.legacy +import android.content.Context import android.content.SharedPreferences import android.content.SharedPreferences.Editor -import de.jrpie.android.launcher.apps.AppInfo +import androidx.core.content.edit import de.jrpie.android.launcher.apps.AbstractAppInfo +import de.jrpie.android.launcher.apps.AppInfo import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.preferences.PREFERENCE_VERSION import de.jrpie.android.launcher.preferences.serialization.MapAbstractAppInfoStringPreferenceSerializer import de.jrpie.android.launcher.preferences.serialization.SetAbstractAppInfoPreferenceSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json -import java.util.HashSet -import androidx.core.content.edit /** * Migrate preferences from version 3 (used until version 0.0.23) to the current format @@ -70,8 +70,7 @@ private fun migrateMapAppInfoString(key: String, preferences: SharedPreferences, } } -fun migratePreferencesFromVersion3() { - assert(PREFERENCE_VERSION == 4) +fun migratePreferencesFromVersion3(context: Context) { assert(LauncherPreferences.internal().versionCode() == 3) val preferences = LauncherPreferences.getSharedPreferences() @@ -82,4 +81,5 @@ fun migratePreferencesFromVersion3() { } LauncherPreferences.internal().versionCode(4) + migratePreferencesFromVersion4(context) } \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version4.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version4.kt new file mode 100644 index 0000000..d4c7441 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version4.kt @@ -0,0 +1,27 @@ +package de.jrpie.android.launcher.preferences.legacy + +import android.content.Context +import de.jrpie.android.launcher.Application +import de.jrpie.android.launcher.preferences.LauncherPreferences +import de.jrpie.android.launcher.preferences.PREFERENCE_VERSION +import de.jrpie.android.launcher.widgets.ClockWidget +import de.jrpie.android.launcher.widgets.WidgetPanel +import de.jrpie.android.launcher.widgets.WidgetPosition + +fun migratePreferencesFromVersion4(context: Context) { + assert(PREFERENCE_VERSION == 5) + assert(LauncherPreferences.internal().versionCode() == 4) + + LauncherPreferences.widgets().widgets( + setOf( + ClockWidget( + (context.applicationContext as Application).appWidgetHost.allocateAppWidgetId(), + WidgetPosition(1, 3, 10, 4), + WidgetPanel.HOME.id + ) + ) + ) + + + LauncherPreferences.internal().versionCode(5) +} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/VersionUnknown.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/VersionUnknown.kt index 2d1152d..f954b31 100644 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/VersionUnknown.kt +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/VersionUnknown.kt @@ -3,10 +3,10 @@ package de.jrpie.android.launcher.preferences.legacy import android.content.Context import android.content.SharedPreferences import android.util.Log +import androidx.core.content.edit import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.preferences.theme.Background import de.jrpie.android.launcher.preferences.theme.ColorTheme -import androidx.core.content.edit private fun migrateStringPreference( @@ -392,5 +392,5 @@ fun migratePreferencesFromVersionUnknown(context: Context) { LauncherPreferences.internal().versionCode(1) Log.i(TAG, "migrated preferences to version 1.") - migratePreferencesFromVersion1() + migratePreferencesFromVersion1(context) } \ No newline at end of file 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..7b5d794 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,8 @@ 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.Widget +import de.jrpie.android.launcher.widgets.WidgetPanel import eu.jonahbauer.android.preference.annotations.serializer.PreferenceSerializationException import eu.jonahbauer.android.preference.annotations.serializer.PreferenceSerializer import kotlinx.serialization.Serializable @@ -28,6 +30,40 @@ class SetAbstractAppInfoPreferenceSerializer : } } + +@Suppress("UNCHECKED_CAST") +class SetWidgetSerializer : + PreferenceSerializer?, java.util.Set?> { + @Throws(PreferenceSerializationException::class) + 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(Widget::deserialize) + ?.toHashSet() as? java.util.Set + } +} + +@Suppress("UNCHECKED_CAST") +class SetWidgetPanelSerializer : + PreferenceSerializer?, java.util.Set?> { + @Throws(PreferenceSerializationException::class) + override fun serialize(value: java.util.Set?): java.util.Set? { + return value?.map(WidgetPanel::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(WidgetPanel::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/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/HomeActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt index 53a0876..192a8e9 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 @@ -10,8 +11,7 @@ import android.view.KeyEvent import android.view.MotionEvent import android.view.View import android.window.OnBackInvokedDispatcher -import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.isVisible +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 +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, @@ -34,10 +33,10 @@ import java.util.Locale * - 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 lateinit var touchGestureDetector: TouchGestureDetector + private var touchGestureDetector: TouchGestureDetector? = null private var sharedPreferencesListener = SharedPreferences.OnSharedPreferenceChangeListener { _, prefKey -> @@ -45,40 +44,26 @@ class HomeActivity : UIObject, AppCompatActivity() { prefKey?.startsWith("display.") == true ) { recreate() + } 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("action.") == true) { - updateSettingsFallbackButtonVisibility() - } } override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) + super.onCreate(savedInstanceState) super.onCreate() - touchGestureDetector = TouchGestureDetector( - this, 0, 0, - LauncherPreferences.enabled_gestures().edgeSwipeEdgeWidth() / 100f - ) - touchGestureDetector.updateScreenSize(windowManager) // Initialise layout binding = HomeBinding.inflate(layoutInflater) setContentView(binding.root) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - binding.root.setOnApplyWindowInsetsListener { _, windowInsets -> - @Suppress("deprecation") // required to support API 29 - val insets = windowInsets.systemGestureInsets - touchGestureDetector.setSystemGestureInsets(insets) - - windowInsets - } - } - - - // Handle back key / gesture on Android 13+, cf. onKeyDown() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { onBackInvokedDispatcher.registerOnBackInvokedCallback( @@ -94,12 +79,11 @@ class HomeActivity : UIObject, AppCompatActivity() { override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) - touchGestureDetector.updateScreenSize(windowManager) + touchGestureDetector?.updateScreenSize(windowManager) } override fun onStart() { - super.onStart() - + super.onStart() super.onStart() // If the tutorial was not finished, start it @@ -110,6 +94,15 @@ class HomeActivity : UIObject, AppCompatActivity() { LauncherPreferences.getSharedPreferences() .registerOnSharedPreferenceChangeListener(sharedPreferencesListener) + (application as Application).appWidgetHost.startListening() + + } + + + + override fun onStop() { + (application as Application).appWidgetHost.stopListening() + super.onStop() } override fun onWindowFocusChanged(hasFocus: Boolean) { @@ -135,44 +128,6 @@ class HomeActivity : UIObject, AppCompatActivity() { } } - 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) @@ -188,11 +143,33 @@ class HomeActivity : UIObject, AppCompatActivity() { override fun onResume() { super.onResume() - touchGestureDetector.edgeWidth = + /* This should be initialized in onCreate() + However on some devices there seems to be a bug where the touchGestureDetector + is not working properly after resuming the app. + Reinitializing the touchGestureDetector every time the app is resumed might help to fix that. + (see issue #138) + */ + touchGestureDetector = TouchGestureDetector( + this, 0, 0, LauncherPreferences.enabled_gestures().edgeSwipeEdgeWidth() / 100f + ).also { + it.updateScreenSize(windowManager) + } - initClock() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + binding.root.setOnApplyWindowInsetsListener { _, windowInsets -> + @Suppress("deprecation") // required to support API 29 + val insets = windowInsets.systemGestureInsets + touchGestureDetector?.setSystemGestureInsets(insets) + + windowInsets + } + } updateSettingsFallbackButtonVisibility() + + binding.homeWidgetContainer.updateWidgets(this@HomeActivity, + LauncherPreferences.widgets().widgets() + ) } override fun onDestroy() { @@ -230,30 +207,10 @@ class HomeActivity : UIObject, AppCompatActivity() { } override fun onTouchEvent(event: MotionEvent): Boolean { - touchGestureDetector.onTouchEvent(event) + touchGestureDetector?.onTouchEvent(event) 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/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/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/java/de/jrpie/android/launcher/ui/list/other/OtherRecyclerAdapter.kt b/app/src/main/java/de/jrpie/android/launcher/ui/list/other/OtherRecyclerAdapter.kt index f176469..06be78a 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/list/other/OtherRecyclerAdapter.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/list/other/OtherRecyclerAdapter.kt @@ -11,6 +11,7 @@ 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.actions.WidgetPanelAction import de.jrpie.android.launcher.ui.list.ListActivity /** @@ -23,8 +24,10 @@ import de.jrpie.android.launcher.ui.list.ListActivity class OtherRecyclerAdapter(val activity: Activity) : RecyclerView.Adapter() { - private val othersList: Array = - LauncherAction.entries.filter { it.isAvailable(activity) }.toTypedArray() + private val othersList: Array = + LauncherAction.entries.filter { it.isAvailable(activity) } + .plus(WidgetPanelAction(-1)) + .toTypedArray() inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener { @@ -36,10 +39,12 @@ class OtherRecyclerAdapter(val activity: Activity) : val pos = bindingAdapterPosition val content = othersList[pos] - activity.finish() val gestureId = (activity as? ListActivity)?.forGesture ?: return val gesture = Gesture.byId(gestureId) ?: return - Action.setActionForGesture(gesture, content) + content.showConfigurationDialog(activity) { configuredAction -> + Action.setActionForGesture(gesture, configuredAction) + activity.finish() + } } init { @@ -48,11 +53,11 @@ class OtherRecyclerAdapter(val activity: Activity) : } override fun onBindViewHolder(viewHolder: ViewHolder, i: Int) { - val otherLabel = activity.getString(othersList[i].label) - val icon = othersList[i].icon + val otherLabel = othersList[i].label(activity) + val icon = othersList[i].getIcon(activity) viewHolder.textView.text = otherLabel - viewHolder.iconView.setImageResource(icon) + viewHolder.iconView.setImageDrawable(icon) } override fun getItemCount(): Int { 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..bb9df74 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,8 @@ 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.ManageWidgetPanelsActivity +import de.jrpie.android.launcher.ui.widgets.manage.ManageWidgetsActivity /** @@ -81,6 +83,22 @@ class SettingsFragmentLauncher : PreferenceFragmentCompat() { true } + val manageWidgets = findPreference( + LauncherPreferences.widgets().keys().widgets() + ) + manageWidgets?.setOnPreferenceClickListener { + startActivity(Intent(requireActivity(), ManageWidgetsActivity::class.java)) + true + } + + val manageWidgetPanels = findPreference( + LauncherPreferences.widgets().keys().customPanels() + ) + manageWidgetPanels?.setOnPreferenceClickListener { + startActivity(Intent(requireActivity(), ManageWidgetPanelsActivity::class.java)) + true + } + val hiddenApps = findPreference( LauncherPreferences.apps().keys().hidden() ) 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 new file mode 100644 index 0000000..d071771 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetContainerView.kt @@ -0,0 +1,144 @@ +package de.jrpie.android.launcher.ui.widgets + +import android.app.Activity +import android.content.Context +import android.graphics.PointF +import android.graphics.RectF +import android.util.AttributeSet +import android.util.Log +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.widgets.Widget +import de.jrpie.android.launcher.widgets.WidgetPanel +import de.jrpie.android.launcher.widgets.WidgetPosition +import kotlin.math.max + + +/** + * This only works in an Activity, not AppCompatActivity + */ +open class WidgetContainerView( + var widgetPanelId: Int, + context: Context, + attrs: AttributeSet? = null +) : ViewGroup(context, attrs) { + constructor(context: Context, attrs: AttributeSet) : this(WidgetPanel.HOME.id, context, attrs) + + var widgetViewById = HashMap() + + open fun updateWidgets(activity: Activity, widgets: Collection?) { + synchronized(widgetViewById) { + if (widgets == null) { + return + } + Log.i("WidgetContainer", "updating ${activity.localClassName}") + widgetViewById.forEach { removeView(it.value) } + widgetViewById.clear() + widgets.filter { it.panelId == widgetPanelId }.forEach { widget -> + widget.createView(activity)?.let { + addView(it, 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 + var maxWidth = suggestedMinimumWidth + + val mWidth = MeasureSpec.getSize(widthMeasureSpec) + val mHeight = MeasureSpec.getSize(heightMeasureSpec) + + (0...onCreate(savedInstanceState) + super.onCreate() + widgetPanelId = intent.getIntExtra(EXTRA_PANEL_ID, WidgetPanel.Companion.HOME.id) + val binding = ActivityWidgetPanelBinding.inflate(layoutInflater) + setContentView(binding.root) + binding.widgetPanelWidgetContainer.widgetPanelId = widgetPanelId + binding.widgetPanelWidgetContainer.updateWidgets( + this, + LauncherPreferences.widgets().widgets() + ) + } + + 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 onStart() { + super.onStart() + super.onStart() + } + + 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/ManageWidgetPanelsActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetPanelsActivity.kt new file mode 100644 index 0000000..b18852f --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetPanelsActivity.kt @@ -0,0 +1,104 @@ +package de.jrpie.android.launcher.ui.widgets.manage + +import android.annotation.SuppressLint +import android.content.Intent +import android.content.SharedPreferences +import android.content.res.Resources +import android.os.Bundle +import android.widget.EditText +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.LinearLayoutManager +import de.jrpie.android.launcher.R +import de.jrpie.android.launcher.databinding.ActivityManageWidgetPanelsBinding +import de.jrpie.android.launcher.preferences.LauncherPreferences +import de.jrpie.android.launcher.ui.UIObject +import de.jrpie.android.launcher.widgets.WidgetPanel +import de.jrpie.android.launcher.widgets.updateWidgetPanel + +class ManageWidgetPanelsActivity : AppCompatActivity(), UIObject { + + private val sharedPreferencesListener = + SharedPreferences.OnSharedPreferenceChangeListener { _, prefKey -> + if (prefKey == LauncherPreferences.widgets().keys().customPanels()) { + viewAdapter.widgetPanels = + (LauncherPreferences.widgets().customPanels() ?: setOf()).toTypedArray() + + @SuppressLint("NotifyDataSetChanged") + viewAdapter.notifyDataSetChanged() + } + } + private lateinit var binding: ActivityManageWidgetPanelsBinding + private lateinit var viewAdapter: WidgetPanelsRecyclerAdapter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + super.onCreate() + + binding = ActivityManageWidgetPanelsBinding.inflate(layoutInflater) + setContentView(binding.main) + + val viewManager = LinearLayoutManager(this) + viewAdapter = WidgetPanelsRecyclerAdapter(this, true) { widgetPanel -> + startActivity( + Intent( + this@ManageWidgetPanelsActivity, + ManageWidgetsActivity::class.java + ).also { + it.putExtra(EXTRA_PANEL_ID, widgetPanel.id) + }) + } + binding.manageWidgetPanelsRecycler.apply { + // improve performance (since content changes don't change the layout size) + setHasFixedSize(true) + layoutManager = viewManager + adapter = viewAdapter + } + binding.manageWidgetPanelsClose.setOnClickListener { finish() } + binding.manageWidgetPanelsAddPanel.setOnClickListener { + AlertDialog.Builder(this@ManageWidgetPanelsActivity, R.style.AlertDialogCustom).apply { + setTitle(R.string.dialog_create_widget_panel_title) + setNegativeButton(R.string.dialog_cancel) { _, _ -> } + setPositiveButton(R.string.dialog_ok) { dialogInterface, _ -> + val panelId = WidgetPanel.allocateId() + val label = (dialogInterface as? AlertDialog) + ?.findViewById(R.id.dialog_create_widget_panel_edit_text)?.text?.toString() + ?: (getString(R.string.widget_panel_default_name, panelId)) + + updateWidgetPanel(WidgetPanel(panelId, label)) + } + setView(R.layout.dialog_create_widget_panel) + }.create().also { it.show() }.apply { + findViewById(R.id.dialog_create_widget_panel_edit_text) + ?.setText( + getString( + R.string.widget_panel_default_name, + WidgetPanel.allocateId() + ) + ) + } + true + } + } + + override fun onStart() { + super.onStart() + super.onStart() + LauncherPreferences.getSharedPreferences() + .registerOnSharedPreferenceChangeListener(sharedPreferencesListener) + } + + override fun onPause() { + LauncherPreferences.getSharedPreferences() + .unregisterOnSharedPreferenceChangeListener(sharedPreferencesListener) + super.onPause() + } + + override fun getTheme(): Resources.Theme { + return modifyTheme(super.getTheme()) + } + + override fun setOnClicks() { + binding.manageWidgetPanelsClose.setOnClickListener { finish() } + } +} \ No newline at end of file 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 new file mode 100644 index 0000000..d191b70 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetsActivity.kt @@ -0,0 +1,185 @@ +package de.jrpie.android.launcher.ui.widgets.manage + +import android.app.Activity +import android.appwidget.AppWidgetManager +import android.content.Intent +import android.content.SharedPreferences +import android.content.res.Resources +import android.graphics.Rect +import android.os.Bundle +import android.util.Log +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import com.google.android.material.floatingactionbutton.FloatingActionButton +import de.jrpie.android.launcher.Application +import de.jrpie.android.launcher.R +import de.jrpie.android.launcher.preferences.LauncherPreferences +import de.jrpie.android.launcher.ui.UIObject +import de.jrpie.android.launcher.ui.widgets.WidgetContainerView +import de.jrpie.android.launcher.widgets.AppWidget +import de.jrpie.android.launcher.widgets.WidgetPanel +import de.jrpie.android.launcher.widgets.WidgetPosition +import kotlin.math.min + + +// http://coderender.blogspot.com/2012/01/hosting-android-widgets-my.html + +const val REQUEST_CREATE_APPWIDGET = 1 +const val REQUEST_PICK_APPWIDGET = 2 + +const val EXTRA_PANEL_ID = "widgetPanelId" + +// We can't use AppCompatActivity, since some AppWidgets don't work there. +class ManageWidgetsActivity : Activity(), UIObject { + + var panelId: Int = WidgetPanel.HOME.id + + private var sharedPreferencesListener = + SharedPreferences.OnSharedPreferenceChangeListener { _, prefKey -> + 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() + ) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + super.onCreate() + setContentView(R.layout.activity_manage_widgets) + + panelId = intent.extras?.getInt(EXTRA_PANEL_ID, WidgetPanel.HOME.id) ?: WidgetPanel.HOME.id + + 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).let { + it.widgetPanelId = panelId + it.updateWidgets(this, (application as Application).widgets.value) + } + } + + override fun onStart() { + super.onStart() + super.onStart() + + 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()) + 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() + ) + it.putExtra( + EXTRA_PANEL_ID, + panelId + ) + }, 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 = AppWidget(appWidgetId, position, panelId, provider) + LauncherPreferences.widgets().widgets( + (LauncherPreferences.widgets().widgets() ?: HashSet()).also { + it.add(widget) + } + ) + } + + private fun configureWidget(data: Intent) { + val extras = data.extras + val appWidgetId = extras!!.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, -1) + val widget = AppWidget(appWidgetId, panelId = panelId) + if (widget.isConfigurable(this)) { + widget.configure(this, REQUEST_CREATE_APPWIDGET) + } else { + createWidget(data) + } + } + + override fun onActivityResult( + requestCode: Int, resultCode: Int, + data: Intent? + ) { + super.onActivityResult(requestCode, resultCode, data) + if (resultCode == RESULT_OK) { + if (requestCode == REQUEST_PICK_APPWIDGET) { + configureWidget(data!!) + } else if (requestCode == REQUEST_CREATE_APPWIDGET) { + createWidget(data!!) + } + } else if (resultCode == RESULT_CANCELED && data != null) { + val appWidgetId = + data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1) + if (appWidgetId != -1) { + AppWidget(appWidgetId).delete(this) + } + } + } + + + /** + * For a better preview, [ManageWidgetsActivity] should behave exactly like [HomeActivity] + */ + override fun isHomeScreen(): Boolean { + return true + } +} 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..a1bd3b5 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/SelectWidgetActivity.kt @@ -0,0 +1,172 @@ +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.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +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.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.WidgetPanel +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 + +/** + * 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 + var widgetPanelId: Int = WidgetPanel.HOME.id + + 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) + it.putExtra(EXTRA_PANEL_ID, widgetPanelId) + } + ) + finish() + } + } + is LauncherClockWidgetProvider -> { + updateWidget(ClockWidget(widgetId, WidgetPosition(0, 4, 12, 3), widgetPanelId)) + 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) + widgetPanelId = intent.getIntExtra(EXTRA_PANEL_ID, WidgetPanel.HOME.id) + if (widgetId == -1) { + widgetId = getAppWidgetHost().allocateAppWidgetId() + } + + 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 + val provider = (data.getSerializableExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER) as? AppWidgetProviderInfo) ?: return + tryBindWidget(LauncherAppWidgetProvider(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].loadLabel(this@SelectWidgetActivity) + val description = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + widgets[i].loadDescription(this@SelectWidgetActivity) + } else { + "" + } + val preview = + widgets[i].loadPreviewImage(this@SelectWidgetActivity) + val icon = + widgets[i].loadIcon(this@SelectWidgetActivity) + + 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..7a355f7 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetManagerView.kt @@ -0,0 +1,175 @@ +package de.jrpie.android.launcher.ui.widgets.manage + +import android.annotation.SuppressLint +import android.app.Activity +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.ui.widgets.WidgetContainerView +import de.jrpie.android.launcher.widgets.Widget +import de.jrpie.android.launcher.widgets.WidgetPanel +import de.jrpie.android.launcher.widgets.WidgetPosition +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(widgetPanelId: Int, context: Context, attrs: AttributeSet? = null) : + WidgetContainerView(widgetPanelId, context, attrs) { + constructor(context: Context, attrs: AttributeSet?) : this(WidgetPanel.HOME.id, 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 onInterceptTouchEvent(ev: MotionEvent?): Boolean { + return true + } + + @SuppressLint("ClickableViewAccessibility") + 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 + selectedWidgetView = widgetViewById.get(view.widgetId) ?: return true + 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 + (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) + } + } + + if (event.actionMasked == MotionEvent.ACTION_UP) { + longPressHandler.removeCallbacksAndMessages(null) + val id = selectedWidgetOverlayView?.widgetId ?: return true + val widget = Widget.byId(context, 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 + } + + override fun updateWidgets(activity: Activity, widgets: Collection?) { + super.updateWidgets(activity, widgets) + if (widgets == null) { + return + } + children.mapNotNull { it as? WidgetOverlayView }.forEach { removeView(it) } + + widgets.filter { it.panelId == widgetPanelId }.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..1b8a2d2 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetOverlayView.kt @@ -0,0 +1,132 @@ +package de.jrpie.android.launcher.ui.widgets.manage + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Rect +import android.graphics.drawable.Drawable +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 + + +private const val HANDLE_SIZE = 100 +private const val HANDLE_EDGE_SIZE = (1.2 * HANDLE_SIZE).toInt() + +/** + * An overlay to show configuration options for a widget in [WidgetManagerView] + */ +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(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) + } + + private var preview: Drawable? = null + var widgetId: Int = -1 + set(newId) { + field = newId + preview = Widget.byId(context, widgetId)?.getPreview(context) + } + + 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.drawRoundRect(bounds.toRectF(), 5f, 5f, paint) + + if (mode == null) { + return + } + + //preview?.bounds = bounds + //preview?.draw(canvas) + + + } + + fun showPopupMenu() { + val widget = Widget.byId(context, widgetId)?: return + val menu = PopupMenu(context, this) + menu.menu.let { + it.add( + context.getString(R.string.widget_menu_remove) + ).setOnMenuItemClickListener { _ -> + Widget.byId(context, widgetId)?.delete(context) + 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) + } + ).setOnMenuItemClickListener { _ -> + widget.allowInteraction = !widget.allowInteraction + updateWidget(widget) + return@setOnMenuItemClickListener true + } + } + 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/ui/widgets/manage/WidgetPanelsRecyclerAdapter.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetPanelsRecyclerAdapter.kt new file mode 100644 index 0000000..40c2c2f --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetPanelsRecyclerAdapter.kt @@ -0,0 +1,98 @@ +package de.jrpie.android.launcher.ui.widgets.manage + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.EditText +import android.widget.PopupMenu +import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import androidx.recyclerview.widget.RecyclerView +import de.jrpie.android.launcher.R +import de.jrpie.android.launcher.preferences.LauncherPreferences +import de.jrpie.android.launcher.widgets.WidgetPanel +import de.jrpie.android.launcher.widgets.updateWidgetPanel + + +class WidgetPanelsRecyclerAdapter( + val context: Context, + val showMenu: Boolean = false, + val onSelectWidgetPanel: (WidgetPanel) -> Unit +) : + RecyclerView.Adapter() { + + var widgetPanels = (LauncherPreferences.widgets().customPanels() ?: setOf()).toTypedArray() + + class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + var labelView: TextView = itemView.findViewById(R.id.list_widget_panels_label) + } + + override fun onBindViewHolder(viewHolder: ViewHolder, i: Int) { + viewHolder.labelView.text = widgetPanels[i].label + + viewHolder.itemView.setOnClickListener { + onSelectWidgetPanel(widgetPanels[i]) + } + + if (showMenu) { + viewHolder.itemView.setOnLongClickListener { + showOptionsPopup( + viewHolder, + widgetPanels[i] + ) + } + } + } + + @Suppress("SameReturnValue") + private fun showOptionsPopup( + viewHolder: ViewHolder, + widgetPanel: WidgetPanel + ): Boolean { + //create the popup menu + + val popup = PopupMenu(context, viewHolder.labelView) + popup.menu.add(R.string.manage_widget_panels_delete).setOnMenuItemClickListener { _ -> + widgetPanel.delete(context) + true + } + popup.menu.add(R.string.manage_widget_panels_rename).setOnMenuItemClickListener { _ -> + AlertDialog.Builder(context, R.style.AlertDialogCustom).apply { + setNegativeButton(R.string.dialog_cancel) { _, _ -> } + setPositiveButton(R.string.dialog_ok) { dialogInterface, _ -> + var newLabel = (dialogInterface as? AlertDialog) + ?.findViewById(R.id.dialog_rename_widget_panel_edit_text) + ?.text?.toString() + if (newLabel == null || newLabel.isEmpty()) { + newLabel = + (context.getString(R.string.widget_panel_default_name, widgetPanel.id)) + } + widgetPanel.label = newLabel + updateWidgetPanel(widgetPanel) + } + setView(R.layout.dialog_rename_widget_panel) + }.create().also { it.show() }.apply { + findViewById(R.id.dialog_rename_widget_panel_edit_text)?.let { + it.setText(widgetPanel.label) + it.hint = context.getString(R.string.widget_panel_default_name, widgetPanel.id) + } + } + true + } + + popup.show() + return true + } + + override fun getItemCount(): Int { + return widgetPanels.size + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view: View = + LayoutInflater.from(context).inflate(R.layout.list_widget_panels_row, parent, false) + val viewHolder = ViewHolder(view) + return viewHolder + } +} \ 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 new file mode 100644 index 0000000..22a63eb --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/AppWidget.kt @@ -0,0 +1,126 @@ +package de.jrpie.android.launcher.widgets + +import android.app.Activity +import android.appwidget.AppWidgetHostView +import android.appwidget.AppWidgetProviderInfo +import android.content.Context +import android.graphics.drawable.Drawable +import android.os.Build +import android.os.Bundle +import android.util.DisplayMetrics +import android.util.SizeF +import android.view.View +import de.jrpie.android.launcher.Application +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +@SerialName("widget:app") +class AppWidget( + override val id: Int, + override var position: WidgetPosition = WidgetPosition(0,0,1,1), + override var panelId: Int = WidgetPanel.HOME.id, + 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) + // 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, + position: WidgetPosition, + panelId: Int, + widgetProviderInfo: AppWidgetProviderInfo + ) : + this( + id, + position, + panelId, + false, + 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) + } + + 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 new file mode 100644 index 0000000..d0d1c0e --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/ClockWidget.kt @@ -0,0 +1,42 @@ +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, + override val panelId: Int, + override var allowInteraction: Boolean = true +) : 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 + } + + 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 new file mode 100644 index 0000000..018b29b --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/LauncherWidgetProvider.kt @@ -0,0 +1,58 @@ +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 +import androidx.appcompat.content.res.AppCompatResources +import de.jrpie.android.launcher.R + +sealed class LauncherWidgetProvider { + 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 fun loadLabel(context: Context): CharSequence? { + return info.loadLabel(context.packageManager) + } + 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 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 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 new file mode 100644 index 0000000..dbe667b --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/Widget.kt @@ -0,0 +1,65 @@ +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.Application +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 + abstract val panelId: Int + abstract var allowInteraction: Boolean + + /** + * @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? + abstract fun isConfigurable(context: Context): Boolean + abstract fun configure(activity: Activity, requestCode: Int) + + fun delete(context: Context) { + context.getAppWidgetHost().deleteAppWidgetId(id) + + LauncherPreferences.widgets().widgets( + LauncherPreferences.widgets().widgets()?.also { + it.remove(this) + } + ) + } + + fun getPanel(): WidgetPanel? { + return WidgetPanel.byId(panelId) + } + + 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(context: Context, id: Int): Widget? { + return (context.applicationContext as Application).widgets.value?.firstOrNull { + it.id == id + } + } + } +} 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..93e588d --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/WidgetPanel.kt @@ -0,0 +1,58 @@ +package de.jrpie.android.launcher.widgets + +import android.content.Context +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("panel") +class WidgetPanel(val id: Int, var label: String) { + + override fun equals(other: Any?): Boolean { + return (other as? WidgetPanel)?.id == id + } + + override fun hashCode(): Int { + return id + } + + fun serialize(): String { + return Json.encodeToString(this) + } + + fun delete(context: Context) { + LauncherPreferences.widgets().customPanels( + (LauncherPreferences.widgets().customPanels() ?: setOf()).minus(this) + ) + (LauncherPreferences.widgets().widgets() ?: return) + .filter { it.panelId == this.id }.forEach { it.delete(context) } + } + + + companion object { + val HOME = WidgetPanel(0, "home") + fun byId(id: Int): WidgetPanel? { + if (id == 0) { + return HOME + } + return LauncherPreferences.widgets().customPanels()?.firstOrNull { it.id == id } + } + + fun allocateId(): Int { + return ( + (LauncherPreferences.widgets().customPanels() ?: setOf()) + .plus(HOME) + .maxOfOrNull { it.id } ?: 0 + ) + 1 + } + + fun deserialize(serialized: String): WidgetPanel { + return Json.decodeFromString(serialized) + } + + } +} \ No newline at end of file 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 new file mode 100644 index 0000000..b7f140b --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt @@ -0,0 +1,97 @@ +package de.jrpie.android.launcher.widgets + +import android.app.Activity +import android.app.Service +import android.appwidget.AppWidgetHost +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.UserManager +import android.util.Log +import de.jrpie.android.launcher.Application +import de.jrpie.android.launcher.preferences.LauncherPreferences + +fun deleteAllWidgets(context: Context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + 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() + } else { id } + + Log.i("Launcher", "Binding new widget ${appWidgetId}") + if (!activity.getAppWidgetManager().bindAppWidgetIdIfAllowed( + appWidgetId, + providerInfo.provider + ) + ) { + 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 getAppWidgetProviders( context: Context ): List { + val list = mutableListOf(LauncherClockWidgetProvider()) + 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 + } + list.addAll( + profiles.map { + appWidgetManager.getInstalledProvidersForProfile(it) + .map { LauncherAppWidgetProvider(it) } + }.flatten() + ) + + + return list +} + + +fun updateWidget(widget: Widget) { + LauncherPreferences.widgets().widgets( + (LauncherPreferences.widgets().widgets() ?: setOf()) + .minus(widget) + .plus(widget) + ) +} + +fun updateWidgetPanel(widgetPanel: WidgetPanel) { + LauncherPreferences.widgets().customPanels( + (LauncherPreferences.widgets().customPanels() ?: setOf()) + .minus(widgetPanel) + .plus(widgetPanel) + ) +} + +fun Context.getAppWidgetHost(): AppWidgetHost { + return (this.applicationContext as Application).appWidgetHost +} +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/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/drawable/baseline_widgets_24.xml b/app/src/main/res/drawable/baseline_widgets_24.xml new file mode 100644 index 0000000..fd0f571 --- /dev/null +++ b/app/src/main/res/drawable/baseline_widgets_24.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/layout/activity_manage_widget_panels.xml b/app/src/main/res/layout/activity_manage_widget_panels.xml new file mode 100644 index 0000000..60413a3 --- /dev/null +++ b/app/src/main/res/layout/activity_manage_widget_panels.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + 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 new file mode 100644 index 0000000..82db94d --- /dev/null +++ b/app/src/main/res/layout/activity_select_widget.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_widget_panel.xml b/app/src/main/res/layout/activity_widget_panel.xml new file mode 100644 index 0000000..6ef6b20 --- /dev/null +++ b/app/src/main/res/layout/activity_widget_panel.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file 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/dialog_create_widget_panel.xml b/app/src/main/res/layout/dialog_create_widget_panel.xml new file mode 100644 index 0000000..900823d --- /dev/null +++ b/app/src/main/res/layout/dialog_create_widget_panel.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_rename_widget_panel.xml b/app/src/main/res/layout/dialog_rename_widget_panel.xml new file mode 100644 index 0000000..effb783 --- /dev/null +++ b/app/src/main/res/layout/dialog_rename_widget_panel.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_select_widget_panel.xml b/app/src/main/res/layout/dialog_select_widget_panel.xml new file mode 100644 index 0000000..5f83d51 --- /dev/null +++ b/app/src/main/res/layout/dialog_select_widget_panel.xml @@ -0,0 +1,26 @@ + + + + + + + + \ 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 ecefdea..717151f 100644 --- a/app/src/main/res/layout/home.xml +++ b/app/src/main/res/layout/home.xml @@ -10,29 +10,10 @@ android:fitsSystemWindows="true" tools:context=".ui.HomeActivity"> - - - + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_widgets_header.xml b/app/src/main/res/layout/list_widgets_header.xml new file mode 100644 index 0000000..7779f2b --- /dev/null +++ b/app/src/main/res/layout/list_widgets_header.xml @@ -0,0 +1,35 @@ + + + + + + + + \ 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..878aaad --- /dev/null +++ b/app/src/main/res/layout/list_widgets_row.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + \ 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/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index 89ec086..f783d2a 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -9,6 +9,8 @@ internal.started_before internal.first_startup internal.version_code + widgets.widgets + widgets.custom_panels apps.favorites apps.hidden apps.pinned_shortcuts @@ -147,6 +149,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 a8bc9b3..7d29128 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -98,6 +98,9 @@ Time Click on time + Manage widgets + Manage widget panels + Choose App @@ -167,6 +170,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 @@ -251,12 +255,12 @@ Music: Next Music: Previous Music: Play / Pause - Expand notifications panel + Expand Notifications Panel Recent Apps - Do nothing + Do Nothing Lock Screen Toggle Torch - Launch other Home Screen + Launch Other Home Screen Add Shortcut @@ -387,5 +391,33 @@ Open Source Licenses No app found to handle search. Can\'t open URL: no browser found. + Choose Widget + + Remove + Configure + Enable Interaction + Disable Interaction + + + Clock + The default clock of μLauncher + Delete + Rename + + Widget Panel #%1$d + + Contains %d widget. + Contains %d widgets. + + + + Ok + Widget Panels + Select a Widget Panel + Create new widget panel + Launcher > Manage Widget Panels.]]> + Open Widget Panel + This widget panel no longer exists. + Widgets diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 20ccb67..c5b7252 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -66,12 +66,12 @@ 0 2 + - - + + + + + + diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 6ef5d07..0ee7c17 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -3,10 +3,10 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> + - + + + + +