Android關於安裝應用的那些事

最近遇到了個要安裝本身的應用的要求,趁如今空閒正好能夠來總結一下。android

權限

在開始前,先說一下須要的權限。bash

<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權限是針對於系統應用的,換言之,想要實現靜默安裝,那你得要是系統應用)session

還有請注意,從8.0開始,安裝應用須要REQUEST_INSTALL_PACKAGES權限,其須要動態申請,而從6.0開始,READ_EXTERNAL_STORAGE權限也是要動態申請的。app

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);
複製代碼

權限請求結果回調:ide

@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 應用來進行安裝,代碼以下:ui

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。this

靜默安裝

靜默安裝主要過程是經過 context.getPackageManager() 獲取到 PackageManager 後,調用 getPackageInstaller() 方法獲得 PackageInstaller,而後實例化一個 PackageInstaller.SessionParams 對象,並根據該 sessionParams 對象建立一個 Session,最後將 apk 文件輸入到該 session,設置回調並commit便可,代碼以下:spa

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;
    }
}
複製代碼

監聽安裝結果的廣播接收器:code

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 註冊它: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();
        }
    }
}
複製代碼
相關文章
相關標籤/搜索