React to changing profile availability

This commit is contained in:
MM20
2024-07-07 14:29:42 +02:00
parent 84f3d9f825
commit 5a28eb584e
29 changed files with 424 additions and 77 deletions
+1
View File
@@ -0,0 +1 @@
/build
+50
View File
@@ -0,0 +1,50 @@
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.plugin.serialization)
}
android {
compileSdk = libs.versions.compileSdk.get().toInt()
defaultConfig {
minSdk = libs.versions.minSdk.get().toInt()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
create("nightly") {
initWith(getByName("release"))
matchingFallbacks += "release"
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
namespace = "de.mm20.launcher2.profiles"
}
dependencies {
implementation(libs.bundles.kotlin)
implementation(libs.androidx.core)
implementation(libs.koin.android)
implementation(project(":core:base"))
implementation(project(":core:ktx"))
implementation(project(":core:permissions"))
}
View File
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
@@ -0,0 +1,8 @@
package de.mm20.launcher2.profiles
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
val profilesModule = module {
single<ProfileManager> { ProfileManager(androidContext(), get()) }
}
@@ -0,0 +1,168 @@
package de.mm20.launcher2.profiles
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.LauncherApps
import android.os.Process
import android.os.UserHandle
import android.os.UserManager
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.content.getSystemService
import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
internal data class ProfileWithState(
val profile: Profile,
val state: Profile.State,
)
class ProfileManager(
private val context: Context,
private val permissionsManager: PermissionsManager,
) {
private val userManager = context.getSystemService<UserManager>()!!
private val launcherApps = context.getSystemService<LauncherApps>()!!
private val scope = CoroutineScope(Dispatchers.Default + Job())
private val profileStates: MutableStateFlow<List<ProfileWithState>> =
MutableStateFlow(emptyList())
/**
* List of profiles that are active and unlocked.
*/
val activeProfiles: Flow<List<Profile>> = profileStates.map {
it.mapNotNull {
if (it.state.hidden) null else it.profile
}
}.shareIn(scope, SharingStarted.WhileSubscribed(), replay = 1)
init {
val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
scope.launch {
refreshProfiles()
}
}
}
context.registerReceiver(
receiver, IntentFilter().apply {
addAction(Intent.ACTION_MANAGED_PROFILE_ADDED)
addAction(Intent.ACTION_MANAGED_PROFILE_REMOVED)
addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE)
addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)
addAction(Intent.ACTION_MANAGED_PROFILE_UNLOCKED)
if (isAtLeastApiLevel(34)) {
addAction(Intent.ACTION_PROFILE_ADDED)
addAction(Intent.ACTION_PROFILE_REMOVED)
}
if (isAtLeastApiLevel(31)) {
addAction(Intent.ACTION_PROFILE_ACCESSIBLE)
addAction(Intent.ACTION_PROFILE_INACCESSIBLE)
}
}
)
scope.launch {
if (isAtLeastApiLevel(35)) {
permissionsManager.hasPermission(PermissionGroup.HiddenProfiles).collectLatest {
refreshProfiles()
}
} else {
refreshProfiles()
}
}
}
private val mutex = Mutex()
private suspend fun refreshProfiles() {
mutex.withLock {
val profiles = mutableListOf<ProfileWithState>()
for (userHandle in launcherApps.profiles) {
profiles.add(
ProfileWithState(
Profile(
type = getProfileType(userHandle),
userHandle = userHandle,
serial = userManager.getSerialNumberForUser(userHandle),
),
getProfileState(userHandle),
)
)
}
profileStates.value = profiles
}
}
fun getProfile(userHandle: UserHandle): Flow<Profile?> {
return profileStates.map {
it.find { it.profile.userHandle == userHandle }?.profile
}
}
fun getProfileState(profile: Profile): Flow<Profile.State?> {
return profileStates.map { profiles ->
profiles.find { it.profile == profile }?.state
}
}
/**
* This only works when the launcher is installed in the primary profile.
*/
private fun getProfileType(userHandle: UserHandle): Profile.Type {
return when {
userManager.isManagedProfile(userHandle) -> Profile.Type.Work
userHandle == Process.myUserHandle() -> Profile.Type.Personal
else -> Profile.Type.Private
}
}
private fun getProfileState(userHandle: UserHandle): Profile.State {
return Profile.State(
locked = !userManager.isUserUnlocked(userHandle),
hidden = !userManager.isUserUnlocked(userHandle),
)
}
@RequiresApi(28)
fun unlockProfile(profile: Profile) {
userManager.requestQuietModeEnabled(false, profile.userHandle)
}
@RequiresApi(28)
fun lockProfile(profile: Profile) {
userManager.requestQuietModeEnabled(true, profile.userHandle)
}
}
internal fun UserManager.isManagedProfile(userHandle: UserHandle): Boolean {
try {
val isManagedProfile = UserManager::class.java.getDeclaredMethod(
"isManagedProfile",
Int::class.javaPrimitiveType
)
val serial = getSerialNumberForUser(userHandle).toInt()
return isManagedProfile.invoke(this, serial) as Boolean
} catch (e: Exception) {
Log.e("MM20", "isManagedProfile could not be invoked", e)
return false
}
}