RK3576-Android15-整合PackageInstaller实现静默安装功能

提示:Android15-整合PackageInstaller实现静默安装功能

文章目录


前言

在RK 平台 Android15 上面做静默安装功能

一、 需求

在Android15 上面实现静默安装功能,不要用脚本实现,借助包安装器PackageInstaller 来实现。

二、参考资料

MTK-Android13-包安装器PackageInstaller 静默安装实现

本文笔记完全参考之前知识点 MTK平台Android13 版本思想,在安装器App-PackageInstaller 基础上更改部分流程实现静默安装功能。

  • 可以参考、copy 全部代码:思想完全一样的,借助安装器,添加自己的业务代码,客户是否静默安装看客户自己选择。
  • 以前是Android13 且 MTK平台,在Android15 上面部分API、机制、权限有区别,适当几个点适配下即可,本章会后面分析会做简单介绍。

Android15-解决后台无法启动应用App问题 静默安装成功后 有 是否直接启动的参数,这个业务在Android15 中无法后台启动App,这里有修改方案


针对PM 安装流程、PackageInstaller 安装流程,如果有需要强烈建议如下知识篇 相关知识点。 对 安装流程、PackageInstaller 安装等知识面和业务流程有非常全面的介绍。

RK-Android11-PackageInstaller安装器自动安装功能实现 魔改PackageInstaller 去掉各种状态跳转,自定义一个界面一个带进度界面安装实现。
Rk3576-Android15-Apk 安装基本知识点和Apk安装拦截功能实现 Framework层 安装过程流程分析
拦截adb install/uninstall安装 - 安装流程分析 命令脚本安装流程分析
PMS安装apk之界面跳转 这个是对 PackageInstaller 安装器安装流程,界面跳转流程分析

三、文件修改

包安装前 PackageInstaller 目录:/frameworks/base/packages/PackageInstaller
新增文件:

java 复制代码
src/com/android/packageinstaller/SilenceInstallManager.java
src/com/android/packageinstaller/SilenceInstallReceiver.java

修改文件:

java 复制代码
 AndroidManifest.xml
 src/com/android/packageinstaller/InstallStart.java
 src/com/android/packageinstaller/PackageInstallerApplication.java
 src/com/android/packageinstaller/UninstallerActivity.java

四、实现方案

1、InstallStart - onCreate 方法中-判断是否拦截进行静默安装-拷贝文件

InstallStart 入口onCreate 方法中,拦截,是否进行静默安装,如果是,那么就进行静默安装逻辑,拷贝文件。拦截正常的安装流程。

java 复制代码
   // modify by fangchen start
        // if silence install  ->to silence install
        if (intent.getBooleanExtra(SilenceInstallReceiver.SILENCE_INSTALL_KEY, false)) {
            Log.d(TAG," StagingAsyncAppTask  to  execute ");
            StagingAsyncAppTask mStagingTask = new StagingAsyncAppTask(intent.getBooleanExtra(SilenceInstallReceiver.IS_LAUNCH_KEY, false));
            mStagingTask.execute(getIntent().getData());
            return;
        }
        // modify by fangchen end

拷贝 文件资源代码如下:

java 复制代码
 @SuppressLint("NewApi")
    private final class StagingAsyncAppTask extends AsyncTask<Uri, Void, File> {

        private boolean mIsLaunch;

        public StagingAsyncAppTask(boolean isLaunch){
            mIsLaunch = isLaunch;
        }


        @Override
        protected File doInBackground(Uri... params) {
            Log.d(TAG, "===========copy file from user app start");
            if (params == null || params.length <= 0) {
                return null;
            }
            Uri packageUri = params[0];
            try (InputStream in = getContentResolver().openInputStream(packageUri)) {
                // Despite the comments in ContentResolver#openInputStream the returned stream can
                // be null.
                if (in == null) {
                    return null;
                }

                File mStagedFile = TemporaryFileManager.getStagedFile(InstallStart.this);

                try (OutputStream out = new FileOutputStream(mStagedFile)) {
                    byte[] buffer = new byte[1024 * 1024];
                    int bytesRead;
                    while ((bytesRead = in.read(buffer)) >= 0) {
                        // Be nice and respond to a cancellation
                        out.write(buffer, 0, bytesRead);
                    }
                }
                return mStagedFile;
            } catch (IOException | SecurityException | IllegalStateException e) {
                Log.w(TAG, "Error staging apk from content URI", e);
            }
            return null;
        }

        @Override
        protected void onPostExecute(File installFile) {
            if (null != installFile) {
                // Now start the installation again from a file
                Log.d(TAG, "copy file from user app finish");

                Intent installIntent = new Intent(SilenceInstallReceiver.SILENCE_INSTALL_APP);
                installIntent.putExtra(SilenceInstallReceiver.APP_URI_KEY, Uri.fromFile(installFile));
                installIntent.putExtra(SilenceInstallReceiver.IS_LAUNCH_KEY, mIsLaunch);
                installIntent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
                installIntent.setPackage("com.android.packageinstaller");
                sendBroadcast(installIntent);

                Log.d(TAG, "send to install");
            } else {
                Log.d(TAG, "copy file from user app fail");
            }

            finish();
        }
    }

2、SilenceInstallReceiver 接收静默安装广播请求-进行静默安装业务

如上,在进行安装初期InstallStart 中 判断当前进行静默后,广播开始通知进行静默安装

java 复制代码
package com.android.packageinstaller;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;


public class SilenceInstallReceiver extends BroadcastReceiver {
    public static final String SILENCE_INSTALL_APP = "com.android.packageinstaller.ACTION_SILENCE_INSTALL";
    public static final String SILENCE_INSTALL_KEY = "silence_install";
    public static final String IS_LAUNCH_KEY = "is_launch";
    public static final String APP_URI_KEY = "app_uri";
    private static final String TAG = "SilenceInstallReceiver";


    @Override
    public void onReceive(Context context, Intent intent) {
		Log.w(TAG, "SilenceInstallReceiver getAction---->" + intent.getAction());

        if (SILENCE_INSTALL_APP.equals(intent.getAction())) {
            Uri uri = intent.getParcelableExtra(APP_URI_KEY);
            boolean isLaunch = intent.getBooleanExtra(IS_LAUNCH_KEY, false);
            SilenceInstallManager.getInstance(context).silenceInstall(uri, isLaunch);
        }

    }

}

3、静默安装核心工具类-SilenceInstallManager

假使对安装流程有相关的了解和掌握,我个人经验就是两点:

  • 文件的拷贝,uri 的传递,按照规范传递参数拼凑数据
  • 进行Session 处理,提交 到Framework 层 进行安装。
  • 对于安装回调进度,注册Session回调而已
java 复制代码
package com.android.packageinstaller;

import android.annotation.SuppressLint;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.Message;
import android.util.ArrayMap;
import android.util.Log;

import androidx.annotation.NonNull;
import android.content.ActivityNotFoundException;


import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
import android.os.ParcelFileDescriptor;
import android.content.res.AssetFileDescriptor;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.content.ActivityNotFoundException;


final class SilenceInstallManager {
    private static final String TAG = "SilenceInstallManager";

    private static final int MSG_WHAT_INSTALL_FINISH_SUCCESS = 0;
    private static final int MSG_WHAT_INSTALL_FINISH_FAIL = 1;
    private static final int MSG_WHAT_UNINSTALL_COMPLETE = 2;

    private Context mContext;

    @SuppressLint("NewApi")
    private ArrayMap<Integer, InstallAppInfo> InstallAppInfoMap = new ArrayMap<>();
    private @Nullable PackageInstaller mInstaller;


    private static volatile SilenceInstallManager INSTANCE;

    private SilenceInstallManager(Context context) {
        mContext = context;
        mInstaller = mContext.getPackageManager().getPackageInstaller();


    }

    public static SilenceInstallManager getInstance(Context context) {
        if (null == INSTANCE) {
            synchronized (SilenceInstallManager.class) {
                if (null == INSTANCE) {
                    INSTANCE = new SilenceInstallManager(context.getApplicationContext());
                }
            }
        }
        return INSTANCE;
    }


    @SuppressLint("NewApi")
    private PackageInstaller.SessionCallback mSessionCallback = new PackageInstaller.SessionCallback() {
        @Override
        public void onCreated(int sessionId) {
            Log.d(TAG, "onCreated---->" + sessionId);
        }

        @Override
        public void onBadgingChanged(int sessionId) {
             Log.w(TAG, "mSessionCallback onBadgingChanged---->" + sessionId);
        }

        @Override
        public void onActiveChanged(int sessionId, boolean active) {
             Log.w(TAG, "mSessionCallback onActiveChanged---->" + sessionId + "  active--->" + active);
        }

        @Override
        public void onProgressChanged(int sessionId, float progress) {
             Log.w(TAG, "mSessionCallback onProgressChanged---->" + sessionId + "  progress--->" + progress);
        }

        @Override
        public void onFinished(int sessionId, boolean success) {
            Log.d(TAG, "onFinished---->" + sessionId + "  success--->" + success);
            Message msg = Message.obtain();
            msg.what = MSG_WHAT_INSTALL_FINISH_SUCCESS;
            msg.arg1 = sessionId;
            msg.obj = success;
            mHandler.sendMessage(msg);
        }
    };

    @SuppressLint("HandlerLeak")
    private Handler mHandler = new Handler() {
        @Override
        public void dispatchMessage(@NonNull Message msg) {
            mInstaller.unregisterSessionCallback(mSessionCallback);
            if (msg.what == MSG_WHAT_INSTALL_FINISH_SUCCESS) {
                boolean result = (boolean) msg.obj;
                int sessionId = msg.arg1;
                InstallAppInfo info = InstallAppInfoMap.remove(sessionId);
                if (result) {
                    Log.d(TAG, "install success");
                    if (null != info) {
                        if (info.isLaunch && null != info.info && null != info.info.packageName && !"".equals(info.info.packageName)) {
                            launchApp(info.info.packageName);
                        }

                        File f = new File(info.filePath);
                        if (f.exists()) {
                            f.delete();
                        }
                    }
                } else {
                    Log.d(TAG, "install fail===========111111111111111===");
                }
            } else if (msg.what == MSG_WHAT_INSTALL_FINISH_FAIL) {
                int sessionId = msg.arg1;
                if (sessionId != -1) {
                    InstallAppInfoMap.remove(sessionId);
                }
                Log.d(TAG, "install fail===========00000000000000000===");
            } else if (msg.what == MSG_WHAT_UNINSTALL_COMPLETE) {
                Log.d(TAG, "uninstall complete--->" + msg.arg1);
                if (msg.arg1 == PackageManager.DELETE_SUCCEEDED) {
                    Log.d(TAG, "delete succeeded");
                } else {
                    Log.d(TAG, "delete fail");
                }
            }
        }
    };

   // public void silenceInstall(String appFilePath, boolean launch) {
    public void silenceInstall(Uri uri,  boolean launch) {

        String appFilePath=uri.getPath();
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
            mInstaller.registerSessionCallback(mSessionCallback);
            PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
                    PackageInstaller.SessionParams.MODE_FULL_INSTALL);
            params.setInstallAsInstantApp(false);
            params.setInstallReason(PackageManager.INSTALL_REASON_USER);
            File file = new File(appFilePath);
            if (!file.exists()) {
                Log.d(TAG,"=====silenceInstall wenjian not exists======appFilePath:"+appFilePath);
                sendFailMsg(-1);
                return;
            }

            final AssetFileDescriptor afd = openAssetFileDescriptor(uri);

            final Uri packageUri =uri;// getIntent().getData();

            ParcelFileDescriptor pfd = afd != null ? afd.getParcelFileDescriptor() : null;


            if (pfd != null) {
               Log.d(TAG,"===========pfd != null=================");
                try {


                  String debugPathName=  packageUri.toString();

                    final PackageInstaller.InstallInfo result = mInstaller.readInstallInfo(pfd,
                            debugPathName, 0);


                    params.setAppPackageName(result.getPackageName());
                    params.setInstallLocation(result.getInstallLocation());
                    params.setSize(result.calculateInstalledSize(params, pfd));
                    params.setSize(file.length());
                }catch (Exception e) {
                    Log.e(TAG, "Cannot parse package " + file + ". Assuming defaults.");
                    Log.e(TAG,
                            "Cannot calculate installed size " + file + ". Try only apk size.");
                    params.setSize(file.length());
                }
            }else{
                Log.d(TAG,"===========pfd  ====== null=================");
            }
            try {

                PackageInfo mPkgInfo = PackageUtil.getPackageInfo(mContext, file, PackageManager.GET_PERMISSIONS);
                int mSessionId =mInstaller.createSession(params);
                InstallAppInfo installAppInfo = new InstallAppInfo(mSessionId, appFilePath, mPkgInfo, launch);
                InstallAppInfoMap.put(mSessionId, installAppInfo);
                InstallingAsyncTask mInstallingTask = new InstallingAsyncTask(mContext, appFilePath, mSessionId);
                mInstallingTask.execute();
            } catch (IOException e) {
                e.printStackTrace();
                Log.d(TAG,"============InstallingAsyncTask execute failed ==============");
                sendFailMsg(-1);
            }
        }
    }

    public  AssetFileDescriptor openAssetFileDescriptor(Uri uri) {
        try {
            return  mContext.getContentResolver().openAssetFileDescriptor(uri, "r");
        } catch (Exception e) {
            Log.d(TAG, "========Failed to open asset file descriptor", e);
            return null;
        }
    }


    private void sendFailMsg(int sessionId) {
        Message msg = Message.obtain();
        msg.what = MSG_WHAT_INSTALL_FINISH_FAIL;
        msg.arg1 = sessionId;
        mHandler.sendMessage(msg);
    }


    @SuppressLint("NewApi")
    private final class InstallingAsyncTask extends AsyncTask<Void, Void,
            PackageInstaller.Session> {

        private Context mContext;
        private String mAppPath;
        private int mSessionId;

        public InstallingAsyncTask(Context context, String appPath, int sessionId) {
            mContext = context;
            mAppPath = appPath;
            mSessionId = sessionId;
        }

        @Override
        protected PackageInstaller.Session doInBackground(Void... params) {
            PackageInstaller.Session session;
            try {
                session = mInstaller.openSession(mSessionId);
            } catch (IOException e) {
                return null;
            }

            session.setStagingProgress(0);

            try {
                File file = new File(mAppPath);
                long totalRead = 0;

                try (InputStream in = new FileInputStream(file)) {
                    long sizeBytes = file.length();
                    try (OutputStream out = session
                            .openWrite("PackageInstaller", 0, sizeBytes)) {
                        byte[] buffer = new byte[1024 * 1024];
                        while (true) {
                            int numRead = in.read(buffer);

                            if (numRead == -1) {
                                session.fsync(out);
                                break;
                            }

                            if (isCancelled()) {
                                session.close();
                                break;
                            }

                            out.write(buffer, 0, numRead);
                            if (sizeBytes > 0) {
                               /* float fraction = ((float) numRead / (float) sizeBytes);
                                session.addProgress(fraction);*/

                                totalRead += numRead;
                                float fraction = ((float) totalRead / (float) sizeBytes);
                                session.setStagingProgress(fraction);
                             //   publishProgress((int) (fraction * 100.0));

                            }
                        }
                    }
                }

                return session;
            } catch (IOException | SecurityException e) {
                Log.e(TAG, "Could not write package", e);
                session.close();
                return null;
            }
        }

        @Override
        protected void onPostExecute(PackageInstaller.Session session) {
            if (session != null) {
                Intent broadcastIntent = new Intent();
                PendingIntent pendingIntent = PendingIntent.getBroadcast(
                        mContext,
                        1,
                        broadcastIntent,
                       // PendingIntent.FLAG_UPDATE_CURRENT);
                      //  PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
                      //   PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE
						 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE |PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT
						 );

                session.commit(pendingIntent.getIntentSender());

                session.close();

                Log.d(TAG, "send install PendingIntent----->");
            } else {
                mInstaller.abandonSession(mSessionId);

                Log.d(TAG,"============session error =======mSessionId");


                sendFailMsg(mSessionId);

                File f = new File(mAppPath);
                if (f.exists()) {
                    f.delete();
                }

                Log.e(TAG, "copy fail delete file----->");
            }

            mContext = null;
            mAppPath = "";
            mSessionId = -1;
        }
    }


    private class InstallAppInfo {
        private int sessionId;
        private String filePath;
        private PackageInfo info;
        private boolean isLaunch;

        public InstallAppInfo(int sessionId, String filePath, PackageInfo info, boolean isLaunch) {
            this.sessionId = sessionId;
            this.filePath = filePath;
            this.info = info;
            this.isLaunch = isLaunch;
        }

        public int getSessionId() {
            return sessionId;
        }

        public String getFilePath() {
            return filePath;
        }

        public PackageInfo getInfo() {
            return info;
        }

        public boolean isLaunch() {
            return isLaunch;
        }
    }


    private void launchApp(String appPackageName) {
        Log.d(TAG,"=======launchApp========appPackageName:"+appPackageName);
        Intent mLaunchIntent = mContext.getPackageManager().getLaunchIntentForPackage(appPackageName);
        if (mLaunchIntent != null) {
            Log.d(TAG, "launch app--->  appPackageName:"+appPackageName);
            if (mLaunchIntent.resolveActivity(mContext.getPackageManager()) != null) {
                Log.d(TAG, "launch app--->  startActivity ");
                try {
                    mLaunchIntent.addFlags(
                            Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
                    mContext.startActivity(mLaunchIntent);
                } catch (ActivityNotFoundException | SecurityException e) {
                    Log.e(TAG, "Could not start activity", e);
                }
            } else {
                Log.e(TAG, "No activity found to handle the intent for: " + appPackageName);
            }
        }else{
            Log.d(TAG,"===============qidong failed========");
        }
    }


    public void silenceUninstall(String packageName) {
        Log.i(TAG, "silenceUninstall--->" + packageName);
       // PackageDeleteObserver observer = new PackageDeleteObserver();
       // mContext.getPackageManager().deletePackage(packageName, observer, PackageManager.DELETE_ALL_USERS);
    }

   /* private class PackageDeleteObserver extends IPackageDeleteObserver.Stub {
        public void packageDeleted(String packageName, int returnCode) {
            Message msg = mHandler.obtainMessage(MSG_WHAT_UNINSTALL_COMPLETE);
            msg.arg1 = returnCode;
            msg.obj = packageName;
            mHandler.sendMessage(msg);
        }
    }*/
}

4、PackageInstallerApplication 初始化静默安装工具

java 复制代码
SilenceInstallManager.getInstance(this);

5、AndroidManifest 配置 广播组件

java 复制代码
   <!-- modify by fangchen start  -->
        <receiver android:name=".SilenceInstallReceiver"
            android:exported="true">
            <intent-filter android:priority="1">
                <action android:name="com.android.packageinstaller.ACTION_SILENCE_INSTALL" />
            </intent-filter>
        </receiver>
        <!-- modify by fangchen end  -->

五、实验验证-进行静默安装测试

这里可以传递两个参数:

  • 是否静默安装 silence_install,如果否 则走默认的安装器安装方式
  • 安装成功后是否自启动 is_launch

备注说明:在实际业务中,如果下载成功 软件包 文件 apk,后直接调用当前业务代码即可实现。

java 复制代码
val aPath ="/data/data/com.fish.togetsn/files" + File.separator + "bilibili.apk"
            Log.d(TAG," aPath path:"+aPath);
            val appFile = File(aPath)
            if (!appFile.exists()) {
                //showToast("请在" + getExternalFilesDir(null)!!.absolutePath + File.separator + "目录中放置升级文件")
                ToastUtils.showShort("请 配置文件")
                return@setOnClickListener
            }
            //大于7.0使用此方法
            val apkUri = FileProvider.getUriForFile(
                ContextProvider.get().context,
                "com.fish.togetsn.fileProvider",
                appFile
            )

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                val installApkIntent = Intent()
                installApkIntent.setAction(Intent.ACTION_VIEW)
                installApkIntent.addCategory(Intent.CATEGORY_DEFAULT)
                installApkIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                installApkIntent.setDataAndType(apkUri, "application/vnd.android.package-archive")
                installApkIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
                //设置静默安装标识
                installApkIntent.putExtra("silence_install", true)
                installApkIntent.setPackage("com.android.packageinstaller")
                //设置安装完成是否启动标识
                installApkIntent.putExtra("is_launch", true)
                //设置后台中启动activity标识
               // installApkIntent.putExtra("allowed_Background", true)
                if (packageManager.queryIntentActivities(installApkIntent, 0).size > 0) {
                    startActivity(installApkIntent)
                }
            }

六、拓展思路-讨论

  • 这里模仿的是下载软件成功后,借用 系统自带安装前PackageInstaller 实现。 其中拷贝文件是在入口InstallStart 这个Activity中实现的,那么实际上 我自己把这部分代码实现,也就是拷贝文件实现,直接发送广播给安装器进行安装业务不行吗?
  • 如上 思考,我们使用了PackageInstaller 的入口InstallStart 这个Activity,屏幕会闪一下,实际过程中这个如果避免达到无感?
  • 这里都是在借助PackageInstaller 这个包实现的,比如我们设置com.android.packageinstaller 指定的包名进行application/vnd.android.package-archive 业务,实际上 静默安装和非静默安装混在一起的, 那么就不能分离开来直接在Framework层的Service 服务中实现吗? 这里先给出答案,绝对可行。
  • 其实静默安装 不管哪一个版本都有自己的一套,比如开机第一次 会内置apk, 把这个业务流程单独提出来不也是一个思路吗。

总结

  • 这里进行了Android15 ,RK平台 静默安装功能,针对以前的知识点再次实践而已,这里面涉及到一些API变化、规则变化,参考PackageInstaller 业务代码和流程实现。
  • 静默安装核心要点还是session 的传递、提交、校验 等。 分为两部分:前端和Framework层,每个知识点都很重要,建议多研究代码,会收获很多。
相关推荐
不屈的铝合金10 个月前
Oracle常用响应文件介绍(19c)
数据库·oracle·静默安装·响应文件
梦幻加菲猫2 年前
CentOS 7静默安装Oracle 11g(记一次最小化CentOS 7安装Oracle 11g的经历)
oracle·静默安装·centos 7
BruceGerGer2 年前
flutter开发实战-应用更新apk下载、安装apk、启动应用实现
android·flutter·apk下载·apk安装·apk启动·dio·静默安装