最近遇到了個要安裝本身的應用的要求,趁如今空閒正好能夠來總結一下。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();
}
}
}
複製代碼