如何应对Android面试官 -> PKMS 权限管理

前言


本章我们继续上一章节,讲解 PKMS 相关知识点;

静默安装


静默安装说的就是:在用户无感知的情况下,给用户的手机安装了某个 app,或者是用户触发安装之后,不需要额外的任何操作即可以安装目标 app 到手机上的行为;

手机内置的应用市场 app,其实现了静默安装的能力,三方 app 如果想实现这个能力是不可能的。因为需要系统的签名,这个签名厂商是不会开放的,所以我们无法实现想要的静默安装能力;

签名

那么,这个签名是什么呢?就是在 PKMS 的构造方法中,添加到系统 Settings 的 UID

复制代码
mSettings.addSharedUserLPw("android.uid.system", Process.SYSTEM_UID,ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);

mSettings.addSharedUserLPw("android.uid.phone", RADIO_UID,ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);

mSettings.addSharedUserLPw("android.uid.log", LOG_UID, ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);

mSettings.addSharedUserLPw("android.uid.nfc", NFC_UID, ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);

mSettings.addSharedUserLPw("android.uid.bluetooth", BLUETOOTH_UID, ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);

mSettings.addSharedUserLPw("android.uid.shell", SHELL_UID, ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);

mSettings.addSharedUserLPw("android.uid.se", SE_UID, ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);

mSettings.addSharedUserLPw("android.uid.networkstack", NETWORKSTACK_UID, ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);

这里额外插入一个小的知识点,如果做系统应用开发,如何让系统应用在系统启动的时候被装载进来,需要使用 adb push 相关命令;

复制代码
1. adb remount
2. adb shell
3. chmod777 system/app
4. exit
5. adb push xxx/yy.apk system/app
6. adb reboot

这里 xxx/yy.apk 是 apk 的绝对路径,system/app 是系统目录;这些操作的本质就是:拷贝 + 扫描,将你定制的系统应用拷贝到 system/app 中然后重启系统执行扫描操作;

厂商静默安装实现

如果想实现系统的『静默安装』能力,我们需要准备哪些能力呢?首先我们需要三个系统的 aidl 文件;

android.content.pm.IPackageDeleteObserver.aidl

android.content.pm.IPackageInstallObserver.aidl

android.content.pm.IPackageMoveObserver.aidl

复制代码
/**
 * API for deletion callbacks from the Package Manager.
 *
 * {@hide}
 */
oneway interface IPackageDeleteObserver {
    void packageDeleted(in String packageName, in int returnCode);
}

/**
 * API for installation callbacks from the Package Manager.
 * @hide
 */
oneway interface IPackageInstallObserver {
    void packageInstalled(in String packageName, int returnCode);
}

/**
 * Callback for moving package resources from the Package Manager.
 * @hide
 */
oneway interface IPackageMoveObserver {
    void onCreated(int moveId, in Bundle extras);
    void onStatusChanged(int moveId, int status, long estMillis);
}

然后我们直接调用 PKMS 的 installPackage 方法

复制代码
String fileName = Environment.getExternalStorageDirectory() + File.separator + File.separator + "wms.apk";
Uri uri = Uri.fromFile(new File(fileName));
int installFlags = 0;
PackageManager pm = getPackageManager();
try {
     PackageInfo pi = pm.getPackageInfo("com.example.pkmsdemo", PackageManager.GET_UNINSTALLED_PACKAGES);
     if (pi != null) {
        installFlags |= PackageManager.INSTALL_REPLACE_EXISTING;
     }
} catch (PackageManager.NameNotFoundException e) {
}
MyPakcageInstallObserver observer = new MyPakcageInstallObserver();
pm.installPackage(uri, observer, installFlags, "com.example.wmsdemo");

到这里的时候,还是不够,我们需要在 AndroidManifest 中声明 android:sharedUserId="android.uid.system" 以及对应的权限

复制代码
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.INSTALL_PACKAGES" />
    <uses-permission android:name="android.permission.DELETE_PACKAGES" />
    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>

由于在上面增加了 android:sharedUserId="android.uid.system" 就需要厂商的签名,需要厂商提供签名文件,而这个厂商的签名属于厂商的机密,这个【platform.x509.pem platform.pem】文件是拿不到的 java -jar signapk.jar platform.x509.pem platform.pem 需要签名的apk 签名之后的 apk

只有拿到签名文件,才能像 MainActivity 的那种方式安装;

运行时权限


Android 在 6.0 引入了动态权限,需要用户在使用到的时候进行授权;常规的动态权限申请,大家直接看开发文档或者百度一下就可以自行实现;我们接下来基于这个运行时权限使用 aspectj 来封装一个权限申请框架;

权限注解

aspectj 是基于注解的注解的方式,首先我们需要定义几个注解,注解如何定义,可以查看我前面的文章 Java中的注解、反射、手写ButterKnife核心实现 关于注解的详细讲解;

复制代码
/**
 * 权限申请注解
 */
@Target(ElementType.METHOD)  // 方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时
public @interface Permission {

    // 具体申请的权限
    String[] value();

    // 默认的
    int requestCode() default -1;

}

我们还需要另外两个注解,一个是权限取消时的回调注解,一个是权限拒绝时的回调注解;

权限取消注解

复制代码
/**
 * 权限取消注解
 */
@Target(ElementType.METHOD)  // 方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时
public @interface PermissionCancel {

    int requestCode() default -1;

}

权限拒绝注解

复制代码
/**
 * 权限拒绝注解
 */
@Target(ElementType.METHOD)  // 方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时
public @interface PermissionDenied {

    int requestCode() default -1;

}

权限申请结果回调

我们还需要一个接口,来回调相关权限申请结果

复制代码
public interface IPermission {

    void ganted(); // 已经授权

    void cancel(); // 取消权限

    void denied(); // 拒绝权限
}

使用声明的注解

复制代码
// 申请权限  函数名可以随意些
@Permission(value = Manifest.permission.READ_EXTERNAL_STORAGE, requestCode = 200)
public void testRequest() {
    Toast.makeText(this, "权限申请成功...", Toast.LENGTH_SHORT).show();
}

桥接 Activity

权限的申请,需要 onRequestPermissionsResult 回调中处理请求结果,而这个方法是 Context 的,我们需要通过一个透明的 Activity 桥接一下请求以及回调结果;

复制代码
public class PermissionActivity extends Activity {

    /** 定义权限处理的标记,接收用户传递进来的 */
    private final static String PARAM_PERMSSION = "param_permission";
    private final static String PARAM_PERMSSION_CODE = "param_permission_code";
    public final static int PARAM_PERMSSION_CODE_DEFAULT = -1;

    /** 真正接收权限存储的变量 */ 
    private String[] permissions;
    private int requestCode;
    
    /** 方便回调的监听,告诉调用者申请结果:已授权,被拒绝,被取消 */
    private static IPermission iPermissionListener;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 设置为 透明 的 Activity
        setContentView(R.layout.activity_my_permission);

        // 接收传递进来的要申请的权限,READ_EXTERNAL_STORAGE 以这个为例子
        permissions = getIntent().getStringArrayExtra(PARAM_PERMSSION);
        requestCode = getIntent().getIntExtra(PARAM_PERMSSION_CODE, PARAM_PERMSSION_CODE_DEFAULT);

        if (permissions == null && requestCode < 0 && iPermissionListener == null) {
            this.finish();
            return;
        }

        // 能够走到这里,就开始去检查,是否已经授权了
        boolean permissionRequest = PermissionUtils.hasPermissionRequest(this, permissions);
        // 已经授权了,不需要申请,直接回调结果
        if (permissionRequest) {
            // 通过监听接口,告诉外交,已经授权了
            iPermissionListener.ganted();
            this.finish();
            return;
        }

        // 能够走到这里,就证明,还需要去申请权限
        ActivityCompat.requestPermissions(this, permissions, requestCode);
    }

    // 申请的结果
    @Override
    public void onRequestPermissionsResult(int requestCode,
                                           @NonNull String[] permissions,
                                           @NonNull int[] grantResults) { 
        // 如果申请三个权限  grantResults.len = 3
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);

        // 返回的结果,需要去验证一下,是否完全成功
        if (PermissionUtils.requestPermissionSuccess(grantResults)) {
            iPermissionListener.ganted(); // 已经授权成功了
            this.finish();
            return;
        }

        // 没有成功
        // 如果用户点击了,拒绝(勾选了"不再提醒") 等操作
        if (!PermissionUtils.shouldShowRequestPermissionRationale(this, permissions)) {
            // 用户拒绝,不再提醒
            iPermissionListener.denied();
            this.finish();
            return;
        }

        // 取消
        iPermissionListener.cancel();
        this.finish();
        return;
    }

    // 让此 Activity 不要有任何动画
    @Override
    public void finish() {
        super.finish();
        overridePendingTransition(0, 0);
    }

    /**
     * 此权限申请专用的 Activity,对外暴露 static
     */
    public static void requestPermissionAction(Context context, String[] permissions,
                                               int requestCode, IPermission iPermissionListener) {
        PermissionActivity.iPermissionListener = iPermissionListener;
        // 启动这个空白的Activity 并接收相关要申请的权限,并发起权限申请
        Intent intent = new Intent(context, PermissionActivity.class);
        // 启动模式
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
        Bundle bundle = new Bundle();
        bundle.putInt(PARAM_PERMSSION_CODE, requestCode);
        bundle.putStringArray(PARAM_PERMSSION, permissions);
        intent.putExtras(bundle);
        context.startActivity(intent);
    }

}

注解处理 Aspectj

接下来我们需要通过 Aspectj 来处理相关的注解;

Aspectj 中有两个概念,一个是切点,一个是切面;切点就是我们要切入的地方,切面就是我们从切入的地方,切下来之后的内容;

切点函数

Aspectj 中通过 @Pointcut 注解来标记切点,我们来声明切点函数;

复制代码
@Aspect
public class PermissionAspect {
    // 通过 @Pointcut 来标记切点函数,传递需要处理的注解,也就是我们前面声明的 Permisson 注解;
    // @Permission == permission
    @Pointcut("execution(@com.example.permission.annotation.Permission * *(..)) && @annotation(permission)")
    public void pointActionMethod(Permission permission) {} // 切点函数
}

切面函数

Aspectj中通过 @Around 注解来标记切面函数,我们来声明切面函数

复制代码
@Aspect
public class PermissionAspect {

    // 注解中传入的切点函数名必须和声明的一样才能找到
    @Around("pointActionMethod(permission)")
    public void aProceedingJoinPoint(final ProceedingJoinPoint point, Permission permission) throws Throwable {
        // 先定义一个上下文操作创建
        Context context = null;
        // thisObject == null 环境有问题
        final Object thisObject = point.getThis(); 
        // context初始化
        if (thisObject instanceof Context) {
            context = (Context) thisObject;
        } else if (thisObject instanceof Fragment) {
            context = ((Fragment) thisObject).getActivity();
        }

        // 判断是否为null
        if (null == context || permission == null) {
            throw new IllegalAccessException("null == context || permission == null is null");
        }
        final Context finalContext = context;

        // 调用空白的 Activity 申请权限;
        // 通过 Aspectj 拿到了 @Permission(value = Manifest.permission.READ_EXTERNAL_STORAGE, requestCode = 200) 注解,并获取注解上的所有信息;
        PermissionActivity.requestPermissionAction(context, permission.value(), permission.requestCode(), new IPermission() {
            // 已经授权
            @Override
            public void ganted() {
                // 申请成功
                try {
                    // 被 Permission 标记的函数,正常执行下去,不拦截,此时就会执行 testRequest 函数
                    point.proceed();
                } catch (Throwable throwable) {
                    throwable.printStackTrace();
                }
            }

            @Override
            public void cancel() {
                // 被拒绝
                PermissionUtils.invokeAnnotion(thisObject, PermissionCancel.class);
            }

            @Override
            public void denied() {
                // 被拒绝(不再提醒的)
                PermissionUtils.invokeAnnotion(thisObject, PermissionDenied.class);
            }
        });
    }
}

Aspectj 是通过『动态修改字节码』来实现切面处理;本质就是:把切面函数的具体实现剪切到了注解标记的函数中;

复制代码
public void testRequest() {
    // 调用 空白的 Activity 申请权限
    MyPermissionActivity.requestPermissionAction(context, permission.value(), permission.requestCode(), new IPermission() {
       // 已经授权
       @Override
       public void ganted() {
           // 申请成功
           try {
               Toast.makeText(this, "权限申请成功...", Toast.LENGTH_SHORT).show();
           } catch (Throwable throwable) {
               throwable.printStackTrace();
           }
       }
       // 省略部分代码
    }                    
}

好了,权限管理就到这里吧

下一章预告


WMS 启动流程分析

欢迎三连


来都来了,点个关注,点个赞吧,你的支持是我最大的动力~~~

相关推荐
非凡ghost18 分钟前
LSPatch官方版:无Root Xposed框架,自由定制手机体验
android·智能手机·软件需求
_extraordinary_18 分钟前
MySQL 库的操作 -- 增删改查,备份和恢复,系统编码
android·mysql·oracle
西瓜本瓜@3 小时前
在Android中如何使用Protobuf上传协议
android·java·开发语言·git·学习·android-studio
似霰6 小时前
安卓adb shell串口基础指令
android·adb
fatiaozhang95278 小时前
中兴云电脑W102D_晶晨S905X2_2+16G_mt7661无线_安卓9.0_线刷固件包
android·adb·电视盒子·魔百盒刷机·魔百盒固件
CYRUS_STUDIO9 小时前
Android APP 热修复原理
android·app·hotfix
鸿蒙布道师10 小时前
鸿蒙NEXT开发通知工具类(ArkTs)
android·ios·华为·harmonyos·arkts·鸿蒙系统·huawei
鸿蒙布道师10 小时前
鸿蒙NEXT开发网络相关工具类(ArkTs)
android·ios·华为·harmonyos·arkts·鸿蒙系统·huawei
大耳猫10 小时前
【解决】Android Gradle Sync 报错 Could not read workspace metadata
android·gradle·android studio
ta叫我小白10 小时前
实现 Android 图片信息获取和 EXIF 坐标解析
android·exif·经纬度