Android关于安装应用的那些事

10,918 阅读3分钟

最近遇到了个要安装自己的应用的要求,趁现在空闲正好可以来总结一下。

权限

在开始前,先说一下需要的权限。

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<!--INSTALL_PACKAGES是针对于系统应用的-->
<uses-permission android:name="android.permission.INSTALL_PACKAGES" />

特别的,如果是静默安装,则需要INSTALL_PACKAGES权限(注意:INSTALL_PACKAGES权限是针对于系统应用的,换言之,想要实现静默安装,那你得要是系统应用)

还有请注意,从8.0开始,安装应用需要REQUEST_INSTALL_PACKAGES权限,其需要动态申请,而从6.0开始,READ_EXTERNAL_STORAGE权限也是要动态申请的。

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    List<String> deniedPermissions = new ArrayList<>();
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        if (!getPackageManager().canRequestPackageInstalls()){
            deniedPermissions.add(Manifest.permission.REQUEST_INSTALL_PACKAGES);
        }
    }
    if (ContextCompat.checkSelfPermission(MainActivity.this,
            Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED){
        deniedPermissions.add(Manifest.permission.READ_EXTERNAL_STORAGE);
    }
    if (ContextCompat.checkSelfPermission(MainActivity.this,
            Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED){
        deniedPermissions.add(Manifest.permission.WRITE_EXTERNAL_STORAGE);
    }
    if (deniedPermissions.size() > 0){
        requestPermissions(deniedPermissions.toArray(new String[]{}),100);
        return;
    }
}
apkInstall(apkAbsolutePath);

权限请求结果回调:

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    for (int i = 0;i < grantResults.length;i++) {
        if (grantResults[i] != PackageManager.PERMISSION_GRANTED) {
            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
                if (Manifest.permission.REQUEST_INSTALL_PACKAGES.equals(permissions[i])){
                    //引导用户去手动开启权限
                    Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES);
                    startActivityForResult(intent, 120);
                }
            }
            Toast.makeText(this, "权限不足!", Toast.LENGTH_SHORT).show();
            return;
        }
    }
    apkInstall(apkAbsolutePath);
}

普通安装

普通安装实际上是调起系统的 PackageInstaller 应用来进行安装,代码如下:

public void apkInstall(String apkAbsolutePath) {
    Log.d(TAG, "apkInstall, path = " + apkAbsolutePath);
    if(!TextUtils.isEmpty(apkAbsolutePath)) {
        File apkFile = new File(apkAbsolutePath);
        if(apkFile.exists()) {
            Log.d(TAG, "apkInstall, default mode");
            Uri apkUri = null;
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
                //获取uri方式一
                apkUri = FileProvider.getUriForFile(context,"com.qkun.installapplication.fileprovider", apkFile);
                //获取uri方式二
                //apkUri = getContentUri(apkAbsolutePath);
            } else {
                apkUri = Uri.fromFile(apkFile);
            }
            Log.d(TAG, "apkUri==" + apkUri);
            if (apkUri != null) {
                Intent intent = new Intent(Intent.ACTION_VIEW);
                intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
                //从9.0开始,不能直接从非Activity环境(如Service,BroadcastReceiver)中启动Activity,需要加这个flag
                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                //表示对目标应用临时授权该Uri所代表的文件
                intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
                context.startActivity(intent);
            }
        } else {
            Toast.makeText(context, "安装包不存在!", Toast.LENGTH_SHORT).show();
        }
    }
}

private Uri getContentUri(String fileAbsolutePath) {
    Uri contentUri = null;
    if(fileAbsolutePath != null && !"".equals(fileAbsolutePath)){
        Uri baseUri = MediaStore.Files.getContentUri("external");
        String[] projection = { MediaStore.MediaColumns._ID };
        String selection = MediaStore.MediaColumns.DATA + " = ?";
        String[] selectionArgs = new String[]{ fileAbsolutePath };
        Cursor cursor = null;
        String providerPackageName = "com.android.providers.media.MediaProvider";
        context.grantUriPermission(providerPackageName, baseUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
        Log.d(TAG, "getContentUri, baseUri = " + baseUri +
                ", projection = " + Arrays.toString(projection) +
                ", selection = " + selection +
                ", selectionArgs = " + Arrays.toString(selectionArgs));
        try {
            cursor = context.getContentResolver().query(baseUri,
                    projection,
                    selection,
                    selectionArgs,
                    null);
            if (cursor != null && cursor.moveToNext()) {
                int type = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID));
                if (type != 0) {
                    long id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID));
                    Log.d(TAG, "getContentUri, item id = " + id);
                    contentUri =  Uri.withAppendedPath(baseUri, String.valueOf(id));
                }
            }
        } catch (Exception e) {
            Log.e(TAG, Log.getStackTraceString(e));
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
    }
    Log.d(TAG, "getContentUri, contentUri = " + contentUri);
    return contentUri;
}

这里需要注意的是uri的获取,在7.0之前是可以用 Uri.fromFile(file) 获取的,而之后就需要使用 FileProvider 了,我这里提供了两种方式,看自己喜欢那种吧,第一种是要自己定义一个 FileProvider(具体怎么定义,这里就不展开了),第二种是用内容解析者获取到id后,根据baseUri拼接成我们需要的uri。

静默安装

静默安装主要过程是通过 context.getPackageManager() 获取到 PackageManager 后,调用 getPackageInstaller() 方法得到 PackageInstaller,然后实例化一个 PackageInstaller.SessionParams 对象,并根据该 sessionParams 对象创建一个 Session,最后将 apk 文件输入到该 session,设置回调并commit即可,代码如下:

private final class SilentInstallApkAsyncTask extends AsyncTask<File, Void, Boolean> {

    @Override
    protected Boolean doInBackground(File... params) {
        Log.d(TAG, "apkInstall, silent mode");
        if (params != null && params.length > 0){
            File apkFile = params[0];
            if (apkFile != null && apkFile.exists()){
                PackageInstaller packageInstaller = context.getPackageManager().getPackageInstaller();
                PackageInstaller.SessionParams sessionParams =
                        new PackageInstaller.SessionParams(
                                PackageInstaller.SessionParams.MODE_FULL_INSTALL);
                sessionParams.setSize(apkFile.length());

                PackageInstaller.Session session = null;
                try {
                    //根据 sessionParams 创建 Session
                    int sessionId = packageInstaller.createSession(sessionParams);
                    session = packageInstaller.openSession(sessionId);
                    //将 apk 文件输入 session
                    if (readApkFileToSession(session, apkFile)) {
                        //提交 session,并且设置回调
                        try {
                            Intent intent = new Intent(InstallResultBroadcastReceiver.ACTION_INSTALL_RESULT);
                            intent.setComponent(new ComponentName(context.getPackageName(),InstallResultBroadcastReceiver.class.getName()));
                            PendingIntent pendingIntent = PendingIntent.getBroadcast(context,
                                    1100,
                                    intent,
                                    PendingIntent.FLAG_UPDATE_CURRENT);
                            session.commit(pendingIntent.getIntentSender());
                            Log.d(TAG,"starting install apk");
                            return true;
                        } catch (Exception e) {
                            Log.e(TAG,Log.getStackTraceString(e));
                        }
                    } else {
                        Log.e(TAG,"read apk file is failed!");
                    }
                } catch (Exception e) {
                    Log.e(TAG,Log.getStackTraceString(e));
                } finally {
                    if (session != null) {
                        session.close();
                    }
                }
            }
        }
        return false;
    }

    @Override
    protected void onPostExecute(Boolean success) {
        Toast.makeText(context, success ? "开始安装。。。" : "安装失败!", Toast.LENGTH_SHORT).show();
    }

    private boolean readApkFileToSession(PackageInstaller.Session session, File apkFile) {
        OutputStream outputStream = null;
        FileInputStream fileInputStream = null;
        try {
            outputStream = session.openWrite(apkFile.getName(), 0, apkFile.length());
            fileInputStream = new FileInputStream(apkFile);
            int read;
            byte[] buffer = new byte[1024 * 1024];
            while ((read = fileInputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, read);
            }
            session.fsync(outputStream);
            fileInputStream.close();
            return true;
        } catch (Exception e) {
            Log.e(TAG,Log.getStackTraceString(e));
        } finally {
            if (fileInputStream != null){
                try {
                    fileInputStream.close();
                } catch (IOException e) {
                    Log.e(TAG,Log.getStackTraceString(e));
                }
            }
            if (outputStream != null){
                try {
                    outputStream.close();
                } catch (IOException e) {
                    Log.e(TAG,Log.getStackTraceString(e));
                }
            }
        }
        return false;
    }
}

监听安装结果的广播接收器:

public class InstallResultBroadcastReceiver extends BroadcastReceiver {

    public static final String ACTION_INSTALL_RESULT = "com.qkun.ACTION_INSTALL_RESULT";

    @Override
    public void onReceive(Context context, Intent intent) {
        if (ACTION_INSTALL_RESULT.equals(intent.getAction())){
            int status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE);
            Toast.makeText(context, status == PackageInstaller.STATUS_SUCCESS ?
                            "静默安装成功!" :
                            "静默安装失败!",
                    Toast.LENGTH_SHORT).show();
        }
    }

}

在 AndroidManifest.xml 注册它:

<receiver android:name=".InstallResultBroadcastReceiver" android:enabled="true">
    <intent-filter>
        <action android:name="com.qkun.ACTION_INSTALL_RESULT"/>
    </intent-filter>
</receiver>

执行安装:

public void apkInstall(String apkAbsolutePath,boolean isDefault) {
    Log.d(TAG, "apkInstall, path = " + apkAbsolutePath);
    if(!TextUtils.isEmpty(apkAbsolutePath)) {
        File apkFile = new File(apkAbsolutePath);
        if(apkFile.exists()) {
            new SilentInstallApkAsyncTask().execute(apkFile);
        } else {
            Toast.makeText(context, "安装包不存在!", Toast.LENGTH_SHORT).show();
        }
    }
}