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

前言


image.png

本章我们继续上一章节,讲解 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 启动流程分析

欢迎三连


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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/598194.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【JavaWeb】网上蛋糕项目商城-注册,登录,修改用户信息,提交订单

概念 通过以上多篇文章的讲解&#xff0c;对该项目的功能已经实现了很多&#xff0c;本文将对该项目的用户注册&#xff0c;登录&#xff0c;修改用户信息&#xff0c;以及用户添加至购物车的商品进行提交订单等功能的实现。 注册功能实现 点击head.jsp头部页面的注册按钮&a…

最新贷款市场报价利率(LPR)数据(1991-2024)

数据来源&#xff1a;东方财富网时间跨度&#xff1a;1991-2024年 数据范围&#xff1a;全国范围 数据指标&#xff1a; LPR_1Y利率(%) LPR_5Y利率(%) 中长期贷款利率:5年以上(%) 短期贷款利率:6个月至1年(含)(%) 日期 样例数据&#xff1a; 下载链接&#xff1a; ht…

win10 截图黑屏解决方法

win10 使用QQ截图等第三方工具截图黑屏&#xff0c;提示 Capturing screen is forbidden! 此时winshiftS截图也无法正常工作&#xff0c;解决方法如下&#xff1a; 参考地址&#xff1a;Redirecting

AHB---数据总线

1. 数据总线 为了实现AHB系统&#xff0c;需要独立的读写数据总线。虽然推荐的最小数据总线宽度被指定为32位&#xff0c;但这可以根据数据总线宽度进行更改。 数据总线包含以下部分&#xff1a; HWDATAHRDATAEndianness&#xff08;字节序&#xff09; 1.1 HWDATA 在写传输…

MinimogWP WordPress 主题下载——优雅至上,功能无限

无论你是个人博客写手、创意工作者还是企业站点的管理员&#xff0c;MinimogWP 都将成为你在 WordPress 平台上的理想之选。以其优雅、灵活和功能丰富而闻名&#xff0c;MinimogWP 不仅提供了令人惊叹的外观&#xff0c;还为你的网站带来了无限的创作和定制可能性。 无与伦比的…

投资者悄然收购二手楼梯楼,在杭州豪掷巨资购买12套!

独家首发 -------------- 日前杭州中介流传&#xff0c;一名投资客大举收购二手楼梯楼&#xff0c;下手就是12套&#xff0c;显示出一些具有前瞻性眼光的投资者悄悄放弃电梯楼&#xff0c;选择了处于价格洼地的楼梯楼。 二手楼梯楼当下被严重低估&#xff0c;在一线城市的二手楼…

Tmux工具使用案例

Tmux工具使用案例 连接linux一般使用ssh&#xff0c;当ssh会话中需要长时间执行命令时&#xff0c;为了避免命令不受ssh会话影响&#xff0c;除了可以将命令通过nohup <cmd> &等方法放到后台执行外&#xff0c;也可以利用Tmux这个工具解绑SSH会话与执行命令&#xff…

Raft共识算法图二解释

下面是有关Raft协议中不同术语和概念的翻译及解释&#xff1a; 术语和概念&#xff1a; 任期号&#xff08;term number&#xff09;&#xff1a;用来区分不同的leader。前一个日志槽位的信息&#xff08;prelogIndex&#xff09;&#xff1a;这是前一个日志条目的索引&#…

关于二手车系统学习--登录模块

1.样式1-17行 <div class"cheader"><div style"width: 80%;margin: 0 auto;line-height: 50px;padding-top: 10px"><el-row><el-col:span"5"style"font-size: 20px;cursor: pointer;color: #00ae66;font-weight: bold…

苹果自研大语言模型“Ajax“ 助力iOS 18升级;Stack Overflow与OpenAI建立API合作伙伴关系

&#x1f989; AI新闻 &#x1f680; 苹果自研大语言模型"Ajax" 助力iOS 18升级 摘要&#xff1a;苹果公司预计通过自研大语言模型Ajax来为iOS 18和Siri带来重大升级&#xff0c;但不计划推出类似ChatGPT的AI聊天机器人。Ajax模型基于Google的Jax框架&#xff0c;并…

Android:弹出对话框方式梳理一览(一)

Android&#xff1a;弹出对话框方式梳理一览&#xff08;一&#xff09; Guide&#xff5c;导言 在Android开发中&#xff0c;对话框可能是我们与用户交互的非常常用的方式&#xff0c;包括弹出一个小界面&#xff0c;可能在实际场景中都非常实用。本篇文章主要就是对Android弹…

搭建Docker私有镜像仓库

大家好&#xff0c;今天给大家分享一下如何搭建私有镜像仓库&#xff0c;私有镜像仓库可以更好地管理和控制镜像的访问和使用&#xff0c;确保只有授权的人员能够获取和使用特定的镜像&#xff0c;而且方便团队内部共享定制化的镜像&#xff0c;提高开发和部署效率&#xff0c;…

MySQL 中的HASH详解

MySQL中的哈希索引&#xff08;Hash Index&#xff09;是一种特殊的数据库索引类型&#xff0c;它利用哈希表&#xff08;Hash Table&#xff09;的数据结构来存储索引项。哈希表通过哈希函数&#xff08;Hash Function&#xff09;将索引列的值转化为一个固定长度的哈希码&…

【目录】500 行或更少(500 Lines or Less)

AOSA 500 行或更少&#xff08;500 Lines or Less&#xff09;是《开源应用程序体系结构》(Architecture of Open Source Applications, AOSA)系列的第四卷。该系列的前三卷是关于大型程序必须解决的大问题&#xff0c;而本书专注于程序员在构建新事物时在小规模中做出的设计决…

数据结构链表

数据结构链表 链表 1&#xff09;链表的概念及结构: 链表是一种物理存储结构上非连续存储结构&#xff0c;数据元素的逻辑顺序是通过链表中的引用链接次序实现的。 2&#xff09;实际中链表的结构非常多样&#xff0c;以下情况组合起来就有8种链表结构&#xff1a; 单向、双向…

SAP PP模块学习提炼第一部分

SAP是ERP的一款软件。 SAP的入门困难&#xff1a; 听不懂&#xff0c;看不懂缺乏知识体系缺乏行业经验 SAP入门引导&#xff1a; 导师引导实战演练 SAP基础介绍 1.什么是SAP? System, Application and Products in Data Processing 即数据处理的系统、应用和产品。 2.…

7: 分配器

文章目录 operator new () 和 malloc()stl 中allocator的使用allocators 内部实现vc的分配器 并没有太多的技巧可言下面是BC5的stl 设计GCC的allocators2.9版本GCC使用的allocator是什么&#xff1f;这里面cookie的作用 4.9版本的GCC allocator operator new () 和 malloc() 所…

Redis系列之key过期策略介绍

为什么要有过期策略&#xff1f; Redis是一个内存型的数据库&#xff0c;数据是放在内存里的&#xff0c;但是内存也是有大小的&#xff0c;所以&#xff0c;需要配置redis占用的最大内存&#xff0c;主要通过maxmemory配置 maxmomory <bytes> # redis占用的最大内存官…

深入解析C#中的async和await关键字

文章目录 一、异步编程的基本概念及其在C#中的实现二、async关键字的定义及其用法三、await关键字的定义及其用法示例代码&#xff1a;使用async和await编写一个简单的异步程序 四、async和await的优点注意事项 五、C#下async和await中常见问题汇总1. 异步方法中的await调用2. …

CST电磁仿真软件远场源的导出调用和提取结果【小白必看】

远场源的导出&调用(1) 提取Hybrid仿真所需的远场源&#xff01; Post-Processing > Tools > Result Templates Tools >Farfield and Antenna Properties > Export Farfields As Source 混合求解(Hybrid Simulation)是对安装在舰船等大型平台上的天线进行仿真…
最新文章