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