some tests

This commit is contained in:
Josia Pietsch 2024-10-01 19:37:12 +02:00
parent 077bd1ce44
commit 49ae7e130f
Signed by: jrpie
GPG key ID: E70B571D66986A2D
11 changed files with 239 additions and 2 deletions

View file

@ -8,6 +8,7 @@
tools:ignore="QueryAllPackagesPermission" /> tools:ignore="QueryAllPackagesPermission" />
<uses-permission android:name="android.permission.ACCESS_HIDDEN_PROFILES" /> <uses-permission android:name="android.permission.ACCESS_HIDDEN_PROFILES" />
<uses-permission android:name="android.permission.EXPAND_STATUS_BAR" /> <uses-permission android:name="android.permission.EXPAND_STATUS_BAR" />
<!--<uses-permission android:name="android.permission.BIND_APPWIDGET" />-->
<application <application
android:name=".Application" android:name=".Application"

View file

@ -12,6 +12,8 @@ import android.os.Build.VERSION_CODES
import android.os.UserHandle import android.os.UserHandle
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import android.appwidget.AppWidgetHost
import android.appwidget.AppWidgetManager
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import de.jrpie.android.launcher.actions.TorchManager import de.jrpie.android.launcher.actions.TorchManager
import de.jrpie.android.launcher.apps.AbstractAppInfo import de.jrpie.android.launcher.apps.AbstractAppInfo
@ -24,9 +26,15 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
const val APP_WIDGET_HOST_ID = 42;
class Application : android.app.Application() { class Application : android.app.Application() {
val apps = MutableLiveData<List<AbstractDetailedAppInfo>>() val apps = MutableLiveData<List<AbstractDetailedAppInfo>>()
val privateSpaceLocked = MutableLiveData<Boolean>() val privateSpaceLocked = MutableLiveData<Boolean>()
lateinit var appWidgetHost: AppWidgetHost
lateinit var appWidgetManager: AppWidgetManager
private val profileAvailabilityBroadcastReceiver = object : BroadcastReceiver() { private val profileAvailabilityBroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) { override fun onReceive(context: Context?, intent: Intent?) {
@ -103,10 +111,15 @@ class Application : android.app.Application() {
torchManager = TorchManager(this) torchManager = TorchManager(this)
} }
appWidgetHost = AppWidgetHost(this.applicationContext, APP_WIDGET_HOST_ID)
appWidgetManager = AppWidgetManager.getInstance(this.applicationContext)
appWidgetHost.startListening()
val preferences = PreferenceManager.getDefaultSharedPreferences(this) val preferences = PreferenceManager.getDefaultSharedPreferences(this)
LauncherPreferences.init(preferences, this.resources) LauncherPreferences.init(preferences, this.resources)
// Try to restore old preferences // Try to restore old preferences
migratePreferencesToNewVersion(this) migratePreferencesToNewVersion(this)
@ -157,4 +170,10 @@ class Application : android.app.Application() {
apps.postValue(getApps(packageManager, applicationContext)) apps.postValue(getApps(packageManager, applicationContext))
} }
} }
override fun onTerminate() {
appWidgetHost.stopListening()
super.onTerminate()
}
} }

View file

@ -6,6 +6,9 @@ import android.app.role.RoleManager
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.appwidget.AppWidgetProviderInfo
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.LauncherApps import android.content.pm.LauncherApps

View file

@ -8,6 +8,7 @@ import de.jrpie.android.launcher.actions.lock.LockMethod;
import de.jrpie.android.launcher.preferences.serialization.MapAbstractAppInfoStringPreferenceSerializer; import de.jrpie.android.launcher.preferences.serialization.MapAbstractAppInfoStringPreferenceSerializer;
import de.jrpie.android.launcher.preferences.serialization.SetAbstractAppInfoPreferenceSerializer; import de.jrpie.android.launcher.preferences.serialization.SetAbstractAppInfoPreferenceSerializer;
import de.jrpie.android.launcher.preferences.serialization.SetPinnedShortcutInfoPreferenceSerializer; import de.jrpie.android.launcher.preferences.serialization.SetPinnedShortcutInfoPreferenceSerializer;
import de.jrpie.android.launcher.preferences.serialization.SetWidgetInfoSerializer;
import de.jrpie.android.launcher.preferences.theme.Background; import de.jrpie.android.launcher.preferences.theme.Background;
import de.jrpie.android.launcher.preferences.theme.ColorTheme; import de.jrpie.android.launcher.preferences.theme.ColorTheme;
import de.jrpie.android.launcher.preferences.theme.Font; import de.jrpie.android.launcher.preferences.theme.Font;
@ -26,6 +27,7 @@ import eu.jonahbauer.android.preference.annotations.Preferences;
@Preference(name = "started_time", type = long.class), @Preference(name = "started_time", type = long.class),
// see PREFERENCE_VERSION in de.jrpie.android.launcher.preferences.Preferences.kt // see PREFERENCE_VERSION in de.jrpie.android.launcher.preferences.Preferences.kt
@Preference(name = "version_code", type = int.class, defaultValue = "-1"), @Preference(name = "version_code", type = int.class, defaultValue = "-1"),
@Preference(name = "widgets", type = Set.class, serializer = SetWidgetInfoSerializer.class)
}), }),
@PreferenceGroup(name = "apps", prefix = "settings_apps_", suffix = "_key", value = { @PreferenceGroup(name = "apps", prefix = "settings_apps_", suffix = "_key", value = {
@Preference(name = "favorites", type = Set.class, serializer = SetAbstractAppInfoPreferenceSerializer.class), @Preference(name = "favorites", type = Set.class, serializer = SetAbstractAppInfoPreferenceSerializer.class),

View file

@ -4,6 +4,7 @@ package de.jrpie.android.launcher.preferences.serialization
import de.jrpie.android.launcher.apps.AbstractAppInfo import de.jrpie.android.launcher.apps.AbstractAppInfo
import de.jrpie.android.launcher.apps.PinnedShortcutInfo import de.jrpie.android.launcher.apps.PinnedShortcutInfo
import de.jrpie.android.launcher.widgets.WidgetInfo
import eu.jonahbauer.android.preference.annotations.serializer.PreferenceSerializationException import eu.jonahbauer.android.preference.annotations.serializer.PreferenceSerializationException
import eu.jonahbauer.android.preference.annotations.serializer.PreferenceSerializer import eu.jonahbauer.android.preference.annotations.serializer.PreferenceSerializer
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -28,6 +29,24 @@ class SetAbstractAppInfoPreferenceSerializer :
} }
} }
@Suppress("UNCHECKED_CAST")
class SetWidgetInfoSerializer :
PreferenceSerializer<java.util.Set<WidgetInfo>?, java.util.Set<java.lang.String>?> {
@Throws(PreferenceSerializationException::class)
override fun serialize(value: java.util.Set<WidgetInfo>?): java.util.Set<java.lang.String> {
return value?.map(WidgetInfo::serialize)
?.toHashSet() as java.util.Set<java.lang.String>
}
@Throws(PreferenceSerializationException::class)
override fun deserialize(value: java.util.Set<java.lang.String>?): java.util.Set<WidgetInfo>? {
return value?.map(java.lang.String::toString)?.map(WidgetInfo::deserialize)
?.toHashSet() as? java.util.Set<WidgetInfo>
}
}
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
class SetPinnedShortcutInfoPreferenceSerializer : class SetPinnedShortcutInfoPreferenceSerializer :
PreferenceSerializer<java.util.Set<PinnedShortcutInfo>?, java.util.Set<java.lang.String>?> { PreferenceSerializer<java.util.Set<PinnedShortcutInfo>?, java.util.Set<java.lang.String>?> {

View file

@ -9,9 +9,12 @@ import android.os.Bundle
import android.view.KeyEvent import android.view.KeyEvent
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup
import android.window.OnBackInvokedDispatcher import android.window.OnBackInvokedDispatcher
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.marginTop
import de.jrpie.android.launcher.Application
import de.jrpie.android.launcher.R import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.actions.Action import de.jrpie.android.launcher.actions.Action
import de.jrpie.android.launcher.actions.Gesture import de.jrpie.android.launcher.actions.Gesture
@ -20,7 +23,13 @@ import de.jrpie.android.launcher.databinding.HomeBinding
import de.jrpie.android.launcher.openTutorial import de.jrpie.android.launcher.openTutorial
import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.ui.tutorial.TutorialActivity import de.jrpie.android.launcher.ui.tutorial.TutorialActivity
import de.jrpie.android.launcher.widgets.bindAppWidget
import de.jrpie.android.launcher.widgets.createAppWidgetView
import de.jrpie.android.launcher.widgets.deleteAllWidgets
import de.jrpie.android.launcher.widgets.getAppWidgetProviders
import java.util.Locale import java.util.Locale
import kotlin.math.absoluteValue
import kotlin.random.Random
/** /**
* [HomeActivity] is the actual application Launcher, * [HomeActivity] is the actual application Launcher,
@ -73,6 +82,17 @@ class HomeActivity : UIObject, AppCompatActivity() {
binding.buttonFallbackSettings.setOnClickListener { binding.buttonFallbackSettings.setOnClickListener {
LauncherAction.SETTINGS.invoke(this) LauncherAction.SETTINGS.invoke(this)
} }
deleteAllWidgets(this)
LauncherPreferences.internal().widgets().forEach { widget ->
createAppWidgetView(this, widget)?.let {
binding.homeWidgetContainer.addView(it)
}
}
// TODO: appWidgetHost.deleteAppWidgetId(appWidgetId)
} }
override fun onConfigurationChanged(newConfig: Configuration) { override fun onConfigurationChanged(newConfig: Configuration) {

View file

@ -0,0 +1,12 @@
package de.jrpie.android.launcher.ui.widgets
import android.content.Context
import android.util.AttributeSet
import android.widget.LinearLayout
// TODO: implement layout logic instead of linear layout
class WidgetContainerView(context: Context, attrs: AttributeSet?): LinearLayout(context, attrs) {
init {
orientation = VERTICAL
}
}

View file

@ -0,0 +1,28 @@
package de.jrpie.android.launcher.widgets;
import de.jrpie.android.launcher.apps.AbstractAppInfo
import kotlinx.serialization.SerialName;
import kotlinx.serialization.Serializable;
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@Serializable
@SerialName("widget")
class WidgetInfo(val id: Int, val width: Int, val height: Int) {
fun serialize(): String {
return Json.encodeToString(this)
}
override fun hashCode(): Int {
return id
}
override fun equals(other: Any?): Boolean {
return (other as? WidgetInfo)?.id == id
}
companion object {
fun deserialize(serialized: String): WidgetInfo {
return Json.decodeFromString(serialized)
}
}
}

View file

@ -0,0 +1,123 @@
package de.jrpie.android.launcher.widgets
import android.app.Activity
import android.app.Service
import android.appwidget.AppWidgetHostView
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProviderInfo
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.os.UserManager
import android.util.Log
import de.jrpie.android.launcher.Application
import de.jrpie.android.launcher.preferences.LauncherPreferences
import kotlin.math.absoluteValue
import kotlin.random.Random
fun deleteAllWidgets(activity: Activity) {
val appWidgetHost = (activity.application as Application).appWidgetHost
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
appWidgetHost.appWidgetIds.forEach { deleteAppWidget(activity, WidgetInfo(it, 0,0)) }
}
}
fun bindAppWidget(activity: Activity, providerInfo: AppWidgetProviderInfo): WidgetInfo? {
val appWidgetHost = (activity.application as Application).appWidgetHost
val appWidgetManager = (activity.application as Application).appWidgetManager
val appWidgetId = appWidgetHost.allocateAppWidgetId()
Log.i("Launcher", "Binding new widget ${appWidgetId}")
if (!appWidgetManager.bindAppWidgetIdIfAllowed(
appWidgetId,
providerInfo.provider
)
) {
requestAppWidgetPermission(activity, appWidgetId, providerInfo)
return null
}
try {
Log.e("widgets", "configure widget")
appWidgetHost.startAppWidgetConfigureActivityForResult(activity, appWidgetId, 0, 1, null)
} catch (e: Exception) {
e.printStackTrace()
}
val widget = WidgetInfo(appWidgetId, 500, 500)
LauncherPreferences.internal().widgets(
(LauncherPreferences.internal().widgets() ?: HashSet()).also {
it.add(widget)
}
)
return widget
}
fun deleteAppWidget(activity: Activity, widget: WidgetInfo) {
Log.i("Launcher", "Deleting widget ${widget.id}")
val appWidgetHost = (activity.application as Application).appWidgetHost
appWidgetHost.deleteAppWidgetId(widget.id)
LauncherPreferences.internal().widgets(
LauncherPreferences.internal().widgets()?.also {
it.remove(widget)
}
)
}
fun createAppWidgetView(activity: Activity, widget: WidgetInfo): AppWidgetHostView? {
val appWidgetHost = (activity.application as Application).appWidgetHost
val appWidgetManager = (activity.application as Application).appWidgetManager
val providerInfo = appWidgetManager.getAppWidgetInfo(widget.id) ?: return null
val view = appWidgetHost.createView(activity, widget.id, providerInfo)
.apply {
setAppWidget(appWidgetId, appWidgetInfo)
}
val newOptions = Bundle().apply {
putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, widget.width)
putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, widget.width)
putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, widget.height)
putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, widget.height)
}
appWidgetManager.updateAppWidgetOptions(
widget.id,
newOptions
)
//view.minimumWidth = widget.width
//view.minimumHeight = widget.height
return view
}
fun getAppWidgetProviders(context: Context): List<AppWidgetProviderInfo> {
return appWidgetProviders(context, (context.applicationContext as Application).appWidgetManager)
}
fun requestAppWidgetPermission(context: Activity, widgetId: Int, info: AppWidgetProviderInfo) {
val intent = Intent(AppWidgetManager.ACTION_APPWIDGET_BIND).apply {
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId)
putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER, info.provider)
}
context.startActivityForResult(intent, 0)//REQUEST_CODE_BIND_WIDGET)
}
fun appWidgetProviders(
context: Context,
appWidgetManager: AppWidgetManager
): List<AppWidgetProviderInfo> {
val userManager = context.getSystemService(Service.USER_SERVICE) as UserManager
return userManager.userProfiles.map {
appWidgetManager.getInstalledProvidersForProfile(it)
}.flatten()
}
fun Activity.bindRandomWidget() {
val selectedWidget =
getAppWidgetProviders(this).let { it.get(Random.nextInt().absoluteValue % it.size) }
bindAppWidget(this, selectedWidget) ?: return
}

View file

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

View file

@ -9,6 +9,7 @@
<string name="settings_internal_started_key" translatable="false">internal.started_before</string> <string name="settings_internal_started_key" translatable="false">internal.started_before</string>
<string name="settings_internal_started_time_key" translatable="false">internal.first_startup</string> <string name="settings_internal_started_time_key" translatable="false">internal.first_startup</string>
<string name="settings_internal_version_code_key" translatable="false">internal.version_code</string> <string name="settings_internal_version_code_key" translatable="false">internal.version_code</string>
<string name="settings_internal_widgets_key" translatable="false">internal.widgets</string>
<string name="settings_apps_favorites_key" translatable="false">apps.favorites</string> <string name="settings_apps_favorites_key" translatable="false">apps.favorites</string>
<string name="settings_apps_hidden_key" translatable="false">apps.hidden</string> <string name="settings_apps_hidden_key" translatable="false">apps.hidden</string>
<string name="settings_apps_pinned_shortcuts_key" translatable="false">apps.pinned_shortcuts</string> <string name="settings_apps_pinned_shortcuts_key" translatable="false">apps.pinned_shortcuts</string>