From 599f6c06bbf22404af66aeabdc571cab4fbf1f3b Mon Sep 17 00:00:00 2001 From: BinTianqi <1220958406@qq.com> Date: Tue, 13 Feb 2024 12:38:33 +0800 Subject: [PATCH] support transform ownership --- Guide.md | 86 ++++++++++++++++--- .../com/binbin/androidowner/DeviceControl.kt | 6 +- .../com/binbin/androidowner/MainActivity.kt | 17 ++-- .../com/binbin/androidowner/ManagedProfile.kt | 2 +- .../java/com/binbin/androidowner/Network.kt | 2 +- .../java/com/binbin/androidowner/Password.kt | 8 +- .../com/binbin/androidowner/Permissions.kt | 37 ++++++++ .../java/com/binbin/androidowner/Receiver.kt | 17 ++++ .../main/java/com/binbin/androidowner/User.kt | 31 ++++--- app/src/main/res/xml/device_admin.xml | 1 + 10 files changed, 162 insertions(+), 45 deletions(-) diff --git a/Guide.md b/Guide.md index 1874a79..aafe806 100644 --- a/Guide.md +++ b/Guide.md @@ -166,6 +166,16 @@ adb shell dpm remove-active-admin com.binbin.androidowner/com.binbin.androidowne 提供支持的长消息不知道有啥用 +### 转移所有权 + +需要Device owner或Profile owner + +需要API28或以上 + +转移设备所有权到另外一个Device owner或Profile owner + +目标应用必须是Device admin且支持被转移所有权 + ## 系统 ### 禁用相机 @@ -450,7 +460,7 @@ API34或以上将不能在系统用户中使用WipeData,如果要恢复出厂 ### 网络日志记录 -需要的权限:Device owner或Profile owner +需要的权限:Device owner或工作资料的Profile owner 需要API26或以上 @@ -476,22 +486,28 @@ API34或以上将不能在系统用户中使用WipeData,如果要恢复出厂 ## 工作资料 -工作资料是一种特殊的用户,使用`adb shell pm list user`命令可以看到工作资料 +工作资料是一种特殊的用户,使用`adb shell pm list user`命令可以看到工作资料,工作资料的默认用户名是“工作资料”或“Work Profile” ### 创建工作资料 设备上不能有Device owner或Profile owner -只能有一个工作资料 +一个设备只能有一个工作资料 选项: -- 跳过加密(需要API24或以上,没啥用) +- 跳过加密(需要API24或以上,没有实际作用) 创建后会跳转到工作资料中的Android owner,请立即按照指引激活工作资料 创建后工作资料中的Android owner会成为Profile owner +在WearOS上创建工作资料会导致SystemUI停止运行一次。WearOS原生的启动器不支持工作资料,你需要使用第三方启动器(比如微软桌面)。你可以通过[ADB命令移除工作资料](#删除工作资料) + +此外,不要作死给工作资料重置密码,不然你连输入密码的地方都没有 + +(只在原生WearOS4(AVD)上测试过) + ### 由组织拥有的工作资料 需要API30或以上 @@ -527,8 +543,6 @@ dpm mark-profile-owner-on-organization-owned-device --user USER_ID com.binbin.an ### 跨资料Intent过滤器 -[安卓开发者:Intent](https://developer.android.google.cn/reference/kotlin/android/content/Intent) - 需要的权限:工作资料的Profile owner 默认情况下,工作资料中的应用不能打开个人应用,个人应用也不可以打开工作资料中的应用 @@ -551,6 +565,12 @@ dpm mark-profile-owner-on-organization-owned-device --user USER_ID com.binbin.an 如果你的工作资料不是由组织拥有的,你可以打开安卓设置->安全->更多安全设置->设备管理器->带工作资料图标的Android owner->移除工作资料(非原生用户自己找) +你也可以使用ADB命令移除工作资料(把USER_ID替换为工作资料的UserID) + +```shell +adb shell pm remove-user USER_ID +``` + ## 应用管理 如果是工作资料,只能管理工作资料中的应用 @@ -770,19 +790,33 @@ Profile owner无法禁用部分功能,工作资料中部分功能无效,wear ## 用户管理 +用户(user)不是账号(account) + +使用ADB查看所有用户: + +```shell +adb shell pm list users +``` + +用户名前面的数字就是UserID + ### 用户信息 -当前用户的信息 +用户已解锁:你能看到这个的时候一定解锁了 + +支持多用户:系统是否支持多用户。WearOS即使写着支持多用户,但不一定支持 系统用户:UserID为0的用户(需API23) +管理员用户:可以创建、删除用户。一个设备可以有多个管理员用户(需API34) + 无头系统用户:~~头被砍掉了~~ 系统用户运行着系统服务,但是没有分配给任何人使用,也不能切换到系统用户(需API31) -可以登出:用户限制->[用户](#用户)->切换用户(需API28) +可以登出:功能未知,无论什么用户都不能登出 -临时用户:临时用户登出后会被删除(需API28) +临时用户:临时用户登出后或重启后会被删除(需API28) -附属用户:运行Device owner的用户是附属于设备的用户,受管理用户可以设置附属用户ID以成为附属用户(开发中) +附属用户:详见[附属用户ID](#附属用户ID) UserID:不是UID。系统用户的UserID为0,其他用户(包括工作资料)的UserID从10开始计算 @@ -798,7 +832,7 @@ UserID:不是UID。系统用户的UserID为0,其他用户(包括工作资 ### 创建并管理用户 -创建一个受管理用户 +创建一个受管理用户,新用户的头像右下方会有公文包标志 需要Device owner和API24 @@ -808,6 +842,36 @@ UserID:不是UID。系统用户的UserID为0,其他用户(包括工作资 - 临时用户(需API28) - 启用所有系统应用(有些系统应用在新用户中是默认不启用的,比如谷歌手机上的YouTube) +创建后,Android owner会成为受管理用户中的Profile owner + +这个功能在WearOS上使用会导致SystemUI停止运行一次,过几秒恢复正常。创建用户实际上成功了,回到Android owner后能看到新用户的序列号,`pm list users`也能看到新用户。如果切换到新用户,SystemUI无法使用,表现为黑屏(可以用ADB命令启动别的应用)。如果黑屏无法使用,ADB执行下面这个命令(把USER_ID替换成受管理用户的用户序列号) + +```shell +adb shell pm remove-user --set-ephemeral-if-in-use USER_ID +``` + +新用户会被设为临时用户,重启后临时用户会被删除并切换到主用户 + +(原生WearOS4(AVD)会出现这个问题,其他版本不知道有没有这个问题) + +### 使用Intent创建用户 + +不需要任何权限,但也没啥用,建议Device owner创建并管理用户 + +可能会导致Android owner停止运行,但是停止运行后没log,所以不知道为什么无法创建 + +### 附属用户ID + +需要Device owner或Profile owner(工作资料中的Profile owner虽然也能设置,但是没有实际作用) + +附属用户ID是一个列表,列表中可以有多个不相同的ID,不考虑顺序 + +当Device owner创建并管理用户时,新的用户不是附属用户。Device owner设置和受管理用户完全相同的附属用户ID后,受管理用户成为附属于Device owner的用户 + +Device owner无论在何时都是附属于设备的用户 + +你可以在用户管理->[用户信息](#用户信息)查看当前用户是否附属用户 + ### 用户名 修改当前用户的用户名 diff --git a/app/src/main/java/com/binbin/androidowner/DeviceControl.kt b/app/src/main/java/com/binbin/androidowner/DeviceControl.kt index 3e50038..0aee4d2 100644 --- a/app/src/main/java/com/binbin/androidowner/DeviceControl.kt +++ b/app/src/main/java/com/binbin/androidowner/DeviceControl.kt @@ -522,6 +522,7 @@ fun DeviceControl(){ TextField( value = reason, onValueChange = {reason=it}, label = {Text("原因")}, + enabled = !confirmed, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), keyboardActions = KeyboardActions(onDone = {focusMgr.clearFocus()}), modifier = Modifier.fillMaxWidth().padding(vertical = 3.dp) @@ -529,6 +530,7 @@ fun DeviceControl(){ } Button( onClick = { + focusMgr.clearFocus() flag = 0 if(externalStorage){flag += WIPE_EXTERNAL_STORAGE} if(protectionData&&VERSION.SDK_INT>=22){flag += WIPE_RESET_PROTECTION_DATA} @@ -571,7 +573,9 @@ fun DeviceControl(){ if(VERSION.SDK_INT>=24&&isProfileOwner(myDpm)&&myDpm.isManagedProfile(myComponent)){ Text(text = "将会删除工作资料", style = bodyTextStyle) } - Text(text = "API34或以上将不能在系统用户中使用WipeData", style = bodyTextStyle) + if(VERSION.SDK_INT>=34){ + Text(text = "API34或以上将不能在系统用户中使用WipeData", style = bodyTextStyle) + } } Spacer(Modifier.padding(vertical = 30.dp)) } diff --git a/app/src/main/java/com/binbin/androidowner/MainActivity.kt b/app/src/main/java/com/binbin/androidowner/MainActivity.kt index 6e2a124..f75eb71 100644 --- a/app/src/main/java/com/binbin/androidowner/MainActivity.kt +++ b/app/src/main/java/com/binbin/androidowner/MainActivity.kt @@ -71,16 +71,15 @@ class MainActivity : ComponentActivity() { } createUser = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { when(it.resultCode){ - UserManager.USER_CREATION_FAILED_NO_MORE_USERS->Toast.makeText(applicationContext, "用户太多了", Toast.LENGTH_SHORT).show() + Activity.RESULT_OK->Toast.makeText(applicationContext, "成功", Toast.LENGTH_SHORT).show() + Activity.RESULT_CANCELED->Toast.makeText(applicationContext, "用户太多了", Toast.LENGTH_SHORT).show() UserManager.USER_CREATION_FAILED_NOT_PERMITTED->Toast.makeText(applicationContext, "不是管理员用户", Toast.LENGTH_SHORT).show() - else->Toast.makeText(applicationContext, "成功", Toast.LENGTH_SHORT).show() + UserManager.USER_CREATION_FAILED_NO_MORE_USERS->Toast.makeText(applicationContext, "用户太多了", Toast.LENGTH_SHORT).show() + else->Toast.makeText(applicationContext, "创建用户结果未知", Toast.LENGTH_SHORT).show() } } createManagedProfile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - when(it.resultCode){ - Activity.RESULT_OK->Toast.makeText(applicationContext, "创建成功", Toast.LENGTH_SHORT).show() - Activity.RESULT_CANCELED->Toast.makeText(applicationContext, "用户已取消", Toast.LENGTH_SHORT).show() - } + if(it.resultCode==Activity.RESULT_CANCELED){Toast.makeText(applicationContext, "用户已取消", Toast.LENGTH_SHORT).show()} } setContent { AndroidOwnerTheme { @@ -141,12 +140,8 @@ fun MyScaffold(){ if(sharedPref.getBoolean("isWear",false)&&topBarName!=R.string.app_name){ FloatingActionButton( onClick = { - navCtrl.navigate("HomePage") { - popUpTo( - navCtrl.graph.findStartDestination().id - ) { saveState = true } - } focusMgr.clearFocus() + navCtrl.navigateUp() }, modifier = Modifier.size(35.dp), containerColor = MaterialTheme.colorScheme.tertiaryContainer, diff --git a/app/src/main/java/com/binbin/androidowner/ManagedProfile.kt b/app/src/main/java/com/binbin/androidowner/ManagedProfile.kt index abb2a10..d85cfb9 100644 --- a/app/src/main/java/com/binbin/androidowner/ManagedProfile.kt +++ b/app/src/main/java/com/binbin/androidowner/ManagedProfile.kt @@ -65,7 +65,7 @@ fun ManagedProfile() { Text("跳转至个人应用") } }else{ - if(!myDpm.isProvisioningAllowed(ACTION_PROVISION_MANAGED_PROFILE)&&!isDeviceOwner(myDpm)){ + if(!myDpm.isProvisioningAllowed(ACTION_PROVISION_MANAGED_PROFILE)&&!isDeviceOwner(myDpm)&&!isProfileOwner(myDpm)){ Button( onClick = { myContext.startActivity(Intent("com.binbin.androidowner.MAIN_ACTION")) }, modifier = Modifier.fillMaxWidth() ){ diff --git a/app/src/main/java/com/binbin/androidowner/Network.kt b/app/src/main/java/com/binbin/androidowner/Network.kt index 162322d..f9db9ee 100644 --- a/app/src/main/java/com/binbin/androidowner/Network.kt +++ b/app/src/main/java/com/binbin/androidowner/Network.kt @@ -216,7 +216,7 @@ fun Network(){ } } - if(VERSION.SDK_INT>=26&&(isDeviceOwner(myDpm)||isProfileOwner(myDpm))){ + if(VERSION.SDK_INT>=26&&(isDeviceOwner(myDpm)||(isProfileOwner(myDpm)&&myDpm.isManagedProfile(myComponent)))){ Column(modifier = sections()){ Text(text = "收集网络日志", style = typography.titleLarge) Text(text = "功能开发中", style = bodyTextStyle) diff --git a/app/src/main/java/com/binbin/androidowner/Password.kt b/app/src/main/java/com/binbin/androidowner/Password.kt index aa31047..77d5ac7 100644 --- a/app/src/main/java/com/binbin/androidowner/Password.kt +++ b/app/src/main/java/com/binbin/androidowner/Password.kt @@ -179,8 +179,8 @@ fun Password(){ Button( onClick = { val resetSuccess = myDpm.resetPasswordWithToken(myComponent,newPwd,myByteArray,resetPwdFlag) - if(resetSuccess){ Toast.makeText(myContext, "设置成功", Toast.LENGTH_SHORT).show() - }else{ Toast.makeText(myContext, "设置失败", Toast.LENGTH_SHORT).show() } + if(resetSuccess){ Toast.makeText(myContext, "设置成功", Toast.LENGTH_SHORT).show();newPwd=""} + else{ Toast.makeText(myContext, "设置失败", Toast.LENGTH_SHORT).show() } confirmed=false }, colors = ButtonDefaults.buttonColors(containerColor = colorScheme.error, contentColor = colorScheme.onError), @@ -193,8 +193,8 @@ fun Password(){ Button( onClick = { val resetSuccess = myDpm.resetPassword(newPwd,resetPwdFlag) - if(resetSuccess){ Toast.makeText(myContext, "设置成功", Toast.LENGTH_SHORT).show() - }else{ Toast.makeText(myContext, "设置失败", Toast.LENGTH_SHORT).show() } + if(resetSuccess){ Toast.makeText(myContext, "设置成功", Toast.LENGTH_SHORT).show();newPwd=""} + else{ Toast.makeText(myContext, "设置失败", Toast.LENGTH_SHORT).show() } confirmed=false }, enabled = confirmed, diff --git a/app/src/main/java/com/binbin/androidowner/Permissions.kt b/app/src/main/java/com/binbin/androidowner/Permissions.kt index be2ed10..907ec5a 100644 --- a/app/src/main/java/com/binbin/androidowner/Permissions.kt +++ b/app/src/main/java/com/binbin/androidowner/Permissions.kt @@ -23,6 +23,7 @@ import androidx.compose.material3.TextField import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.focus.FocusManager import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager @@ -32,6 +33,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.core.content.ContextCompat.startActivity import androidx.navigation.NavHostController +import java.lang.IllegalArgumentException @Composable @@ -320,6 +322,41 @@ fun DpmPermissions(navCtrl:NavHostController){ DeviceOwnerInfo(R.string.long_support_msg,R.string.long_support_msg_desc,R.string.message,focusManager,myContext, {myDpm.getLongSupportMessage(myComponent)},{content -> myDpm.setLongSupportMessage(myComponent,content)}) } + + if(VERSION.SDK_INT>=28&&(isDeviceOwner(myDpm)||isProfileOwner(myDpm))){ + Column(modifier = sections()){ + var pkg by remember{mutableStateOf("")} + var cls by remember{mutableStateOf("")} + Text(text = "转移所有权", style = typography.titleLarge) + Text(text = "把Device owner或Profile owner权限转移到另一个应用。目标必须是Device admin", style = bodyTextStyle) + TextField( + value = pkg, onValueChange = {pkg = it}, label = {Text("目标包名")}, + modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp), + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), + keyboardActions = KeyboardActions(onNext = {focusManager.moveFocus(FocusDirection.Down)}) + ) + TextField( + value = cls, onValueChange = {cls = it}, label = {Text("目标类名")}, + modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp), + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardActions = KeyboardActions(onDone = {focusManager.clearFocus()}) + ) + Button( + onClick = { + try { + myDpm.transferOwnership(myComponent,ComponentName(pkg, cls),null) + Toast.makeText(myContext, "成功", Toast.LENGTH_SHORT).show() + }catch(e:IllegalArgumentException){ + Toast.makeText(myContext, "失败", Toast.LENGTH_SHORT).show() + } + }, + modifier = Modifier.fillMaxWidth() + ) { + Text("转移") + } + } + } + if(isWear&&(myDpm.isAdminActive(myComponent)||isProfileOwner(myDpm)||isDeviceOwner(myDpm))){ Column(modifier = sections(), horizontalAlignment = Alignment.CenterHorizontally) { Button( diff --git a/app/src/main/java/com/binbin/androidowner/Receiver.kt b/app/src/main/java/com/binbin/androidowner/Receiver.kt index 8ae7aa4..0d368eb 100644 --- a/app/src/main/java/com/binbin/androidowner/Receiver.kt +++ b/app/src/main/java/com/binbin/androidowner/Receiver.kt @@ -7,6 +7,7 @@ import android.content.Context import android.content.Intent import android.content.pm.PackageInstaller.* import android.os.Build.VERSION +import android.os.PersistableBundle import android.util.Log import android.widget.Toast @@ -15,6 +16,22 @@ class MyDeviceAdminReceiver : DeviceAdminReceiver() { super.onEnabled(context, intent) Toast.makeText(context, "已启用", Toast.LENGTH_SHORT).show() } + + override fun onTransferOwnershipComplete(context: Context, bundle: PersistableBundle?) { + super.onTransferOwnershipComplete(context, bundle) + if(bundle!=null){ + Toast.makeText(context,"转移控制权完毕,附加内容长度:${bundle.size()}",Toast.LENGTH_SHORT).show() + Log.d("TransferOwnerShip",bundle.toString()) + }else{ + Toast.makeText(context,"转移控制权完毕,无附加内容}",Toast.LENGTH_SHORT).show() + } + } + + override fun onProfileProvisioningComplete(context: Context, intent: Intent) { + super.onProfileProvisioningComplete(context, intent) + Toast.makeText(context, "创建工作资料完成", Toast.LENGTH_SHORT).show() + } + @SuppressLint("UnsafeProtectedBroadcastReceiver") override fun onReceive(context: Context, intent: Intent) { super.onReceive(context, intent) diff --git a/app/src/main/java/com/binbin/androidowner/User.kt b/app/src/main/java/com/binbin/androidowner/User.kt index 4d1517b..c47f9d5 100644 --- a/app/src/main/java/com/binbin/androidowner/User.kt +++ b/app/src/main/java/com/binbin/androidowner/User.kt @@ -49,11 +49,9 @@ fun UserManage() { Column(modifier = sections()) { Text(text = "用户信息", style = typography.titleLarge,color = colorScheme.onPrimaryContainer) Text("用户已解锁:${UserManagerCompat.isUserUnlocked(myContext)}",style = bodyTextStyle) - if(VERSION.SDK_INT>=24){ - Text("支持多用户:${UserManager.supportsMultipleUsers()}",style = bodyTextStyle) - if(isWear&&UserManager.supportsMultipleUsers()){Text(text = "实际上手表可能不支持", style = typography.bodyMedium, color = colorScheme.error)} - } - if(VERSION.SDK_INT>=23){Text(text = "系统用户:${userManager.isSystemUser}")} + if(VERSION.SDK_INT>=24){ Text("支持多用户:${UserManager.supportsMultipleUsers()}",style = bodyTextStyle) } + if(VERSION.SDK_INT>=23){ Text(text = "系统用户:${userManager.isSystemUser}", style = bodyTextStyle) } + if(VERSION.SDK_INT>=34){ Text(text = "管理员用户:${userManager.isAdminUser}", style = bodyTextStyle) } if(VERSION.SDK_INT>=31){ Text(text = "无头系统用户: ${UserManager.isHeadlessSystemUserMode()}",style = bodyTextStyle) } Spacer(Modifier.padding(vertical = if(isWear){2.dp}else{5.dp})) if (VERSION.SDK_INT >= 28) { @@ -140,7 +138,6 @@ fun UserManage() { onClick = { focusMgr.clearFocus() if(myDpm.switchUser(myComponent,userHandleById)){ - focusMgr.clearFocus() Toast.makeText(myContext, "成功", Toast.LENGTH_SHORT).show() }else{ Toast.makeText(myContext, "失败", Toast.LENGTH_SHORT).show() @@ -200,7 +197,6 @@ fun UserManage() { onValueChange = {userName=it}, label = {Text("用户名")}, modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), - enabled = isDeviceOwner(myDpm), keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), keyboardActions = KeyboardActions(onDone = {focusMgr.clearFocus()}) ) @@ -223,16 +219,18 @@ fun UserManage() { ) { Text("创建(Owner)") } - Button( - onClick = { - val intent = UserManager.createUserCreationIntent(userName,null,null,null) - createUser.launch(intent) - }, - modifier = Modifier.fillMaxWidth() - ) { - Text("创建(Intent)") + if(UserManager.supportsMultipleUsers()&&(VERSION.SDK_INT<34||(VERSION.SDK_INT>=34&&userManager.isAdminUser))){ + Button( + onClick = { + val intent = UserManager.createUserCreationIntent(userName,null,null,null) + createUser.launch(intent) + }, + modifier = Modifier.fillMaxWidth() + ) { + Text("创建(Intent)") + } + Text(text = "尽量用Device owner模式创建,Intent模式可能没有效果", style = bodyTextStyle) } - Text(text = "尽量用Device owner模式创建,Intent模式可能没有效果", style = bodyTextStyle) if(newUserHandle!=null){ Text(text = "新用户的序列号:${userManager.getSerialNumberForUser(newUserHandle)}", style = bodyTextStyle) } } } @@ -288,6 +286,7 @@ fun UserManage() { ) { Text("应用") } + Text(text = "如果多用户,附属用户ID相同时可以让其他用户附属于主用户", style = bodyTextStyle) } } diff --git a/app/src/main/res/xml/device_admin.xml b/app/src/main/res/xml/device_admin.xml index 325a464..0a4da1a 100644 --- a/app/src/main/res/xml/device_admin.xml +++ b/app/src/main/res/xml/device_admin.xml @@ -12,4 +12,5 @@ + \ No newline at end of file