diff --git a/app/build.gradle b/app/build.gradle
index 1c7618a..84d04bc 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -17,8 +17,8 @@ android {
applicationId "com.rosan.accounts"
minSdk 21
targetSdk 33
- versionCode 5
- versionName "1.4"
+ versionCode 6
+ versionName "1.5"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -74,7 +74,7 @@ android {
compose true
}
composeOptions {
- kotlinCompilerExtensionVersion '1.3.2'
+ kotlinCompilerExtensionVersion '1.4.5'
}
packagingOptions {
resources {
@@ -103,10 +103,22 @@ dependencies {
debugImplementation 'androidx.compose.ui:ui-tooling'
debugImplementation 'androidx.compose.ui:ui-test-manifest'
- implementation project(':hidden-api')
+ 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"
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 97612e6..c1a5d1b 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -12,6 +12,7 @@
diff --git a/app/src/main/java/com/rosan/accounts/AccountInfo.kt b/app/src/main/java/com/rosan/accounts/AccountInfo.kt
deleted file mode 100644
index f75c20a..0000000
--- a/app/src/main/java/com/rosan/accounts/AccountInfo.kt
+++ /dev/null
@@ -1,3 +0,0 @@
-package com.rosan.accounts
-
-data class AccountInfo(val userId: Int, val type: String, val name: String)
diff --git a/app/src/main/java/com/rosan/accounts/AccountType.kt b/app/src/main/java/com/rosan/accounts/AccountType.kt
deleted file mode 100644
index 1cad241..0000000
--- a/app/src/main/java/com/rosan/accounts/AccountType.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.rosan.accounts
-
-import android.graphics.drawable.ColorDrawable
-import android.graphics.drawable.Drawable
-
-data class AccountType(
- val userId: Int = 0,
- val packageName: String,
- val type: String = packageName,
- val label: String = packageName,
- val icon: Drawable = ColorDrawable(0x00000000),
- val values: List = emptyList()
-)
diff --git a/app/src/main/java/com/rosan/accounts/App.kt b/app/src/main/java/com/rosan/accounts/App.kt
new file mode 100644
index 0000000..88629c9
--- /dev/null
+++ b/app/src/main/java/com/rosan/accounts/App.kt
@@ -0,0 +1,22 @@
+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)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/rosan/accounts/MainActivity.kt b/app/src/main/java/com/rosan/accounts/MainActivity.kt
deleted file mode 100644
index 826aeb5..0000000
--- a/app/src/main/java/com/rosan/accounts/MainActivity.kt
+++ /dev/null
@@ -1,357 +0,0 @@
-package com.rosan.accounts
-
-import android.annotation.SuppressLint
-import android.os.Bundle
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.setContent
-import androidx.compose.animation.AnimatedContent
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.ExperimentalAnimationApi
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.clickable
-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.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
-import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
-import androidx.compose.foundation.lazy.staggeredgrid.items
-import androidx.compose.material.ExperimentalMaterialApi
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.twotone.Close
-import androidx.compose.material.icons.twotone.ContentCopy
-import androidx.compose.material.icons.twotone.Delete
-import androidx.compose.material.icons.twotone.Face
-import androidx.compose.material.pullrefresh.PullRefreshIndicator
-import androidx.compose.material.pullrefresh.pullRefresh
-import androidx.compose.material.pullrefresh.rememberPullRefreshState
-import androidx.compose.material3.AlertDialog
-import androidx.compose.material3.ElevatedCard
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.NavigationBar
-import androidx.compose.material3.NavigationBarItem
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Surface
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
-import androidx.compose.material3.TopAppBar
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.MutableState
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateMapOf
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.unit.dp
-import androidx.lifecycle.lifecycleScope
-import com.google.accompanist.drawablepainter.rememberDrawablePainter
-import com.rosan.accounts.data.common.utils.contentCopy
-import com.rosan.accounts.data.common.utils.requireShizukuPermissionGranted
-import com.rosan.accounts.data.common.utils.toast
-import com.rosan.accounts.ui.theme.AccountsTheme
-import kotlinx.coroutines.CancellationException
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import org.json.JSONArray
-
-class MainActivity : ComponentActivity() {
- private val context = this
-
- private var jobOrNull: Job? = null
-
- private val refreshState = mutableStateOf(true)
-
- private val users = mutableStateMapOf>()
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContent()
- }
-
- override fun onResume() {
- super.onResume()
- refresh()
- }
-
- private fun refresh() {
- refreshState.value = true
- shizukuJob {
- // Wait for the system cache be refreshed
- delay(1500)
- getUsers()
- }
- }
-
- @OptIn(
- ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class
- )
- private fun setContent() {
- setContent {
- AccountsTheme {
- Surface(
- modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background
- ) {
- val refreshing by refreshState
- val pullRefreshState = rememberPullRefreshState(
- refreshing = refreshing, onRefresh = ::refresh
- )
-
- var currentIndex by rememberSaveable {
- mutableStateOf(0)
- }
- val userKeys = (if (users.isNotEmpty()) users.keys else listOf(
- UserInfo(
- 0,
- "Default",
- 0
- )
- )).sortedBy { it.id }
- if (currentIndex >= userKeys.size)
- currentIndex = 0
- val currentUser = userKeys[currentIndex]
- val accountTypes = users[currentUser] ?: emptyList()
-
- Scaffold(topBar = {
- TopAppBar(navigationIcon = {
- IconButton(onClick = { this.finish() }) {
- Icon(
- imageVector = Icons.TwoTone.Close, contentDescription = null
- )
- }
- }, title = {
- Text(stringResource(id = R.string.app_name))
- }, actions = {
- AnimatedVisibility(currentUser.id != 0) {
- val show = remember {
- mutableStateOf(false)
- }
- IconButton(onClick = {
- show.value = true
- }) {
- Icon(
- imageVector = Icons.TwoTone.Delete,
- contentDescription = "delete user"
- )
- }
- if (!show.value) return@AnimatedVisibility
- DeleteDialog(show, currentUser)
- }
- IconButton(onClick = {
- val json = JSONArray()
- accountTypes.map { it.packageName }
- .distinct()
- .forEach {
- json.put(it)
- }
- context.contentCopy(json.toString())
- context.toast("copied, import it in Hail!")
- }) {
- Icon(
- imageVector = Icons.TwoTone.ContentCopy,
- contentDescription = "copy"
- )
- }
- })
- }) {
- Box(
- modifier = Modifier
- .padding(it)
- .pullRefresh(pullRefreshState)
- ) {
- UsersPage(
- currentUser = currentUser,
- users = userKeys,
- accountTypes = accountTypes,
- onSelected = {
- currentIndex = userKeys.indexOf(it)
- }
- )
-
- PullRefreshIndicator(
- modifier = Modifier.align(Alignment.TopCenter),
- refreshing = refreshing,
- state = pullRefreshState
- )
- }
- }
- }
- }
- }
- }
-
- @OptIn(ExperimentalFoundationApi::class, ExperimentalAnimationApi::class)
- @Composable
- private fun UsersPage(
- currentUser: UserInfo,
- users: List,
- accountTypes: List,
- onSelected: (UserInfo) -> Unit
- ) {
- Column(modifier = Modifier.fillMaxSize()) {
- AnimatedContent(
- accountTypes, modifier = Modifier
- .weight(1f)
- .fillMaxSize()
- ) { accountTypes ->
- if (accountTypes.isEmpty()) Box(
- modifier = Modifier.fillMaxSize()
- ) {
- Text(
- text = "No Application For Account",
- modifier = Modifier.align(Alignment.Center),
- style = MaterialTheme.typography.titleMedium
- )
- } else LazyVerticalStaggeredGrid(
- modifier = Modifier.fillMaxSize(),
- columns = if (accountTypes.isEmpty()) StaggeredGridCells.Fixed(1) else StaggeredGridCells.Adaptive(
- 200.dp
- ),
- contentPadding = PaddingValues(16.dp),
- verticalItemSpacing = 16.dp,
- horizontalArrangement = Arrangement.spacedBy(16.dp)
- ) {
- items(accountTypes, key = {
- "${it.userId}:${it.type}"
- }) {
- ItemWidget(it)
- }
- }
- }
- AnimatedContent(users) { users ->
- NavigationBar(
- modifier = Modifier
- .fillMaxWidth()
- ) {
- users.forEach { user ->
- NavigationBarItem(
- selected = currentUser.id == user.id,
- onClick = { onSelected(user) },
- icon = {
- Icon(
- imageVector = Icons.TwoTone.Face,
- contentDescription = null
- )
- },
- label = {
- Text("${user.name} (${user.id})")
- },
- alwaysShowLabel = false
- )
- }
- }
- }
- }
- }
-
- @SuppressLint("InlinedApi")
- @Composable
- private fun ItemWidget(accountType: AccountType) {
- ElevatedCard {
- var showAccounts by remember {
- mutableStateOf(false)
- }
- Row(modifier = Modifier
- .clickable {
- showAccounts = !showAccounts
- }
- .padding(horizontal = 24.dp, vertical = 16.dp),
- horizontalArrangement = Arrangement.spacedBy(24.dp)) {
- Image(
- modifier = Modifier
- .size(32.dp)
- .align(Alignment.CenterVertically),
- painter = rememberDrawablePainter(accountType.icon),
- contentDescription = null
- )
- Column(modifier = Modifier.weight(1f)) {
- @Composable
- fun MyText(
- text: String, style: TextStyle = MaterialTheme.typography.bodyMedium
- ) {
- Text(text, style = style)
- }
- MyText(accountType.label, style = MaterialTheme.typography.titleMedium)
- MyText("userId: ${accountType.userId}")
- MyText("package: ${accountType.packageName}")
- MyText("type: ${accountType.type}")
- AnimatedVisibility(visible = showAccounts && accountType.values.isNotEmpty()) {
- MyText("accounts: ${accountType.values.joinToString()}")
- }
- }
- }
- }
- }
-
- @Composable
- private fun DeleteDialog(show: MutableState, user: UserInfo) {
- AlertDialog(onDismissRequest = { show.value = false }, title = {
- Text("${user.name} (${user.id})")
- }, text = {
- Text(
- """Are you sure you want to delete this user space?
-All applications and data in this user space will be lost"""
- )
- }, confirmButton = {
- TextButton(onClick = {
- shizukuJob {
- show.value = false
- removeUser(user.id)
- refresh()
- }
- }) {
- Text("Sure")
- }
- }, dismissButton = {
- TextButton(onClick = { show.value = false }) {
- Text("Cancel")
- }
- })
- }
-
- private fun shizukuJob(action: suspend () -> Unit) {
- jobOrNull?.cancel()
- jobOrNull = lifecycleScope.launch(Dispatchers.IO) {
- val result = kotlin.runCatching {
- requireShizukuPermissionGranted(context) {
- action.invoke()
- }
- }
- withContext(Dispatchers.Main) {
- result.onFailure {
- if (it is CancellationException) return@onFailure
- it.printStackTrace()
- this@MainActivity.toast("$it ${it.localizedMessage}")
- }
- refreshState.value = false
- }
- }
- }
-
- private suspend fun getUsers() {
- val result = UserService.getUsers(this@MainActivity)
- withContext(Dispatchers.Main) {
- users.clear()
- users.putAll(result)
- refreshState.value = false
- }
- }
-
- private suspend fun removeUser(userId: Int): Boolean = UserService.removeUser(userId)
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/rosan/accounts/UserInfo.kt b/app/src/main/java/com/rosan/accounts/UserInfo.kt
deleted file mode 100644
index ff1aa89..0000000
--- a/app/src/main/java/com/rosan/accounts/UserInfo.kt
+++ /dev/null
@@ -1,3 +0,0 @@
-package com.rosan.accounts
-
-data class UserInfo(val id: Int, val name: String, val numberOfAccounts: Int)
\ No newline at end of file
diff --git a/app/src/main/java/com/rosan/accounts/UserService.kt b/app/src/main/java/com/rosan/accounts/UserService.kt
deleted file mode 100644
index 5b71219..0000000
--- a/app/src/main/java/com/rosan/accounts/UserService.kt
+++ /dev/null
@@ -1,153 +0,0 @@
-package com.rosan.accounts
-
-import android.accounts.IAccountManager
-import android.content.Context
-import android.content.pm.IPackageManager
-import android.os.Binder
-import android.os.Build
-import android.os.IBinder
-import android.os.IUserManager
-import android.os.Parcel
-import android.os.ParcelFileDescriptor
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-import rikka.shizuku.ShizukuBinderWrapper
-import rikka.shizuku.SystemServiceHelper
-import java.io.FileDescriptor
-
-object UserService {
- suspend fun removeUser(userId: Int): Boolean {
- return IUserManager.Stub.asInterface(shizukuBinder(Context.USER_SERVICE))
- .removeUser(userId)
- }
-
- suspend fun getUsers(context: Context): Map> {
- val basePackageManager = context.packageManager
- val accountManager =
- IAccountManager.Stub.asInterface(shizukuBinder(Context.ACCOUNT_SERVICE))
- val packageManager = IPackageManager.Stub.asInterface(shizukuBinder("package"))
- val users = mutableMapOf>()
- getAccounts().forEach { (user, accounts) ->
- val userId = user.id
- val types = accountManager.getAuthenticatorTypes(userId).map {
- val type = it.type
- val values = accounts.filter { it.type == type }.map { it.name }
- if (values.isEmpty()) return@map null
-
- val packageName = it.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
- var label = basePackageManager.getApplicationLabel(applicationInfo).toString()
- val accountName =
- basePackageManager.getText(packageName, it.labelId, applicationInfo)
-
- if (accountName != null && label != accountName)
- label += " - $accountName"
-
- val icon = basePackageManager.getApplicationIcon(applicationInfo)
- AccountType(
- userId = userId,
- packageName = it.packageName,
- type = type,
- label = label,
- icon = icon,
- values = values
- )
- }
- users[user] = types.filterNotNull()
- }
- return users
- }
-
- private suspend fun getAccounts(): Map> {
- val text = dumpsysAccount()
-
- val result = mutableMapOf>()
-
- val users = "User UserInfo\\{(\\d+):(.*):.*\\}".toRegex().findAll(text).toList().map {
- val userId = it.groupValues[1].toInt()
- val name = it.groupValues[2]
- UserInfo(id = userId, name = name, numberOfAccounts = 0).apply {
- result[this] = listOf()
- }
- }.let { users ->
- "Accounts: (\\d+)".toRegex().findAll(text).toList().let {
- users.mapIndexed { index, user ->
- val numberOfAccounts = it[index].groupValues[1].toInt()
- user.copy(numberOfAccounts = numberOfAccounts)
- }
- }
- }.toMutableList()
-
- fun readUser(): UserInfo {
- val user = users.first().let { user ->
- if (user.numberOfAccounts > 0) return@let user
- return readUser()
- }.let {
- it.copy(numberOfAccounts = it.numberOfAccounts - 1)
- }
- users[0] = user
- return user
- }
-
- "Account \\{name=(.*), type=(.*)\\}".toRegex().findAll(text).toList().map {
- val name = it.groupValues[1]
- val type = it.groupValues[2]
- val userId = readUser().id
-
- AccountInfo(
- userId = userId,
- type = type,
- name = name
- )
- }.groupBy { it.userId }.map { (userId, accounts) ->
- result[result.keys.first { userId == it.id }] = accounts
- }
-
- return result
- }
-
- private suspend fun dumpsysAccount(): String = withContext(Dispatchers.IO) {
- val binder = SystemServiceHelper.getSystemService(Context.ACCOUNT_SERVICE)
- val pipe = ParcelFileDescriptor.createPipe()
- val readFD = pipe[0]
- val writeFD = pipe[1]
- writeFD.use {
- binderWrapperDump(binder, writeFD.fileDescriptor)
- }
- return@withContext readFD.use {
- ParcelFileDescriptor.AutoCloseInputStream(it)
- .readBytes()
- .decodeToString()
- }
- }
-
- private fun binderWrapperDump(
- binder: IBinder,
- fd: FileDescriptor,
- args: Array? = null
- ) {
- val data = Parcel.obtain()
- val reply = Parcel.obtain()
- try {
- data.writeFileDescriptor(fd)
- data.writeStringArray(args)
- shizukuBinder(binder).transact(Binder.DUMP_TRANSACTION, data, reply, 0)
- reply.readException()
- } finally {
- data.recycle()
- reply.recycle()
- }
- }
-
- private fun shizukuBinder(name: String): IBinder {
- return shizukuBinder(SystemServiceHelper.getSystemService(name))
- }
-
- private fun shizukuBinder(binder: IBinder): IBinder {
- return ShizukuBinderWrapper(binder)
- }
-}
diff --git a/app/src/main/java/com/rosan/accounts/data/common/utils/BinderUtil.kt b/app/src/main/java/com/rosan/accounts/data/common/utils/BinderUtil.kt
new file mode 100644
index 0000000..18c3f1d
--- /dev/null
+++ b/app/src/main/java/com/rosan/accounts/data/common/utils/BinderUtil.kt
@@ -0,0 +1,37 @@
+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()
\ No newline at end of file
diff --git a/app/src/main/java/com/rosan/accounts/data/common/utils/ContextUtil.kt b/app/src/main/java/com/rosan/accounts/data/common/utils/ContextUtil.kt
index cb69fcf..0d4a2ca 100644
--- a/app/src/main/java/com/rosan/accounts/data/common/utils/ContextUtil.kt
+++ b/app/src/main/java/com/rosan/accounts/data/common/utils/ContextUtil.kt
@@ -16,7 +16,7 @@ fun runOnUiThread(action: () -> Unit) {
}
}
-fun Context.contentCopy(text: CharSequence) {
+fun Context.copy(text: CharSequence) {
runOnUiThread {
val manager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
manager.setPrimaryClip(ClipData.newPlainText("Label", text))
diff --git a/app/src/main/java/com/rosan/accounts/data/common/utils/MapsUtil.kt b/app/src/main/java/com/rosan/accounts/data/common/utils/MapsUtil.kt
new file mode 100644
index 0000000..07c65dd
--- /dev/null
+++ b/app/src/main/java/com/rosan/accounts/data/common/utils/MapsUtil.kt
@@ -0,0 +1,7 @@
+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
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/rosan/accounts/data/common/utils/ShizukuUtil.kt b/app/src/main/java/com/rosan/accounts/data/common/utils/ShizukuUtil.kt
index 4d34194..0ce8cf4 100644
--- a/app/src/main/java/com/rosan/accounts/data/common/utils/ShizukuUtil.kt
+++ b/app/src/main/java/com/rosan/accounts/data/common/utils/ShizukuUtil.kt
@@ -2,10 +2,13 @@ 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 kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.first
import rikka.shizuku.Shizuku
+import rikka.shizuku.ShizukuBinderWrapper
import rikka.sui.Sui
suspend fun requireShizukuPermissionGranted(context: Context, action: suspend () -> T): T {
@@ -29,4 +32,9 @@ suspend fun requireShizukuPermissionGranted(context: Context, action: suspen
}
}.first()
return action()
-}
\ No newline at end of file
+}
+
+fun shizukuBinder(name: String): IBinder =
+ shizukuBinder(ServiceManager.getService(name))
+
+fun shizukuBinder(binder: IBinder): IBinder = ShizukuBinderWrapper(binder)
\ No newline at end of file
diff --git a/app/src/main/java/com/rosan/accounts/data/service/entity/AccountAuthenticatorEntity.kt b/app/src/main/java/com/rosan/accounts/data/service/entity/AccountAuthenticatorEntity.kt
new file mode 100644
index 0000000..9f6631c
--- /dev/null
+++ b/app/src/main/java/com/rosan/accounts/data/service/entity/AccountAuthenticatorEntity.kt
@@ -0,0 +1,11 @@
+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
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/rosan/accounts/data/service/entity/AccountEntity.kt b/app/src/main/java/com/rosan/accounts/data/service/entity/AccountEntity.kt
new file mode 100644
index 0000000..dd9a90d
--- /dev/null
+++ b/app/src/main/java/com/rosan/accounts/data/service/entity/AccountEntity.kt
@@ -0,0 +1,7 @@
+package com.rosan.accounts.data.service.entity
+
+data class AccountEntity(
+ val userId: Int,
+ val type: String,
+ val name: String
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/rosan/accounts/data/service/entity/UserEntity.kt b/app/src/main/java/com/rosan/accounts/data/service/entity/UserEntity.kt
new file mode 100644
index 0000000..3390589
--- /dev/null
+++ b/app/src/main/java/com/rosan/accounts/data/service/entity/UserEntity.kt
@@ -0,0 +1,6 @@
+package com.rosan.accounts.data.service.entity
+
+data class UserEntity(
+ val id: Int,
+ val name: String?
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/rosan/accounts/data/service/model/ShizukuUserService.kt b/app/src/main/java/com/rosan/accounts/data/service/model/ShizukuUserService.kt
new file mode 100644
index 0000000..1595e32
--- /dev/null
+++ b/app/src/main/java/com/rosan/accounts/data/service/model/ShizukuUserService.kt
@@ -0,0 +1,128 @@
+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
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/rosan/accounts/data/service/repo/UserService.kt b/app/src/main/java/com/rosan/accounts/data/service/repo/UserService.kt
new file mode 100644
index 0000000..f972ebb
--- /dev/null
+++ b/app/src/main/java/com/rosan/accounts/data/service/repo/UserService.kt
@@ -0,0 +1,17 @@
+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
+}
diff --git a/app/src/main/java/com/rosan/accounts/di/init/app_modules.kt b/app/src/main/java/com/rosan/accounts/di/init/app_modules.kt
new file mode 100644
index 0000000..eac4a1d
--- /dev/null
+++ b/app/src/main/java/com/rosan/accounts/di/init/app_modules.kt
@@ -0,0 +1,9 @@
+package com.rosan.accounts.di.init
+
+import com.rosan.accounts.di.serviceModule
+import com.rosan.accounts.di.viewModelModule
+
+val appModules = listOf(
+ viewModelModule,
+ serviceModule
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/rosan/accounts/di/service_module.kt b/app/src/main/java/com/rosan/accounts/di/service_module.kt
new file mode 100644
index 0000000..4756bbe
--- /dev/null
+++ b/app/src/main/java/com/rosan/accounts/di/service_module.kt
@@ -0,0 +1,12 @@
+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())
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/rosan/accounts/di/viewmodel_module.kt b/app/src/main/java/com/rosan/accounts/di/viewmodel_module.kt
new file mode 100644
index 0000000..f8838f9
--- /dev/null
+++ b/app/src/main/java/com/rosan/accounts/di/viewmodel_module.kt
@@ -0,0 +1,16 @@
+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())
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/rosan/accounts/ui/activity/MainActivity.kt b/app/src/main/java/com/rosan/accounts/ui/activity/MainActivity.kt
new file mode 100644
index 0000000..b863ed5
--- /dev/null
+++ b/app/src/main/java/com/rosan/accounts/ui/activity/MainActivity.kt
@@ -0,0 +1,26 @@
+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()
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/rosan/accounts/ui/page/account_manager/AccountManagerPage.kt b/app/src/main/java/com/rosan/accounts/ui/page/account_manager/AccountManagerPage.kt
new file mode 100644
index 0000000..5cd29f9
--- /dev/null
+++ b/app/src/main/java/com/rosan/accounts/ui/page/account_manager/AccountManagerPage.kt
@@ -0,0 +1,173 @@
+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)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/rosan/accounts/ui/page/account_manager/AccountManagerViewAction.kt b/app/src/main/java/com/rosan/accounts/ui/page/account_manager/AccountManagerViewAction.kt
new file mode 100644
index 0000000..96992cc
--- /dev/null
+++ b/app/src/main/java/com/rosan/accounts/ui/page/account_manager/AccountManagerViewAction.kt
@@ -0,0 +1,5 @@
+package com.rosan.accounts.ui.page.account_manager
+
+sealed class AccountManagerViewAction {
+ object Load : AccountManagerViewAction()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/rosan/accounts/ui/page/account_manager/AccountManagerViewModel.kt b/app/src/main/java/com/rosan/accounts/ui/page/account_manager/AccountManagerViewModel.kt
new file mode 100644
index 0000000..ba84252
--- /dev/null
+++ b/app/src/main/java/com/rosan/accounts/ui/page/account_manager/AccountManagerViewModel.kt
@@ -0,0 +1,54 @@
+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)
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/rosan/accounts/ui/page/account_manager/AccountManagerViewState.kt b/app/src/main/java/com/rosan/accounts/ui/page/account_manager/AccountManagerViewState.kt
new file mode 100644
index 0000000..f39693d
--- /dev/null
+++ b/app/src/main/java/com/rosan/accounts/ui/page/account_manager/AccountManagerViewState.kt
@@ -0,0 +1,13 @@
+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
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/rosan/accounts/ui/page/main/MainPage.kt b/app/src/main/java/com/rosan/accounts/ui/page/main/MainPage.kt
new file mode 100644
index 0000000..2ce93cb
--- /dev/null
+++ b/app/src/main/java/com/rosan/accounts/ui/page/main/MainPage.kt
@@ -0,0 +1,57 @@
+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
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/rosan/accounts/ui/page/main/MainScreen.kt b/app/src/main/java/com/rosan/accounts/ui/page/main/MainScreen.kt
new file mode 100644
index 0000000..11f4b8e
--- /dev/null
+++ b/app/src/main/java/com/rosan/accounts/ui/page/main/MainScreen.kt
@@ -0,0 +1,10 @@
+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"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/rosan/accounts/ui/page/user_manager/UserManagerPage.kt b/app/src/main/java/com/rosan/accounts/ui/page/user_manager/UserManagerPage.kt
new file mode 100644
index 0000000..00cbb2f
--- /dev/null
+++ b/app/src/main/java/com/rosan/accounts/ui/page/user_manager/UserManagerPage.kt
@@ -0,0 +1,212 @@
+package com.rosan.accounts.ui.page.user_manager
+
+import android.os.Process
+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.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
+)
+@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))
+ }
+ )
+ },
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(it)
+ ) {
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxSize(),
+ contentPadding = PaddingValues(16.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ items(viewModel.state, 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))
+ }
+ })
+}
diff --git a/app/src/main/java/com/rosan/accounts/ui/page/user_manager/UserManagerViewAction.kt b/app/src/main/java/com/rosan/accounts/ui/page/user_manager/UserManagerViewAction.kt
new file mode 100644
index 0000000..86cf7e0
--- /dev/null
+++ b/app/src/main/java/com/rosan/accounts/ui/page/user_manager/UserManagerViewAction.kt
@@ -0,0 +1,9 @@
+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()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/rosan/accounts/ui/page/user_manager/UserManagerViewModel.kt b/app/src/main/java/com/rosan/accounts/ui/page/user_manager/UserManagerViewModel.kt
new file mode 100644
index 0000000..f456831
--- /dev/null
+++ b/app/src/main/java/com/rosan/accounts/ui/page/user_manager/UserManagerViewModel.kt
@@ -0,0 +1,51 @@
+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(emptyList())
+ 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) {
+ state = userService.getUsers().sortedBy { it.id }
+ delay(1500)
+ }
+ }
+ }
+ }
+
+ private fun remove(user: UserEntity) {
+ viewModelScope.launch {
+ userService.removeUser(user)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/rosan/accounts/ui/theme/Theme.kt b/app/src/main/java/com/rosan/accounts/ui/theme/Theme.kt
index f753ab1..64de9b0 100644
--- a/app/src/main/java/com/rosan/accounts/ui/theme/Theme.kt
+++ b/app/src/main/java/com/rosan/accounts/ui/theme/Theme.kt
@@ -2,6 +2,7 @@ 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
@@ -10,6 +11,7 @@ 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
@@ -54,17 +56,24 @@ fun AccountsTheme(
else -> LightColorScheme
}
val view = LocalView.current
- if (!view.isInEditMode) {
- SideEffect {
- val window = (view.context as Activity).window
- window.statusBarColor = colorScheme.surface.toArgb()
- WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme
- window.navigationBarColor = colorScheme.surface.toArgb()
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
- window.navigationBarDividerColor = colorScheme.surface.toArgb()
- WindowCompat.getInsetsController(window, view).isAppearanceLightNavigationBars =
- !darkTheme
- }
+ 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(
diff --git a/app/src/main/java/com/rosan/accounts/ui/widget/PositionDialog.kt b/app/src/main/java/com/rosan/accounts/ui/widget/PositionDialog.kt
new file mode 100644
index 0000000..d29369b
--- /dev/null
+++ b/app/src/main/java/com/rosan/accounts/ui/widget/PositionDialog.kt
@@ -0,0 +1,229 @@
+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
\ No newline at end of file
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000..03b4489
--- /dev/null
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,14 @@
+
+
+ 用户管理
+ 未命名
+
+ 确定要删除此用户吗?\n所有处于此用户空间的应用和数据都会消失!!!
+ 是的,我确定!
+
+ 取消
+ 移除
+ 账号管理
+ 没有账户存在
+ 复制成功,请在【雹】中导入!
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 9f7e2ff..3f0cf5c 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,3 +1,16 @@
- Accounts
+ 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!
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index bb2ab92..8daf508 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -1,5 +1,8 @@
-
+
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 4314313..fd3d478 100644
--- a/build.gradle
+++ b/build.gradle
@@ -2,5 +2,5 @@
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.7.20' apply false
+ id 'org.jetbrains.kotlin.android' version '1.8.20' apply false
}
\ No newline at end of file
diff --git a/hidden-api/build.gradle b/hidden-api/build.gradle
index e374ce2..f850f6c 100644
--- a/hidden-api/build.gradle
+++ b/hidden-api/build.gradle
@@ -12,4 +12,6 @@ android {
}
dependencies {
+ def annotation = "1.6.0"
+ compileOnly "androidx.annotation:annotation:$annotation"
}
\ No newline at end of file
diff --git a/hidden-api/src/main/java/android/accounts/IAccountManager.java b/hidden-api/src/main/java/android/accounts/IAccountManager.java
index 0d6e83a..8e849a0 100644
--- a/hidden-api/src/main/java/android/accounts/IAccountManager.java
+++ b/hidden-api/src/main/java/android/accounts/IAccountManager.java
@@ -1,12 +1,22 @@
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();
diff --git a/hidden-api/src/main/java/android/content/pm/IPackageManager.java b/hidden-api/src/main/java/android/content/pm/IPackageManager.java
index 35d86b5..6cbcea7 100644
--- a/hidden-api/src/main/java/android/content/pm/IPackageManager.java
+++ b/hidden-api/src/main/java/android/content/pm/IPackageManager.java
@@ -1,12 +1,18 @@
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 {
diff --git a/hidden-api/src/main/java/android/content/pm/UserInfo.java b/hidden-api/src/main/java/android/content/pm/UserInfo.java
new file mode 100644
index 0000000..f094d22
--- /dev/null
+++ b/hidden-api/src/main/java/android/content/pm/UserInfo.java
@@ -0,0 +1,37 @@
+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) {
+ }
+}
diff --git a/hidden-api/src/main/java/android/os/IUserManager.java b/hidden-api/src/main/java/android/os/IUserManager.java
index 98c9cc8..0df37c1 100644
--- a/hidden-api/src/main/java/android/os/IUserManager.java
+++ b/hidden-api/src/main/java/android/os/IUserManager.java
@@ -1,6 +1,19 @@
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 {
diff --git a/hidden-api/src/main/java/android/os/ServiceManager.java b/hidden-api/src/main/java/android/os/ServiceManager.java
new file mode 100644
index 0000000..9250605
--- /dev/null
+++ b/hidden-api/src/main/java/android/os/ServiceManager.java
@@ -0,0 +1,104 @@
+/*
+ * 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);
+ }
+ }
+}
\ No newline at end of file