Repository: iamr0s/AndroidAccounts Branch: main Commit: 5448d79ddc14 Files: 59 Total size: 76.2 KB Directory structure: gitextract_gszllfdf/ ├── .gitignore ├── app/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ ├── androidTest/ │ │ └── java/ │ │ └── com/ │ │ └── rosan/ │ │ └── accounts/ │ │ └── ExampleInstrumentedTest.kt │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── com/ │ │ │ └── rosan/ │ │ │ └── accounts/ │ │ │ ├── App.kt │ │ │ ├── data/ │ │ │ │ ├── common/ │ │ │ │ │ ├── cause/ │ │ │ │ │ │ └── ShizukuNotWorkException.kt │ │ │ │ │ └── utils/ │ │ │ │ │ ├── BinderUtil.kt │ │ │ │ │ ├── CauseUtil.kt │ │ │ │ │ ├── ContextUtil.kt │ │ │ │ │ ├── KoinUtil.kt │ │ │ │ │ ├── MapsUtil.kt │ │ │ │ │ ├── ProcessUtil.kt │ │ │ │ │ ├── ShizukuUtil.kt │ │ │ │ │ └── UserHandleUtil.kt │ │ │ │ └── service/ │ │ │ │ ├── entity/ │ │ │ │ │ ├── AccountAuthenticatorEntity.kt │ │ │ │ │ ├── AccountEntity.kt │ │ │ │ │ └── UserEntity.kt │ │ │ │ ├── model/ │ │ │ │ │ └── ShizukuUserService.kt │ │ │ │ └── repo/ │ │ │ │ └── UserService.kt │ │ │ ├── di/ │ │ │ │ ├── init/ │ │ │ │ │ └── app_modules.kt │ │ │ │ ├── service_module.kt │ │ │ │ └── viewmodel_module.kt │ │ │ └── ui/ │ │ │ ├── activity/ │ │ │ │ └── MainActivity.kt │ │ │ ├── page/ │ │ │ │ ├── account_manager/ │ │ │ │ │ ├── AccountManagerPage.kt │ │ │ │ │ ├── AccountManagerViewAction.kt │ │ │ │ │ ├── AccountManagerViewModel.kt │ │ │ │ │ └── AccountManagerViewState.kt │ │ │ │ ├── main/ │ │ │ │ │ ├── MainPage.kt │ │ │ │ │ └── MainScreen.kt │ │ │ │ └── user_manager/ │ │ │ │ ├── UserManagerPage.kt │ │ │ │ ├── UserManagerViewAction.kt │ │ │ │ ├── UserManagerViewModel.kt │ │ │ │ └── UserManagerViewState.kt │ │ │ ├── theme/ │ │ │ │ ├── Color.kt │ │ │ │ ├── Theme.kt │ │ │ │ └── Type.kt │ │ │ └── widget/ │ │ │ └── PositionDialog.kt │ │ └── res/ │ │ ├── values/ │ │ │ ├── colors.xml │ │ │ ├── strings.xml │ │ │ └── themes.xml │ │ └── values-zh-rCN/ │ │ └── strings.xml │ └── test/ │ └── java/ │ └── com/ │ └── rosan/ │ └── accounts/ │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── hidden-api/ │ ├── .gitignore │ ├── build.gradle │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ └── java/ │ └── android/ │ ├── accounts/ │ │ └── IAccountManager.java │ ├── content/ │ │ └── pm/ │ │ ├── IPackageManager.java │ │ └── UserInfo.java │ └── os/ │ ├── IUserManager.java │ └── ServiceManager.java └── settings.gradle ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ *.iml .gradle /local.properties /.idea .DS_Store /build /captures /keystore .externalNativeBuild .cxx local.properties ================================================ FILE: app/.gitignore ================================================ /build ================================================ FILE: app/build.gradle ================================================ plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' } def keystoreProps = new Properties() def keystorePropsFile = rootProject.file('keystore/r0s.properties') if (keystorePropsFile.exists()) { keystoreProps.load(new FileInputStream(keystorePropsFile)) } android { namespace 'com.rosan.accounts' compileSdk 33 defaultConfig { applicationId "com.rosan.accounts" minSdk 21 targetSdk 33 versionCode 6 versionName "1.5" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { useSupportLibrary true } } signingConfigs { debug { keyAlias keystoreProps['keyAlias'] keyPassword keystoreProps['keyPassword'] storeFile keystoreProps['storeFile'] ? file(keystoreProps['storeFile']) : null storePassword keystoreProps['storePassword'] v1SigningEnabled true v2SigningEnabled true } release { keyAlias keystoreProps['keyAlias'] storePassword keystoreProps['storePassword'] keyPassword keystoreProps['keyPassword'] storeFile keystoreProps['storeFile'] ? file(new File(keystoreProps['storeFile'])) : null v1SigningEnabled true v2SigningEnabled true } } buildTypes { debug { signingConfig signingConfigs.debug minifyEnabled false zipAlignEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } release { signingConfig signingConfigs.release minifyEnabled true zipAlignEnabled true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = '1.8' } buildFeatures { buildConfig true compose true } composeOptions { kotlinCompilerExtensionVersion '1.4.5' } packagingOptions { resources { excludes += '/META-INF/{AL2.0,LGPL2.1}' } } } dependencies { implementation 'androidx.core:core-ktx:1.10.1' implementation platform('org.jetbrains.kotlin:kotlin-bom:1.8.0') implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1' implementation 'androidx.activity:activity-compose:1.7.2' implementation platform('androidx.compose:compose-bom:2022.10.00') implementation 'androidx.compose.ui:ui' implementation 'androidx.compose.ui:ui-graphics' implementation 'androidx.compose.ui:ui-tooling-preview' implementation 'androidx.compose.material:material' implementation 'androidx.compose.material:material-icons-extended' implementation 'androidx.compose.material3:material3' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' androidTestImplementation platform('androidx.compose:compose-bom:2022.10.00') androidTestImplementation 'androidx.compose.ui:ui-test-junit4' debugImplementation 'androidx.compose.ui:ui-tooling' debugImplementation 'androidx.compose.ui:ui-test-manifest' compileOnly project(':hidden-api') implementation 'androidx.navigation:navigation-compose:2.6.0' implementation 'org.lsposed.hiddenapibypass:hiddenapibypass:4.3' def accompanist_version = '0.30.1' implementation "com.google.accompanist:accompanist-drawablepainter:$accompanist_version" implementation "com.google.accompanist:accompanist-navigation-animation:$accompanist_version" implementation "com.google.accompanist:accompanist-insets:$accompanist_version" implementation "com.google.accompanist:accompanist-insets-ui:$accompanist_version" implementation "com.google.accompanist:accompanist-systemuicontroller:$accompanist_version" implementation 'io.insert-koin:koin-core:3.4.2' implementation 'io.insert-koin:koin-android:3.4.2' implementation 'io.insert-koin:koin-androidx-compose:3.4.5' def shizuku_version = "13.1.4" implementation "dev.rikka.shizuku:api:$shizuku_version" implementation "dev.rikka.shizuku:provider:$shizuku_version" } ================================================ FILE: app/proguard-rules.pro ================================================ # 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 -keep public interface ** extends android.os.IInterface {*;} -keep public class ** extends android.app.Activity -dontwarn ** ================================================ FILE: app/src/androidTest/java/com/rosan/accounts/ExampleInstrumentedTest.kt ================================================ package com.rosan.accounts import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Test import org.junit.runner.RunWith import org.junit.Assert.* /** * Instrumented test, which will execute on an Android device. * * See [testing documentation](http://d.android.com/tools/testing). */ @RunWith(AndroidJUnit4::class) class ExampleInstrumentedTest { @Test fun useAppContext() { // Context of the app under test. val appContext = InstrumentationRegistry.getInstrumentation().targetContext assertEquals("com.rosan.accounts", appContext.packageName) } } ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/java/com/rosan/accounts/App.kt ================================================ package com.rosan.accounts import android.app.Application import android.os.Build import com.rosan.accounts.di.init.appModules import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidLogger import org.koin.core.context.startKoin import org.lsposed.hiddenapibypass.HiddenApiBypass class App : Application() { override fun onCreate() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) HiddenApiBypass.addHiddenApiExemptions("") super.onCreate() startKoin { androidLogger() androidContext(this@App) modules(appModules) } } } ================================================ FILE: app/src/main/java/com/rosan/accounts/data/common/cause/ShizukuNotWorkException.kt ================================================ package com.rosan.accounts.data.common.cause data class ShizukuNotWorkException( override val message: String? = null, override val cause: Throwable? = null ) : RuntimeException(message, cause) ================================================ FILE: app/src/main/java/com/rosan/accounts/data/common/utils/BinderUtil.kt ================================================ package com.rosan.accounts.data.common.utils import android.os.Binder import android.os.IBinder import android.os.Parcel import android.os.ParcelFileDescriptor import java.io.FileDescriptor fun IBinder.transactDump(fd: FileDescriptor, args: Array? = null) { val data = Parcel.obtain() val reply = Parcel.obtain() try { data.writeFileDescriptor(fd) data.writeStringArray(args) this.transact(Binder.DUMP_TRANSACTION, data, reply, 0) reply.readException() } finally { data.recycle() reply.recycle() } } fun IBinder.dumpBytes(args: Array? = null): ByteArray { val pipe = ParcelFileDescriptor.createPipe() val readFD = pipe[0] val writeFD = pipe[1] writeFD.use { this.transactDump(it.fileDescriptor, args) } return readFD.use { ParcelFileDescriptor.AutoCloseInputStream(it) .readBytes() } } fun IBinder.dumpText(args: Array? = null): String = dumpBytes(args).decodeToString() ================================================ FILE: app/src/main/java/com/rosan/accounts/data/common/utils/CauseUtil.kt ================================================ package com.rosan.accounts.data.common.utils import android.content.Context import com.rosan.accounts.R import com.rosan.accounts.data.common.cause.ShizukuNotWorkException fun Throwable.help(): String? { val context = defaultKoin().get() return when (this) { is ShizukuNotWorkException -> context.getString(R.string.shizuku_not_working) else -> null } } ================================================ FILE: app/src/main/java/com/rosan/accounts/data/common/utils/ContextUtil.kt ================================================ package com.rosan.accounts.data.common.utils import android.content.ClipData import android.content.ClipboardManager import android.content.Context import android.content.Intent import android.net.Uri import android.os.Handler import android.os.Looper import android.widget.Toast import androidx.annotation.StringRes fun runOnUiThread(action: () -> Unit) { Handler(Looper.getMainLooper()).post { action.invoke() } } fun Context.copy(text: CharSequence) { runOnUiThread { val manager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager manager.setPrimaryClip(ClipData.newPlainText("Label", text)) } } fun Context.openUrlInBrowser(url: String) { runOnUiThread { val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) startActivity(intent) } } fun Context.toast(text: CharSequence, duration: Int = Toast.LENGTH_SHORT) { runOnUiThread { Toast.makeText(this, text, duration).show() } } fun Context.toast(@StringRes resId: Int, duration: Int = Toast.LENGTH_SHORT) { runOnUiThread { toast(getString(resId), duration) } } ================================================ FILE: app/src/main/java/com/rosan/accounts/data/common/utils/KoinUtil.kt ================================================ package com.rosan.accounts.data.common.utils import org.koin.core.Koin import org.koin.mp.KoinPlatformTools fun defaultKoin(): Koin = KoinPlatformTools.defaultContext().get() ================================================ FILE: app/src/main/java/com/rosan/accounts/data/common/utils/MapsUtil.kt ================================================ package com.rosan.accounts.data.common.utils inline fun MutableMap.replace(key: K, action: (value: V?) -> V): V { val newValue = this[key].let(action) this[key] = newValue return newValue } ================================================ FILE: app/src/main/java/com/rosan/accounts/data/common/utils/ProcessUtil.kt ================================================ package com.rosan.accounts.data.common.utils fun Process.isActive(): Boolean { return try { exitValue() false } catch (e: IllegalArgumentException) { true } } ================================================ FILE: app/src/main/java/com/rosan/accounts/data/common/utils/ShizukuUtil.kt ================================================ package com.rosan.accounts.data.common.utils import android.content.Context import android.content.pm.PackageManager import android.os.IBinder import android.os.ServiceManager import com.rosan.accounts.data.common.cause.ShizukuNotWorkException import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.first import rikka.shizuku.Shizuku import rikka.shizuku.ShizukuBinderWrapper import rikka.sui.Sui private suspend fun blockingRequestShizukuPermission() = callbackFlow { val requestCode = (Int.MIN_VALUE..Int.MAX_VALUE).random() val listener = Shizuku.OnRequestPermissionResultListener { _requestCode, grantResult -> if (_requestCode != requestCode) return@OnRequestPermissionResultListener if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) trySend(Unit) else close(ShizukuNotWorkException("sui/shizuku permission denied")) } Shizuku.addRequestPermissionResultListener(listener) Shizuku.requestPermission(requestCode) awaitClose { Shizuku.removeRequestPermissionResultListener(listener) } }.catch { throw if (it !is ShizukuNotWorkException) ShizukuNotWorkException(cause = it) else it }.first() suspend fun requireShizukuPermissionGranted(context: Context, action: suspend () -> T): T { Sui.init(context.packageName) val binder = Shizuku.getBinder() if (binder == null || !binder.pingBinder()) throw ShizukuNotWorkException("sui/shizuku isn't activated") if (Shizuku.checkSelfPermission() != PackageManager.PERMISSION_GRANTED) blockingRequestShizukuPermission() return action() } fun shizukuBinder(name: String): IBinder = shizukuBinder(ServiceManager.getService(name)) fun shizukuBinder(binder: IBinder): IBinder = ShizukuBinderWrapper(binder) ================================================ FILE: app/src/main/java/com/rosan/accounts/data/common/utils/UserHandleUtil.kt ================================================ package com.rosan.accounts.data.common.utils import android.os.UserHandle val UserHandle.id: Int get() = this.hashCode() ================================================ FILE: app/src/main/java/com/rosan/accounts/data/service/entity/AccountAuthenticatorEntity.kt ================================================ package com.rosan.accounts.data.service.entity import android.graphics.drawable.Drawable data class AccountAuthenticatorEntity( val userId: Int, val type: String, val packageName: String, val label: String, val icon: Drawable ) ================================================ FILE: app/src/main/java/com/rosan/accounts/data/service/entity/AccountEntity.kt ================================================ package com.rosan.accounts.data.service.entity data class AccountEntity( val userId: Int, val type: String, val name: String ) ================================================ FILE: app/src/main/java/com/rosan/accounts/data/service/entity/UserEntity.kt ================================================ package com.rosan.accounts.data.service.entity data class UserEntity( val id: Int, val name: String? ) ================================================ FILE: app/src/main/java/com/rosan/accounts/data/service/model/ShizukuUserService.kt ================================================ package com.rosan.accounts.data.service.model import android.accounts.IAccountManager import android.content.Context import android.content.pm.IPackageManager import android.os.Build import android.os.IUserManager import android.util.Log import com.rosan.accounts.data.common.utils.dumpText import com.rosan.accounts.data.common.utils.requireShizukuPermissionGranted import com.rosan.accounts.data.common.utils.shizukuBinder import com.rosan.accounts.data.service.entity.AccountAuthenticatorEntity import com.rosan.accounts.data.service.entity.AccountEntity import com.rosan.accounts.data.service.entity.UserEntity import com.rosan.accounts.data.service.repo.UserService import rikka.shizuku.Shizuku class ShizukuUserService(private val context: Context) : UserService { private val basePackageManager by lazy { context.packageManager } private val userManager by lazy { IUserManager.Stub.asInterface(shizukuBinder(Context.USER_SERVICE)) } private val accountManager by lazy { IAccountManager.Stub.asInterface(shizukuBinder(Context.ACCOUNT_SERVICE)) } private val packageManager by lazy { IPackageManager.Stub.asInterface(shizukuBinder("package")) } override suspend fun removeUser(user: UserEntity): Boolean = removeUser(user.id) override suspend fun removeUser(userId: Int): Boolean = userManager.removeUser(userId) override suspend fun getUsers(): List = requireShizukuPermissionGranted(context) { (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) userManager.getUsers(false, false, false) else userManager.getUsers(false)).map { UserEntity(id = it.id, name = it.name) } } override suspend fun getAccountAuthenticators(userId: Int): List = requireShizukuPermissionGranted(context) { accountManager.getAuthenticatorTypes(userId).map { description -> val packageName = description.packageName val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) packageManager.getPackageInfo(packageName, 0L, userId) else packageManager.getPackageInfo(packageName, 0, userId) val applicationInfo = packageInfo.applicationInfo val name = basePackageManager.getText(packageName, description.labelId, applicationInfo) val label = basePackageManager.getApplicationLabel(applicationInfo).toString().let { if (it == name) it else "$it - $name" } val icon = basePackageManager.getApplicationIcon(applicationInfo) AccountAuthenticatorEntity( userId = userId, type = description.type, packageName = description.packageName, label = label, icon = icon ) } } override suspend fun getAccounts(userId: Int): List = requireShizukuPermissionGranted(context) { if (Shizuku.getUid() == 0) getAccountsByManager(userId) else getAccountsByDump(userId) } private fun getAccountsByManager(userId: Int): List { return (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { val packageName = basePackageManager.getPackagesForUid(Shizuku.getUid())?.firstOrNull() ?: "android" accountManager.getAccountsAsUser(null, userId, packageName) } else accountManager.getAccountsAsUser(null, userId)).map { AccountEntity( userId = userId, type = it.type, name = it.name ) } } private fun getAccountsByDump(userId: Int): List { val text = accountManager.asBinder().dumpText() fun getUserIds() = "User UserInfo\\{(\\d+):.*?\\}".toRegex() .findAll(text) .toList() .map { it.groupValues[1].toInt() } fun getLengthsOfAccounts() = "Accounts: (\\d+)".toRegex() .findAll(text) .toList() .map { it.groupValues[1].toInt() } val userIds = getUserIds() val index = userIds.indexOf(userId) val lengthsOfAccounts = getLengthsOfAccounts() val skipLength = lengthsOfAccounts.slice(0 until index).fold(0) { cur, len -> cur + len } val length = lengthsOfAccounts[index] return "Account \\{name=(.*), type=(.*)\\}".toRegex() .findAll(text) .toList() .slice(skipLength until skipLength + length) .map { val name = it.groupValues[1] val type = it.groupValues[2] AccountEntity( userId = userId, type = type, name = name ) } } } ================================================ FILE: app/src/main/java/com/rosan/accounts/data/service/repo/UserService.kt ================================================ package com.rosan.accounts.data.service.repo import com.rosan.accounts.data.service.entity.AccountAuthenticatorEntity import com.rosan.accounts.data.service.entity.AccountEntity import com.rosan.accounts.data.service.entity.UserEntity interface UserService { suspend fun removeUser(user: UserEntity): Boolean suspend fun removeUser(userId: Int): Boolean suspend fun getUsers(): List suspend fun getAccountAuthenticators(userId: Int): List suspend fun getAccounts(userId: Int): List } ================================================ FILE: app/src/main/java/com/rosan/accounts/di/init/app_modules.kt ================================================ package com.rosan.accounts.di.init import com.rosan.accounts.di.serviceModule import com.rosan.accounts.di.viewModelModule val appModules = listOf( viewModelModule, serviceModule ) ================================================ FILE: app/src/main/java/com/rosan/accounts/di/service_module.kt ================================================ package com.rosan.accounts.di import com.rosan.accounts.data.service.model.ShizukuUserService import com.rosan.accounts.data.service.repo.UserService import org.koin.android.ext.koin.androidContext import org.koin.dsl.module val serviceModule = module { single { ShizukuUserService(androidContext()) } } ================================================ FILE: app/src/main/java/com/rosan/accounts/di/viewmodel_module.kt ================================================ package com.rosan.accounts.di import com.rosan.accounts.ui.page.account_manager.AccountManagerViewModel import com.rosan.accounts.ui.page.user_manager.UserManagerViewModel import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module val viewModelModule = module { viewModel { UserManagerViewModel() } viewModel { AccountManagerViewModel(get()) } } ================================================ FILE: app/src/main/java/com/rosan/accounts/ui/activity/MainActivity.kt ================================================ package com.rosan.accounts.ui.activity import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.ui.Modifier import com.rosan.accounts.ui.page.main.MainPage import com.rosan.accounts.ui.theme.AccountsTheme class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { AccountsTheme { Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { MainPage() } } } } } ================================================ FILE: app/src/main/java/com/rosan/accounts/ui/page/account_manager/AccountManagerPage.kt ================================================ package com.rosan.accounts.ui.page.account_manager import androidx.compose.animation.AnimatedContent import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.spring import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.displayCutout import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.twotone.ContentCopy import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedCard import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.navigation.NavController import com.google.accompanist.drawablepainter.rememberDrawablePainter import com.rosan.accounts.R import com.rosan.accounts.data.common.utils.copy import com.rosan.accounts.data.common.utils.toast import org.json.JSONArray import org.koin.androidx.compose.getViewModel import org.koin.core.parameter.parametersOf @OptIn( ExperimentalMaterial3Api::class, ExperimentalAnimationApi::class, ExperimentalFoundationApi::class ) @Composable fun AccountManagerPage( userId: Int, navController: NavController, viewModel: AccountManagerViewModel = getViewModel { parametersOf(userId) } ) { SideEffect { viewModel.dispatch(AccountManagerViewAction.Load) } val context = LocalContext.current Scaffold( modifier = Modifier .fillMaxSize() .windowInsetsPadding(WindowInsets.displayCutout), topBar = { TopAppBar( title = { Text(stringResource(R.string.account_manager)) }, actions = { IconButton(onClick = { val array = JSONArray() viewModel.state.authenticators.forEach { array.put(it.auth.packageName) } context.copy(array.toString()) context.toast(R.string.copied_format_hail) }) { Icon( imageVector = Icons.TwoTone.ContentCopy, contentDescription = null ) } } ) }, ) { AnimatedContent( viewModel.state.authenticators.isEmpty(), modifier = Modifier .fillMaxSize() .padding(it) ) { if (it) { Box( modifier = Modifier.fillMaxSize() ) { Text( stringResource(R.string.account_empty), modifier = Modifier.align(Alignment.Center) ) } } else { LazyColumn( modifier = Modifier.fillMaxSize(), contentPadding = PaddingValues(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp) ) { items(viewModel.state.authenticators, key = { it.auth.type }) { var alpha by remember { mutableStateOf(0f) } ItemWidget( modifier = Modifier .fillMaxWidth() // .clip(RoundedCornerShape(8.dp)) .animateItemPlacement() .graphicsLayer( alpha = animateFloatAsState( targetValue = alpha, animationSpec = spring(stiffness = 100f) ).value ), authenticator = it ) SideEffect { alpha = 1f } } } } } } } @Composable private fun ItemWidget( modifier: Modifier = Modifier, authenticator: AccountManagerViewState.Authenticator ) { OutlinedCard(modifier = modifier) { Row( modifier = Modifier .padding(horizontal = 24.dp, vertical = 16.dp), horizontalArrangement = Arrangement.spacedBy(24.dp) ) { Image( modifier = Modifier .size(32.dp) .align(Alignment.CenterVertically), painter = rememberDrawablePainter(authenticator.auth.icon), contentDescription = null ) Column { Text(authenticator.auth.label, style = MaterialTheme.typography.titleMedium) Text(authenticator.auth.type, style = MaterialTheme.typography.bodyMedium) } } } } ================================================ FILE: app/src/main/java/com/rosan/accounts/ui/page/account_manager/AccountManagerViewAction.kt ================================================ package com.rosan.accounts.ui.page.account_manager sealed class AccountManagerViewAction { object Load : AccountManagerViewAction() } ================================================ FILE: app/src/main/java/com/rosan/accounts/ui/page/account_manager/AccountManagerViewModel.kt ================================================ package com.rosan.accounts.ui.page.account_manager import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.rosan.accounts.data.common.utils.replace import com.rosan.accounts.data.service.repo.UserService import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent import org.koin.core.component.inject class AccountManagerViewModel( private val userId: Int ) : ViewModel(), KoinComponent { private val jobs = mutableMapOf() private val userService by inject() var state by mutableStateOf(AccountManagerViewState()) private set fun dispatch(action: AccountManagerViewAction) { when (action) { AccountManagerViewAction.Load -> load() } } private fun load() { jobs.replace("load") { it?.cancel() viewModelScope.launch(Dispatchers.IO) { while (true) { val auths = userService.getAccountAuthenticators(userId) val accounts = userService.getAccounts(userId) state = state.copy( authenticators = auths.map { auth -> AccountManagerViewState.Authenticator( auth = auth, accounts = accounts.filter { auth.type == it.type } ) }.filter { it.accounts.isNotEmpty() } ) delay(3000) } } } } } ================================================ FILE: app/src/main/java/com/rosan/accounts/ui/page/account_manager/AccountManagerViewState.kt ================================================ package com.rosan.accounts.ui.page.account_manager import com.rosan.accounts.data.service.entity.AccountAuthenticatorEntity import com.rosan.accounts.data.service.entity.AccountEntity data class AccountManagerViewState( val authenticators: List = emptyList() ) { data class Authenticator( val auth: AccountAuthenticatorEntity, val accounts: List ) } ================================================ FILE: app/src/main/java/com/rosan/accounts/ui/page/main/MainPage.kt ================================================ package com.rosan.accounts.ui.page.main import androidx.compose.animation.AnimatedContentScope import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.runtime.Composable import androidx.navigation.NavType import androidx.navigation.navArgument import com.google.accompanist.navigation.animation.AnimatedNavHost import com.google.accompanist.navigation.animation.composable import com.google.accompanist.navigation.animation.rememberAnimatedNavController import com.rosan.accounts.ui.page.account_manager.AccountManagerPage import com.rosan.accounts.ui.page.user_manager.UserManagerPage @OptIn(ExperimentalAnimationApi::class) @Composable fun MainPage() { val navController = rememberAnimatedNavController() AnimatedNavHost( navController = navController, startDestination = MainScreen.UserManager.route ) { composable(route = MainScreen.UserManager.route) { UserManagerPage( navController = navController ) } composable( route = MainScreen.AccountManager.route, arguments = listOf( navArgument("id") { type = NavType.IntType } ), enterTransition = { slideIntoContainer( AnimatedContentScope.SlideDirection.Up, ) }, popExitTransition = { slideOutOfContainer( AnimatedContentScope.SlideDirection.Down, ) } ) { val userId = it.arguments?.getInt("id") if (userId == null) { navController.navigateUp() return@composable } AccountManagerPage( userId = userId, navController = navController ) } } } ================================================ FILE: app/src/main/java/com/rosan/accounts/ui/page/main/MainScreen.kt ================================================ package com.rosan.accounts.ui.page.main sealed class MainScreen(val route: String) { object UserManager : MainScreen("user") object AccountManager : MainScreen("user/{id}/account") { fun builder(id: Int): String = "user/$id/account" } } ================================================ FILE: app/src/main/java/com/rosan/accounts/ui/page/user_manager/UserManagerPage.kt ================================================ package com.rosan.accounts.ui.page.user_manager import android.os.Process import androidx.compose.animation.AnimatedContent import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.spring import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.displayCutout import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.twotone.Warning import androidx.compose.material3.AlertDialog import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedCard import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.navigation.NavController import com.rosan.accounts.R import com.rosan.accounts.data.common.utils.help import com.rosan.accounts.data.common.utils.id import com.rosan.accounts.data.service.entity.UserEntity import com.rosan.accounts.ui.page.main.MainScreen import org.koin.androidx.compose.getViewModel @OptIn( ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class, ExperimentalAnimationApi::class ) @Composable fun UserManagerPage( navController: NavController, viewModel: UserManagerViewModel = getViewModel() ) { SideEffect { viewModel.dispatch(UserManagerViewAction.Load) } Scaffold( modifier = Modifier .fillMaxSize() .windowInsetsPadding(WindowInsets.displayCutout), topBar = { TopAppBar( title = { Text(stringResource(R.string.user_manager)) } ) }, ) { AnimatedContent( viewModel.state.cause != null, modifier = Modifier .fillMaxSize() .padding(it) ) { if (it) Box( modifier = Modifier.fillMaxSize() ) { Text( viewModel.state.cause.let { it?.help() ?: it?.localizedMessage ?: it?.toString() ?: "" }, modifier = Modifier.align(Alignment.Center) ) } else LazyColumn( modifier = Modifier .fillMaxSize(), contentPadding = PaddingValues(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp) ) { items(viewModel.state.users, key = { it.id }) { var alpha by remember { mutableStateOf(0f) } ItemWidget( modifier = Modifier .fillMaxWidth() .animateItemPlacement() .graphicsLayer( alpha = animateFloatAsState( targetValue = alpha, animationSpec = spring(stiffness = 100f) ).value ), viewModel = viewModel, navController = navController, user = it ) SideEffect { alpha = 1f } } } } } } @Composable private fun ItemWidget( modifier: Modifier = Modifier, viewModel: UserManagerViewModel, navController: NavController, user: UserEntity ) { OutlinedCard( modifier = modifier ) { Column( modifier = Modifier.padding(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { Row( modifier = Modifier .padding(horizontal = 16.dp) .padding(top = 8.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp) ) { Text( user.id.toString(), color = MaterialTheme.colorScheme.primary, style = MaterialTheme.typography.titleMedium ) Text( user.name ?: stringResource(R.string.user_name_default), style = MaterialTheme.typography.titleMedium ) } Row( modifier = Modifier .padding(horizontal = 8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically ) { Spacer(modifier = Modifier.weight(1f)) TextButton(onClick = { navController.navigate(MainScreen.AccountManager.builder(user.id)) }) { Text(stringResource(R.string.account_manager)) } val curUserId = Process.myUserHandle().id if (curUserId != user.id) { var showing by remember { mutableStateOf(false) } TextButton(onClick = { showing = true }) { Text(stringResource(R.string.remove)) } DeleteUserDialog( viewModel = viewModel, showing = showing, onDismissRequest = { showing = false }, user = user ) } } } } } @Composable private fun DeleteUserDialog( viewModel: UserManagerViewModel, showing: Boolean, onDismissRequest: () -> Unit, user: UserEntity ) { if (!showing) return AlertDialog(onDismissRequest = onDismissRequest, icon = { Icon(imageVector = Icons.TwoTone.Warning, contentDescription = null) }, title = { Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { Text( user.id.toString(), color = MaterialTheme.colorScheme.primary ) Text(user.name ?: stringResource(R.string.user_name_default)) } }, text = { Text(stringResource(R.string.delete_user_warning)) }, confirmButton = { TextButton(onClick = { viewModel.dispatch(UserManagerViewAction.Remove(user)) onDismissRequest() }) { Text(stringResource(R.string.delete_user_confirm)) } }, dismissButton = { TextButton(onClick = onDismissRequest) { Text(stringResource(R.string.delete_user_cancel)) } }) } ================================================ FILE: app/src/main/java/com/rosan/accounts/ui/page/user_manager/UserManagerViewAction.kt ================================================ package com.rosan.accounts.ui.page.user_manager import com.rosan.accounts.data.service.entity.UserEntity sealed class UserManagerViewAction { object Load : UserManagerViewAction() data class Remove(val user: UserEntity) : UserManagerViewAction() } ================================================ FILE: app/src/main/java/com/rosan/accounts/ui/page/user_manager/UserManagerViewModel.kt ================================================ package com.rosan.accounts.ui.page.user_manager import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.rosan.accounts.data.common.utils.replace import com.rosan.accounts.data.service.entity.UserEntity import com.rosan.accounts.data.service.repo.UserService import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent import org.koin.core.component.inject class UserManagerViewModel : ViewModel(), KoinComponent { private val jobs = mutableMapOf() private val userService by inject() var state by mutableStateOf(UserManagerViewState()) private set fun dispatch(action: UserManagerViewAction) { when (action) { UserManagerViewAction.Load -> load() is UserManagerViewAction.Remove -> remove(action.user) } } private fun load() { jobs.replace("load") { it?.cancel() viewModelScope.launch(Dispatchers.IO) { while (true) { kotlin.runCatching { userService.getUsers().sortedBy { it.id } }.onFailure { it.printStackTrace() state = state.copy(cause = it) }.onSuccess { state = state.copy(users = it, cause = null) } delay(1500) } } } } private fun remove(user: UserEntity) { viewModelScope.launch { userService.removeUser(user) } } } ================================================ FILE: app/src/main/java/com/rosan/accounts/ui/page/user_manager/UserManagerViewState.kt ================================================ package com.rosan.accounts.ui.page.user_manager import com.rosan.accounts.data.service.entity.UserEntity data class UserManagerViewState( val users: List = emptyList(), val cause: Throwable? = null ) ================================================ FILE: app/src/main/java/com/rosan/accounts/ui/theme/Color.kt ================================================ package com.rosan.accounts.ui.theme import androidx.compose.ui.graphics.Color val Purple80 = Color(0xFFD0BCFF) val PurpleGrey80 = Color(0xFFCCC2DC) val Pink80 = Color(0xFFEFB8C8) val Purple40 = Color(0xFF6650a4) val PurpleGrey40 = Color(0xFF625b71) val Pink40 = Color(0xFF7D5260) ================================================ FILE: app/src/main/java/com/rosan/accounts/ui/theme/Theme.kt ================================================ package com.rosan.accounts.ui.theme import android.app.Activity import android.os.Build import android.view.WindowManager import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView import androidx.core.view.WindowCompat private val DarkColorScheme = darkColorScheme( primary = Purple80, secondary = PurpleGrey80, tertiary = Pink80 ) private val LightColorScheme = lightColorScheme( primary = Purple40, secondary = PurpleGrey40, tertiary = Pink40 /* Other default colors to override background = Color(0xFFFFFBFE), surface = Color(0xFFFFFBFE), onPrimary = Color.White, onSecondary = Color.White, onTertiary = Color.White, onBackground = Color(0xFF1C1B1F), onSurface = Color(0xFF1C1B1F), */ ) @Composable fun AccountsTheme( darkTheme: Boolean = isSystemInDarkTheme(), // Dynamic color is available on Android 12+ dynamicColor: Boolean = true, content: @Composable () -> Unit ) { val colorScheme = when { dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { val context = LocalContext.current if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) } darkTheme -> DarkColorScheme else -> LightColorScheme } val view = LocalView.current SideEffect { val window = (view.context as Activity).window if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES WindowCompat.setDecorFitsSystemWindows(window, false) window.statusBarColor = Color.Transparent.toArgb() WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme window.navigationBarColor = Color.Transparent.toArgb() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) window.navigationBarDividerColor = Color.Transparent.toArgb() WindowCompat.getInsetsController(window, view).isAppearanceLightNavigationBars = !darkTheme } MaterialTheme( colorScheme = colorScheme, typography = Typography, content = content ) } ================================================ FILE: app/src/main/java/com/rosan/accounts/ui/theme/Type.kt ================================================ package com.rosan.accounts.ui.theme import androidx.compose.material3.Typography import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp // Set of Material typography styles to start with val Typography = Typography( bodyLarge = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, fontSize = 16.sp, lineHeight = 24.sp, letterSpacing = 0.5.sp ) /* Other default text styles to override titleLarge = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, fontSize = 22.sp, lineHeight = 28.sp, letterSpacing = 0.sp ), labelSmall = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Medium, fontSize = 11.sp, lineHeight = 16.sp, letterSpacing = 0.5.sp ) */ ) ================================================ FILE: app/src/main/java/com/rosan/accounts/ui/widget/PositionDialog.kt ================================================ package com.rosan.accounts.ui.widget import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.sizeIn import androidx.compose.material3.AlertDialogDefaults import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties @Composable fun PositionDialog( properties: DialogProperties = DialogProperties(), onDismissRequest: () -> Unit, modifier: Modifier = Modifier, shape: Shape = AlertDialogDefaults.shape, containerColor: Color = AlertDialogDefaults.containerColor, iconContentColor: Color = AlertDialogDefaults.iconContentColor, titleContentColor: Color = AlertDialogDefaults.titleContentColor, textContentColor: Color = AlertDialogDefaults.textContentColor, tonalElevation: Dp = AlertDialogDefaults.TonalElevation, leftIcon: @Composable (() -> Unit)? = null, centerIcon: @Composable (() -> Unit)? = null, rightIcon: @Composable (() -> Unit)? = null, leftTitle: @Composable (() -> Unit)? = null, centerTitle: @Composable (() -> Unit)? = null, rightTitle: @Composable (() -> Unit)? = null, leftSubtitle: @Composable (() -> Unit)? = null, centerSubtitle: @Composable (() -> Unit)? = null, rightSubtitle: @Composable (() -> Unit)? = null, leftText: @Composable (() -> Unit)? = null, centerText: @Composable (() -> Unit)? = null, rightText: @Composable (() -> Unit)? = null, leftContent: @Composable (() -> Unit)? = null, centerContent: @Composable (() -> Unit)? = null, rightContent: @Composable (() -> Unit)? = null, leftButton: @Composable (() -> Unit)? = null, centerButton: @Composable (() -> Unit)? = null, rightButton: @Composable (() -> Unit)? = null ) { Dialog(onDismissRequest = onDismissRequest, properties = properties) { Box(modifier = Modifier .fillMaxSize() .pointerInput(null) { detectTapGestures(onTap = { onDismissRequest() }) }) { Box(modifier = Modifier .align(Alignment.Center) .pointerInput(null) { detectTapGestures(onTap = {}) }) { Surface( modifier = modifier, shape = shape, color = containerColor, tonalElevation = tonalElevation ) { Box( modifier = Modifier .sizeIn(minWidth = MinWidth, maxHeight = MaxWidth) .padding(DialogPadding) ) { // set the button always in bottom var buttonHeightPx by remember { mutableStateOf(0) } val buttonHeight = (buttonHeightPx / LocalDensity.current.density).dp Box(modifier = Modifier .align(Alignment.BottomCenter) .onSizeChanged { buttonHeightPx = it.height }) { PositionChildWidget( leftButton, centerButton, rightButton ) { button -> CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.primary) { val textStyle = MaterialTheme.typography.labelLarge ProvideTextStyle(value = textStyle) { Box( modifier = Modifier.padding(ButtonPadding) ) { button?.invoke() } } } } } Column( modifier = Modifier.padding(bottom = animateDpAsState(targetValue = buttonHeight).value) ) { PositionChildWidget( leftIcon, centerIcon, rightIcon ) { icon -> CompositionLocalProvider(LocalContentColor provides iconContentColor) { Box( modifier = Modifier .padding(IconPadding) .align(Alignment.CenterHorizontally) ) { icon?.invoke() } } } PositionChildWidget( leftTitle, centerTitle, rightTitle ) { title -> CompositionLocalProvider(LocalContentColor provides titleContentColor) { ProvideTextStyle(MaterialTheme.typography.headlineSmall) { Box( modifier = Modifier .padding(TitlePadding) .align(Alignment.CenterHorizontally) ) { title?.invoke() } } } } PositionChildWidget( leftSubtitle, centerSubtitle, rightSubtitle ) { subtitle -> CompositionLocalProvider(LocalContentColor provides titleContentColor) { val textStyle = MaterialTheme.typography.bodyMedium ProvideTextStyle(textStyle) { Box( modifier = Modifier .padding(SubtitlePadding) .align(Alignment.CenterHorizontally) ) { subtitle?.invoke() } } } } val contentMode = leftContent != null || centerContent != null || rightContent != null PositionChildWidget( if (contentMode) leftContent else leftText, if (contentMode) centerContent else centerText, if (contentMode) rightContent else rightText ) { text -> CompositionLocalProvider(LocalContentColor provides textContentColor) { val textStyle = MaterialTheme.typography.bodyMedium ProvideTextStyle(textStyle) { Box( modifier = Modifier .weight(weight = 1f, fill = false) .padding(if (contentMode) ContentPadding else TextPadding) ) { text?.invoke() } } } } } } } } } } } @Composable private fun PositionChildWidget( left: @Composable (() -> Unit)? = null, center: @Composable (() -> Unit)? = null, right: @Composable (() -> Unit)? = null, parent: @Composable ((child: @Composable (() -> Unit)?) -> Unit) ) { if (left == null && center == null && right == null) return Box(modifier = Modifier.fillMaxWidth()) { Box(modifier = Modifier.align(Alignment.TopStart)) { parent.invoke(left) } Box(modifier = Modifier.align(Alignment.TopCenter)) { parent.invoke(center) } Box(modifier = Modifier.align(Alignment.TopEnd)) { parent.invoke(right) } } } private val ButtonsMainAxisSpacing = 8.dp private val ButtonsCrossAxisSpacing = 12.dp private val DialogSinglePadding = 24.dp private val DialogPadding = PaddingValues(top = DialogSinglePadding, bottom = DialogSinglePadding) private val IconPadding = PaddingValues.Absolute(left = DialogSinglePadding, right = DialogSinglePadding, bottom = 12.dp) private val TitlePadding = PaddingValues.Absolute(left = DialogSinglePadding, right = DialogSinglePadding, bottom = 12.dp) private val SubtitlePadding = PaddingValues.Absolute(left = DialogSinglePadding, right = DialogSinglePadding, bottom = 12.dp) private val TextPadding = PaddingValues.Absolute(left = DialogSinglePadding, right = DialogSinglePadding, bottom = 12.dp) private val ContentPadding = PaddingValues.Absolute(bottom = 12.dp) private val ButtonPadding = PaddingValues(horizontal = DialogSinglePadding) private val MinWidth = 280.dp private val MaxWidth = 560.dp ================================================ FILE: app/src/main/res/values/colors.xml ================================================ #FFBB86FC #FF6200EE #FF3700B3 #FF03DAC5 #FF018786 #FF000000 #FFFFFFFF ================================================ FILE: app/src/main/res/values/strings.xml ================================================ Accounts User Manager Untitled Are you sure you want to delete this user space?\nAll applications and data in this user space will be lost!!! Yes, I\'m sure! No Remove Account Manager No account exists copied, import it in Hail! Please activate Shizuku and agree to grant permission. ================================================ FILE: app/src/main/res/values/themes.xml ================================================ ================================================ FILE: app/src/main/res/values-zh-rCN/strings.xml ================================================ 用户管理 未命名 确定要删除此用户吗?\n所有处于此用户空间的应用和数据都会消失!!! 是的,我确定! 取消 移除 账号管理 没有账户存在 复制成功,请在【雹】中导入! 请激活Shizuku并同意权限请求! ================================================ FILE: app/src/test/java/com/rosan/accounts/ExampleUnitTest.kt ================================================ package com.rosan.accounts import org.junit.Test import org.junit.Assert.* /** * Example local unit test, which will execute on the development machine (host). * * See [testing documentation](http://d.android.com/tools/testing). */ class ExampleUnitTest { @Test fun addition_isCorrect() { assertEquals(4, 2 + 2) } } ================================================ FILE: build.gradle ================================================ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { id 'com.android.application' version '8.0.2' apply false id 'com.android.library' version '8.0.2' apply false id 'org.jetbrains.kotlin.android' version '1.8.20' apply false } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ #Sat Jun 17 18:49:09 CST 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: gradle.properties ================================================ # Project-wide Gradle settings. # IDE (e.g. Android Studio) users: # Gradle settings configured through the IDE *will override* # any settings specified in this file. # For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true # AndroidX package structure to make it clearer which packages are bundled with the # Android operating system, and which are packaged with your app's APK # https://developer.android.com/topic/libraries/support-library/androidx-rn android.useAndroidX=true # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official # Enables namespacing of each library's R class so that its R class includes only the # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library android.nonTransitiveRClass=true ================================================ FILE: gradlew ================================================ #!/usr/bin/env sh # # Copyright 2015 the original author or authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn () { echo "$*" } die () { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin or MSYS, switch paths to Windows format before running java if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=`expr $i + 1` done case $i in 0) set -- ;; 1) set -- "$args0" ;; 2) set -- "$args0" "$args1" ;; 3) set -- "$args0" "$args1" "$args2" ;; 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Escape application args save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } APP_ARGS=`save "$@"` # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: hidden-api/.gitignore ================================================ /build ================================================ FILE: hidden-api/build.gradle ================================================ plugins { id("com.android.library") } android { namespace "com.rosan.hidden_api" compileSdk 33 defaultConfig { minSdk 19 } } dependencies { def annotation = "1.6.0" compileOnly "androidx.annotation:annotation:$annotation" } ================================================ FILE: hidden-api/src/main/AndroidManifest.xml ================================================ ================================================ FILE: hidden-api/src/main/java/android/accounts/IAccountManager.java ================================================ package android.accounts; import android.os.Binder; import android.os.Build; import android.os.IBinder; import android.os.IInterface; import androidx.annotation.DeprecatedSinceApi; import androidx.annotation.RequiresApi; public interface IAccountManager extends IInterface { AuthenticatorDescription[] getAuthenticatorTypes(int userId); @RequiresApi(Build.VERSION_CODES.M) Account[] getAccountsAsUser(String accountType, int userId, String opPackageName); @DeprecatedSinceApi(api = Build.VERSION_CODES.M) Account[] getAccountsAsUser(String accountType, int userId); abstract class Stub extends Binder implements IAccountManager { public static IAccountManager asInterface(IBinder obj) { throw new UnsupportedOperationException(); } } } ================================================ FILE: hidden-api/src/main/java/android/content/pm/IPackageManager.java ================================================ package android.content.pm; import android.os.Binder; import android.os.Build; import android.os.IBinder; import android.os.IInterface; import androidx.annotation.DeprecatedSinceApi; import androidx.annotation.RequiresApi; public interface IPackageManager extends IInterface { @RequiresApi(Build.VERSION_CODES.TIRAMISU) PackageInfo getPackageInfo(String packageName, long flags, int userId); @DeprecatedSinceApi(api = Build.VERSION_CODES.TIRAMISU) PackageInfo getPackageInfo(String packageName, int flags, int userId); abstract class Stub extends Binder implements IPackageManager { public static IPackageManager asInterface(IBinder obj) { throw new UnsupportedOperationException(); } } } ================================================ FILE: hidden-api/src/main/java/android/content/pm/UserInfo.java ================================================ package android.content.pm; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; public class UserInfo implements Parcelable { public int id; public @Nullable String name; protected UserInfo(Parcel in) { } public static final Creator CREATOR = new Creator() { @Override public UserInfo createFromParcel(Parcel in) { return new UserInfo(in); } @Override public UserInfo[] newArray(int size) { return new UserInfo[size]; } }; @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { } } ================================================ FILE: hidden-api/src/main/java/android/os/IUserManager.java ================================================ package android.os; import android.content.pm.UserInfo; import androidx.annotation.DeprecatedSinceApi; import androidx.annotation.RequiresApi; import java.util.List; public interface IUserManager extends IInterface { @RequiresApi(Build.VERSION_CODES.R) List getUsers(boolean excludePartial, boolean excludeDying, boolean excludePreCreated); @DeprecatedSinceApi(api = Build.VERSION_CODES.R) List getUsers(boolean excludeDying); boolean removeUser(int userHandle); abstract class Stub extends Binder implements IUserManager { public static IUserManager asInterface(IBinder obj) { throw new UnsupportedOperationException(); } } } ================================================ FILE: hidden-api/src/main/java/android/os/ServiceManager.java ================================================ /* * Copyright (C) 2007 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.os; import java.util.Map; /** * @hide */ public final class ServiceManager { public static IBinder getService(String name) { throw new RuntimeException("STUB"); } /** * Returns a reference to a service with the given name, or throws * {@link NullPointerException} if none is found. * * @hide */ public static IBinder getServiceOrThrow(String name) throws ServiceNotFoundException { throw new RuntimeException("STUB"); } /** * Place a new @a service called @a name into the service * manager. * * @param name the name of the new service * @param service the service object */ public static void addService(String name, IBinder service) { throw new RuntimeException("STUB"); } /** * Place a new @a service called @a name into the service * manager. * * @param name the name of the new service * @param service the service object * @param allowIsolated set to true to allow isolated sandboxed processes * to access this service */ public static void addService(String name, IBinder service, boolean allowIsolated) { throw new RuntimeException("STUB"); } /** * Retrieve an existing service called @a name from the * service manager. Non-blocking. */ public static IBinder checkService(String name) { throw new RuntimeException("STUB"); } /** * Return a list of all currently running services. * * @return an array of all currently running services, or null in * case of an exception */ public static String[] listServices() { throw new RuntimeException("STUB"); } /** * This is only intended to be called when the process is first being brought * up and bound by the activity manager. There is only one thread in the process * at that time, so no locking is done. * * @param cache the cache of service references * @hide */ public static void initServiceCache(Map cache) { throw new RuntimeException("STUB"); } /** * Exception thrown when no service published for given name. This might be * thrown early during boot before certain services have published * themselves. * * @hide */ public static class ServiceNotFoundException extends Exception { public ServiceNotFoundException(String name) { super("No service published for: " + name); } } } ================================================ FILE: settings.gradle ================================================ pluginManagement { repositories { mavenLocal() google() mavenCentral() gradlePluginPortal() } } dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { mavenLocal() google() mavenCentral() } } rootProject.name = "Accounts" include ':app' include ':hidden-api'