basic support for private space (#98)

* add an action to toggle private space lock
* hide apps from private space if private space is locked
This commit is contained in:
Josia Pietsch 2025-01-22 01:44:51 +01:00
parent 679c90130d
commit 785e024ddb
Signed by: jrpie
GPG key ID: E70B571D66986A2D
12 changed files with 159 additions and 28 deletions

View file

@ -1,5 +1,9 @@
package de.jrpie.android.launcher
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.SharedPreferences
import android.content.pm.LauncherApps
import android.content.pm.ShortcutInfo
@ -7,6 +11,7 @@ import android.os.AsyncTask
import android.os.Build
import android.os.Build.VERSION_CODES
import android.os.UserHandle
import androidx.core.content.ContextCompat
import androidx.lifecycle.MutableLiveData
import androidx.preference.PreferenceManager
import de.jrpie.android.launcher.actions.TorchManager
@ -19,6 +24,14 @@ import de.jrpie.android.launcher.preferences.resetPreferences
class Application : android.app.Application() {
val apps = MutableLiveData<List<DetailedAppInfo>>()
private val profileAvailabilityBroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
// TODO: only update specific apps
// use Intent.EXTRA_USER
loadApps()
}
}
// TODO: only update specific apps
private val launcherAppsCallback = object : LauncherApps.Callback() {
override fun onPackageRemoved(p0: String?, p1: UserHandle?) {
@ -107,6 +120,21 @@ class Application : android.app.Application() {
val launcherApps = getSystemService(LAUNCHER_APPS_SERVICE) as LauncherApps
launcherApps.registerCallback(launcherAppsCallback)
if (Build.VERSION.SDK_INT >= VERSION_CODES.N) {
val filter = IntentFilter().also {
if (Build.VERSION.SDK_INT >= VERSION_CODES.VANILLA_ICE_CREAM) {
it.addAction(Intent.ACTION_PROFILE_AVAILABLE)
it.addAction(Intent.ACTION_PROFILE_UNAVAILABLE)
} else {
it.addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE)
it.addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)
}
}
ContextCompat.registerReceiver(this, profileAvailabilityBroadcastReceiver, filter,
ContextCompat.RECEIVER_EXPORTED
)
}
loadApps()
}

View file

@ -18,6 +18,7 @@ import de.jrpie.android.launcher.actions.Action
import de.jrpie.android.launcher.actions.Gesture
import de.jrpie.android.launcher.apps.AppInfo
import de.jrpie.android.launcher.apps.DetailedAppInfo
import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.ui.tutorial.TutorialActivity
@ -30,31 +31,36 @@ const val REQUEST_SET_DEFAULT_HOME = 42
const val LOG_TAG = "Launcher"
fun setDefaultHomeScreen(context: Context, checkDefault: Boolean = false) {
if (checkDefault
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
&& context is Activity
) {
fun isDefaultHomeScreen(context: Context): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val roleManager = context.getSystemService(RoleManager::class.java)
if (!roleManager.isRoleHeld(RoleManager.ROLE_HOME)) {
context.startActivityForResult(
roleManager.createRequestRoleIntent(RoleManager.ROLE_HOME),
REQUEST_SET_DEFAULT_HOME
)
}
return
}
if (checkDefault) {
return roleManager.isRoleHeld(RoleManager.ROLE_HOME)
} else {
val testIntent = Intent(Intent.ACTION_MAIN)
testIntent.addCategory(Intent.CATEGORY_HOME)
val defaultHome = testIntent.resolveActivity(context.packageManager)?.packageName
if (defaultHome == context.packageName) {
// Launcher is already the default home app
return
}
return defaultHome == context.packageName
}
}
fun setDefaultHomeScreen(context: Context, checkDefault: Boolean = false) {
if (checkDefault && isDefaultHomeScreen(context)) {
// Launcher is already the default home app
return
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
&& context is Activity
&& checkDefault // using role manager only works when µLauncher is not already the default.
) {
val roleManager = context.getSystemService(RoleManager::class.java)
context.startActivityForResult(
roleManager.createRequestRoleIntent(RoleManager.ROLE_HOME),
REQUEST_SET_DEFAULT_HOME
)
return
}
val intent = Intent(Settings.ACTION_HOME_SETTINGS)
context.startActivity(intent)
}
@ -94,6 +100,21 @@ fun getApps(packageManager: PackageManager, context: Context): MutableList<Detai
// TODO: shortcuts - launcherApps.getShortcuts()
val users = userManager.userProfiles
for (user in users) {
// don't load apps from a user profile that has quiet mode enabled
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
if (userManager.isQuietModeEnabled(user)) {
// hide paused apps
if (LauncherPreferences.apps().hidePausedApps()) {
continue
}
// hide apps from private space
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM &&
launcherApps.getLauncherUserInfo(user)?.userType == UserManager.USER_TYPE_PROFILE_PRIVATE
) {
continue
}
}
}
launcherApps.getActivityList(null, user).forEach {
loadList.add(DetailedAppInfo(it))
}

View file

@ -1,17 +1,22 @@
package de.jrpie.android.launcher.actions
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.content.pm.LauncherApps
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.media.AudioManager
import android.os.Build
import android.os.SystemClock
import android.os.UserManager
import android.provider.Settings
import android.view.KeyEvent
import android.widget.Toast
import de.jrpie.android.launcher.Application
import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.apps.AppFilter
import de.jrpie.android.launcher.isDefaultHomeScreen
import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.ui.list.ListActivity
import de.jrpie.android.launcher.ui.settings.SettingsActivity
@ -33,7 +38,8 @@ enum class LauncherAction(
val id: String,
val label: Int,
val icon: Int,
val launch: (Context) -> Unit
val launch: (Context) -> Unit,
val available: (Context) -> Boolean = { true }
) : Action {
SETTINGS(
"settings",
@ -53,6 +59,13 @@ enum class LauncherAction(
R.drawable.baseline_favorite_24,
{ context -> openAppsList(context, true) }
),
TOGGLE_PRIVATE_SPACE_LOCK(
"toggle_private_space_lock",
R.string.list_other_toggle_private_space_lock,
R.drawable.baseline_security_24,
::togglePrivateSpaceLock,
available = { Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM }
),
VOLUME_UP(
"volume_up",
R.string.list_other_volume_up,
@ -207,6 +220,46 @@ private fun expandNotificationsPanel(context: Context) {
}
}
private fun togglePrivateSpaceLock(context: Context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) {
Toast.makeText(
context,
context.getString(R.string.alert_requires_android_v),
Toast.LENGTH_LONG
).show()
return
}
val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager
val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
val privateSpaceUser = userManager.userProfiles.firstOrNull { u ->
launcherApps.getLauncherUserInfo(u)?.userType == UserManager.USER_TYPE_PROFILE_PRIVATE
}
if (privateSpaceUser == null) {
Toast.makeText(context, context.getString(R.string.toast_private_space_not_available), Toast.LENGTH_LONG).show()
if (!isDefaultHomeScreen(context)) {
Toast.makeText(context, context.getString(R.string.toast_private_space_default_home_screen), Toast.LENGTH_LONG).show()
return
}
try {
context.startActivity(Intent(Settings.ACTION_PRIVACY_SETTINGS))
} catch (_: ActivityNotFoundException) {}
return
}
if (userManager.isQuietModeEnabled(privateSpaceUser)) {
userManager.requestQuietModeEnabled(false, privateSpaceUser)
Toast.makeText(
context,
context.getString(R.string.toast_private_space_unlocked),
Toast.LENGTH_LONG
).show()
return
}
userManager.requestQuietModeEnabled(true, privateSpaceUser)
Toast.makeText(context, context.getString(R.string.toast_private_space_locked), Toast.LENGTH_LONG).show()
}
private fun expandSettingsPanel(context: Context) {
/* https://stackoverflow.com/a/31898506 */
try {
@ -260,6 +313,7 @@ private class LauncherActionSerializer : KSerializer<LauncherAction> {
) {
element("value", String.serializer().descriptor)
}
override fun deserialize(decoder: Decoder): LauncherAction {
val s = decoder.decodeStructure(descriptor) {
decodeElementIndex(descriptor)

View file

@ -5,8 +5,8 @@ import java.util.Set;
import de.jrpie.android.launcher.R;
import de.jrpie.android.launcher.actions.lock.LockMethod;
import de.jrpie.android.launcher.preferences.serialization.SetAppInfoPreferenceSerializer;
import de.jrpie.android.launcher.preferences.serialization.MapAppInfoStringPreferenceSerializer;
import de.jrpie.android.launcher.preferences.serialization.SetAppInfoPreferenceSerializer;
import de.jrpie.android.launcher.preferences.theme.Background;
import de.jrpie.android.launcher.preferences.theme.ColorTheme;
import de.jrpie.android.launcher.preferences.theme.Font;
@ -29,6 +29,7 @@ import eu.jonahbauer.android.preference.annotations.Preferences;
@Preference(name = "hidden", type = Set.class, serializer = SetAppInfoPreferenceSerializer.class),
@Preference(name = "custom_names", type = HashMap.class, serializer = MapAppInfoStringPreferenceSerializer.class),
@Preference(name = "hide_bound_apps", type = boolean.class, defaultValue = "false"),
@Preference(name = "hide_paused_apps", type = boolean.class, defaultValue = "false"),
}),
@PreferenceGroup(name = "list", prefix = "settings_list_", suffix = "_key", value = {
@Preference(name = "layout", type = ListLayout.class, defaultValue = "DEFAULT")

View file

@ -2,8 +2,8 @@ package de.jrpie.android.launcher.preferences.theme
import android.content.Context
import android.content.res.Resources
import de.jrpie.android.launcher.R
import com.google.android.material.color.DynamicColors
import de.jrpie.android.launcher.R
@Suppress("unused")
enum class ColorTheme(

View file

@ -3,7 +3,6 @@ package de.jrpie.android.launcher.ui
import android.annotation.SuppressLint
import android.content.SharedPreferences
import android.content.res.Resources
import android.os.AsyncTask
import android.os.Build
import android.os.Bundle
import android.util.DisplayMetrics
@ -20,10 +19,7 @@ import de.jrpie.android.launcher.actions.Action
import de.jrpie.android.launcher.actions.Gesture
import de.jrpie.android.launcher.actions.LauncherAction
import de.jrpie.android.launcher.databinding.HomeBinding
import de.jrpie.android.launcher.openTutorial
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.ui.tutorial.TutorialActivity
import java.text.SimpleDateFormat
import java.util.Date

View file

@ -23,7 +23,8 @@ import de.jrpie.android.launcher.ui.list.forGesture
class OtherRecyclerAdapter(val activity: Activity) :
RecyclerView.Adapter<OtherRecyclerAdapter.ViewHolder>() {
private val othersList: Array<LauncherAction> = LauncherAction.values()
private val othersList: Array<LauncherAction> =
LauncherAction.entries.filter { it.isAvailable(activity) }.toTypedArray()
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView),
View.OnClickListener {

View file

@ -40,6 +40,11 @@ class SettingsFragmentLauncher : PreferenceFragmentCompat() {
)
val lightTheme = LauncherPreferences.theme().colorTheme() == ColorTheme.LIGHT
background?.isVisible = !lightTheme
val hidePausedApps = findPreference<androidx.preference.Preference>(
LauncherPreferences.apps().keys().hidePausedApps()
)
hidePausedApps?.isVisible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
}
override fun onStart() {

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp">
<path
android:fillColor="?android:textColor"
android:pathData="M12,1L3,5v6c0,5.55 3.84,10.74 9,12 5.16,-1.26 9,-6.45 9,-12L21,5l-9,-4zM12,11.99h7c-0.53,4.12 -3.28,7.79 -7,8.94L12,12L5,12L5,6.3l7,-3.11v8.8z" />
</vector>

View file

@ -13,6 +13,7 @@
<string name="settings_apps_hidden_key" translatable="false">apps.hidden</string>
<string name="settings_apps_custom_names_key" translatable="false">apps.custom_names</string>
<string name="settings_apps_hide_bound_apps_key" translatable="false">apps.hide_bound_apps</string>
<string name="settings_apps_hide_paused_apps_key" translatable="false">apps.hide_paused_apps</string>
<string name="settings_list_layout_key" translatable="false">list.layout</string>
<string name="settings_general_choose_home_screen_key" translatable="false">general.select_launcher</string>

View file

@ -145,6 +145,7 @@
<string name="settings_launcher_section_apps">Apps</string>
<string name="settings_apps_hidden">Hidden apps</string>
<string name="settings_apps_hide_bound_apps">Don\'t show apps that are bound to a gesture in the app list</string>
<string name="settings_apps_hide_paused_apps">Hide paused apps</string>
<string name="settings_list_layout">Layout of app list</string>
<string name="settings_list_layout_item_default">Default</string>
@ -207,6 +208,7 @@
<string name="list_other_settings">µLauncher Settings</string>
<string name="list_other_list">All Applications</string>
<string name="list_other_list_favorites">Favorite Applications</string>
<string name="list_other_toggle_private_space_lock">Toggle Private Space Lock</string>
<string name="list_other_volume_up">Music: Louder</string>
<string name="list_other_volume_down">Music: Quieter</string>
<string name="list_other_track_next">Music: Next</string>
@ -245,6 +247,7 @@
<string name="ic_menu_alt">More options</string>
<string name="alert_cant_expand_status_bar_panel">Error: Can\'t expand status bar. This action is using functionality that is not part of the published Android API. Unfortunately, it does not seem to work on your device.</string>
<string name="alert_requires_android_m">This functionality requires Android 6.0 or later.</string>
<string name="alert_requires_android_v">This functionality requires Android 15.0 or later.</string>
<string name="snackbar_app_hidden">App hidden. You can make it visible again in settings.</string>
<string name="undo">Undo</string>
<string name="list_other_expand_settings_panel">Quick Settings</string>
@ -255,6 +258,10 @@
<string name="alert_torch_access_exception">Error: Can\'t access torch.</string>
<string name="alert_lock_screen_failed">Error: Failed to lock screen. (If you just upgraded the app, try to disable and re-enable the accessibility service in phone settings)</string>
<string name="toast_accessibility_service_not_enabled">μLauncher\'s accessibility service is not enabled. Please enable it in settings</string>
<string name="toast_private_space_locked">Private space locked</string>
<string name="toast_private_space_unlocked">Private space unlocked</string>
<string name="toast_private_space_not_available">Private space is not available</string>
<string name="toast_private_space_default_home_screen">µLauncher needs to be the default home screen to access private space.</string>
<string name="toast_lock_screen_not_supported">Error: Locking the screen using accessibility is not supported on this device. Please use device admin instead.</string>
<string name="accessibility_service_name">µLauncher - lock screen</string>
<string name="accessibility_service_description">

View file

@ -145,6 +145,11 @@
android:title="@string/settings_apps_hide_bound_apps"
android:defaultValue="false" />
<SwitchPreference
android:key="@string/settings_apps_hide_paused_apps_key"
android:title="@string/settings_apps_hide_paused_apps"
android:defaultValue="false" />
<DropDownPreference
android:key="@string/settings_list_layout_key"
android:title="@string/settings_list_layout"