如何应对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 启动流程分析

欢迎三连


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

相关推荐
Mr Lee_12 分钟前
android 配置鼠标右键快捷对apk进行反编译
android
顾北川_野1 小时前
Android CALL关于电话音频和紧急电话设置和获取
android·音视频
&岁月不待人&1 小时前
Kotlin by lazy和lateinit的使用及区别
android·开发语言·kotlin
Winston Wood3 小时前
Android Parcelable和Serializable的区别与联系
android·序列化
清风徐来辽3 小时前
Android 项目模型配置管理
android
帅得不敢出门3 小时前
Gradle命令编译Android Studio工程项目并签名
android·ide·android studio·gradlew
problc4 小时前
Flutter中文字体设置指南:打造个性化的应用体验
android·javascript·flutter
帅得不敢出门14 小时前
安卓设备adb执行AT指令控制电话卡
android·adb·sim卡·at指令·电话卡
我又来搬代码了16 小时前
【Android】使用productFlavors构建多个变体
android
德育处主任18 小时前
Mac和安卓手机互传文件(ADB)
android·macos