从此
📄文章 #️⃣专题 🌐酷站 👨‍💻技术 📺 📱

Android(基于AOSP)应用开发授权、ADB权限能力、root解锁bootloader、偏好设置(隐私权政策、反馈)


综合/最新


内容

ADB命令:
  打开图片  ./adb shell am start -a android.intent.action.VIEW -t image/* -d /data/user/0/people.projects.moodlighting/cache/x.png
  下载图片  ./adb pull /data/user/0/people.projects.moodlighting/cache/x.png D:\temp\


权限:
  普通权限直接xml声明即可;危险权限需要代码动态申请
  特殊权限则需要编程式Intent请求 - SYSTEM_ALERT_WINDOW 或 WRITE_SETTINGS

  授权判断:
    Settings.canDrawOverlays(this); // SYSTEM_ALERT_WINDOW

  手动开启应用权限:设置》应用》权限》勾上麦克风等
  权限框:拒绝(Deny)、本次运行允许(Allow only while the app is use)、仅在使用中允许(Allow all the time/即长期授权)。

  权限最佳实践:
    MediaProjection 调用期间会自动授予 SYSTEM_ALERT_WINDOW 权限,故可互调保活。

  依赖库权限合并情况 - build\intermediates\manifest_merge_blame_file\
  删除第三方权限 - <uses-permission android:name="android.permission.X" tools:node="remove" />;谷歌gms和firebase库依赖的datatransport均引入了网络访问权限ACCESS_NETWORK_STATE。
  获取xml声明的权限列表(即<uses-permission />) - pm.getPackageInfo(pkg, PackageManager.GET_PERMISSIONS).requestedPermissions;
  授权框 - com.google.android.permissioncontroller/com.android.permissioncontroller.permission.ui.GrantPermissionsActivity

  重置app权限状态(首次和二次拒绝) - ./adb shell pm clear-permission-flags com.example.myapplication android.permission.CAMERA  user-set user-fixed

  说明 - AS IDE权限检查的抑制注解:@SuppressLint("MissingPermission")
    改RequestPermission为RequestMultiplePermissions可同时请求多个权限
    运行时权限 - Protection level: dangerous 标识了normal的安装时即获取
        https://developer.android.com/reference/android/Manifest.permission
    注意 - 需要动态授权的项目,比如TakePicturePreview不能直接launch(null),必须外层用RequestPermission包裹下。

  实例:
    说明 - 只能用于 Activity 和 Fragment 类中,而非 Service 或广播;Android 12、15 授权框必须后退才会dismiss(),Android 14 失去焦点就触发dismiss()。
    ActivityResultCallback<Boolean> arc = isGranted -> {
        System.out.println("isGranted - " + isGranted);
        if (isGranted) { /* 做业务 */ } else {
            System.out.println("拒绝"); // dismiss()也会进入。
        }
    };
    ActivityResultLauncher<String> arl = registerForActivityResult(
            new ActivityResultContracts.RequestPermission(), arc);


    // 上方放类成员,下方则放调用处。
    var p = android.Manifest.permission.CAMERA;
    // [弹框检测方案] 在 onCreate 预热下授权框,避免设备重启后首次载入较慢;将未定义权限名请求加入:
    ActivityCompat.requestPermissions(this, new String[]{ p }, Integer.MAX_VALUE);
    if (ContextCompat.checkSelfPermission(this, p) == PackageManager.PERMISSION_GRANTED) {
        arc.onActivityResult(true); // 授权后代码归并至一处
    } else if (this.shouldShowRequestPermissionRationale(p)) {
        arl.launch(p); // First Time Deny
    } else {
        System.out.println("First Time Launch and Second Time Deny - else");
        arl.launch(p);
        new Handler().postDelayed(() -> { // 为true则指已拒绝2次不再弹框,为false则由授权框dismiss()触发。
            // or 回调快慢方案 time = System.currentTimeMillis(); (System.currentTimeMillis() - time)<400;
            if (notShownGrantPermissionsActivity(MainActivity.this)) {
              new AlertDialog.Builder(this).setMessage("[扫码]功能需要摄像头拍照"+"\n\uD83D\uDE4F[" + p + "]")
                .setPositiveButton("✅ 开始授权", (di, i) -> PermissionCommon.toAppSettings(MainActivity.this))
                .setNegativeButton("\uD83D\uDEAB 拒绝授权(功能受限)", (di, i) ->
                  Toast.makeText(this, "仅基本功能", Toast.LENGTH_SHORT).show()).show();
            }
        }, 250);
    }

    public static boolean notShownGrantPermissionsActivity(Activity activity) {
        boolean dialogIsHide = false;
        var activityManager = (ActivityManager) activity.getSystemService(Context.ACTIVITY_SERVICE);
        var infos = activityManager.getRunningTasks(Integer.MAX_VALUE);
        for (var info : infos) {
            if (info.id == activity.getTaskId()) {
                Log.d("TaskInfo", "Found task: " + info.topActivity);
                dialogIsHide = activity.getComponentName().equals(info.topActivity);
            }
        }
        return dialogIsHide;
    }

    try {
        var packageInfo = getPackageManager().getPackageInfo(getPackageName(), PackageManager.GET_PERMISSIONS);
        var ps = packageInfo.requestedPermissions; // 静态权限列表,非runtime权限。
        System.out.println("permissions - " + ps);
        if (ps != null) {
            for (var permission : ps) {
                var permissionInfo = pm.getPermissionInfo(permission, PackageManager.GET_META_DATA);
                System.out.println("permissionInfo - " + permissionInfo);
                // 获取 flag 值,可能需要反射
                int flags = permissionInfo.flags;
                // 根据 flags 值判断权限状态
                // PackageManager.java
                final int FLAG_PERMISSION_USER_SET = 1 << 0;
                final int FLAG_PERMISSION_USER_FIXED = 1 << 1;
                System.out.println("flags - " + flags);
                if ((flags & FLAG_PERMISSION_USER_FIXED) != 0) {
                    // 用户已明确拒绝权限
                    System.out.println("用户已明确拒绝权限");
                } else if ((flags & FLAG_PERMISSION_USER_SET) != 0) {
                    // 用户曾手动设置过该权限
                    System.out.println("用户曾手动设置过该权限");
                }
            }
        }
    } catch (PackageManager.NameNotFoundException e) {
        e.printStackTrace(System.err);
    }

root、解锁bootloader

概念:
  解锁 Bootloader 引导 - 刷入 TWRP Recovery 之前提。
  刷入 Recovery 恢复模式 - 官方 Recovery 缺乏 TeamWin Recovery Project(TWRP)的高级功能,比如刷入未签名ZIP包。
  获取 root 权限 - 两种方式: 刷入 /boot 分区(Magisk 面具 Systemless Root 不会影响保修状态) 和 刷入 /system 分区,因未签名,故只能通过 TWRP 来刷入。
  OS 增强 - Magisk Modules 取代了 Xposed 及类似工具。

还能root的手机型号:一加(root后依然保修)、华为p20系列、华为mate10系列、小米8、小米9。
提供root解锁网店:https://shop155814799.taobao.com/ 费用80元。

其他

允许WebView明文访问:<manifest android:targetSandboxVersion="1" ...><application android:usesCleartextTraffic="true" ...></application></manifest>
  注意 - 设置 android:targetSandboxVersion="1" 后,需手动转移免安装应用数据,值为 2 时则会自动转移。
  不设置则WebView的http页面会显示“net::ERR_CLEARTEXT_NOT_PERMITTED”。