一些产品要求APP在升级时能够实现静默安装,而无需弹出安装界面让用户确认。这里提出两种实现方案:
方案一:通过pm命令安装
APP调用『pm』命令实现静默安装,此方案无须修改Android源码,但需要root权限。实现如下:
/**
* Silent install
*
* @param path Package
* @return true: success false: failed
*/
public static boolean installSilent(String path) {
boolean result = false;
BufferedReader es = null;
DataOutputStream os = null;
try {
Process process = Runtime.getRuntime().exec("su");
os = new DataOutputStream(process.getOutputStream());
String command = "pm install -r " + path + "\n";
os.write(command.getBytes(Charset.forName("utf-8")));
os.flush();
os.writeBytes("exit\n");
os.flush();
process.waitFor();
es = new BufferedReader(new InputStreamReader(process.getErrorStream()));
String line;
StringBuilder builder = new StringBuilder();
while ((line = es.readLine()) != null) {
builder.append(line);
}
Log.d(TAG, "install msg is " + builder.toString());
/* Installation is considered a Failure if the result contains
the Failure character, or a success if it is not.
*/
if (!builder.toString().contains("Failure")) {
result = true;
}
} catch (Exception e) {
Log.e(TAG, e.getMessage(), e);
} finally {
try {
if (os != null) {
os.close();
}
if (es != null) {
es.close();
}
} catch (IOException e) {
Log.e(TAG, e.getMessage(), e);
}
}
return result;
}
方案二 修改PackageInstaller源码
如果没有root权限,方案一将无法实现,因此我们通过定制 PackageInstaller 来实现指定包名可以静默安装,并增加Intent参数来指定静默安装还是默认安装。具体修改如下:
diff --git a/packages/apps/PackageInstaller/src/com/android/packageinstaller/InstallStart.java b/packages/apps/Pac
old mode 100644
new mode 100755
index 12441b5..cbf8c41
--- a/packages/apps/PackageInstaller/src/com/android/packageinstaller/InstallStart.java
+++ b/packages/apps/PackageInstaller/src/com/android/packageinstaller/InstallStart.java
@@ -22,17 +22,30 @@ import android.app.ActivityManager;
import android.app.AppGlobals;
import android.app.IActivityManager;
import android.content.ContentResolver;
+import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageManager;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageParser;
+import android.content.pm.PackageUserState;
+import android.content.pm.ProviderInfo;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.RemoteException;
+import android.os.SystemProperties;
import android.support.annotation.Nullable;
+import android.text.TextUtils;
import android.util.Log;
+import android.content.pm.IPackageInstallObserver;
+import android.support.v4.content.FileProvider;
+
+import java.io.File;
+import java.lang.reflect.Method;
+import java.util.List;
import com.android.internal.annotations.VisibleForTesting;
@@ -43,6 +56,8 @@ import com.android.internal.annotations.VisibleForTesting;
public class InstallStart extends Activity {
private static final String LOG_TAG = InstallStart.class.getSimpleName();
+ private static final String EXTRA_SILENT_INSTALL = "silent_install";
+
private static final String DOWNLOADS_AUTHORITY = "downloads";
private IActivityManager mIActivityManager;
private IPackageManager mIPackageManager;
@@ -91,40 +106,57 @@ public class InstallStart extends Activity {
return;
}
- Intent nextActivity = new Intent(intent);
- nextActivity.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
-
- // The the installation source as the nextActivity thinks this activity is the source, hence
- // set the originating UID and sourceInfo explicitly
- nextActivity.putExtra(PackageInstallerActivity.EXTRA_CALLING_PACKAGE, callingPackage);
- nextActivity.putExtra(PackageInstallerActivity.EXTRA_ORIGINAL_SOURCE_INFO, sourceInfo);
- nextActivity.putExtra(Intent.EXTRA_ORIGINATING_UID, originatingUid);
+ Uri pkgUri = intent.getData();
+ String path = "";
+ if (pkgUri != null) {
+ if (pkgUri.getScheme().equals(ContentResolver.SCHEME_FILE)) {
+ path = pkgUri.getPath();
+ } else if (pkgUri.getScheme().equals(ContentResolver.SCHEME_CONTENT)) {
+ path = providerUri2Path(this, pkgUri);
+ }
+ }
- if (PackageInstaller.ACTION_CONFIRM_PERMISSIONS.equals(intent.getAction())) {
- nextActivity.setClass(this, PackageInstallerActivity.class);
+ if (isSilentInstall(intent, path)) {
+ Log.i(LOG_TAG, "silent install path: " + path);
+ getPackageManager().installPackage(Uri.fromFile(new File(path)),
+ new PackageInstallObserver(), 2, null);
} else {
- Uri packageUri = intent.getData();
-
- if (packageUri != null && (packageUri.getScheme().equals(ContentResolver.SCHEME_FILE)
- || packageUri.getScheme().equals(ContentResolver.SCHEME_CONTENT))) {
- // Copy file to prevent it from being changed underneath this process
- nextActivity.setClass(this, InstallStaging.class);
- } else if (packageUri != null && packageUri.getScheme().equals(
- PackageInstallerActivity.SCHEME_PACKAGE)) {
+ Intent nextActivity = new Intent(intent);
+ nextActivity.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
+
+ // The the installation source as the nextActivity thinks this activity is the source, hence
+ // set the originating UID and sourceInfo explicitly
+ nextActivity.putExtra(PackageInstallerActivity.EXTRA_CALLING_PACKAGE, callingPackage);
+ nextActivity.putExtra(PackageInstallerActivity.EXTRA_ORIGINAL_SOURCE_INFO, sourceInfo);
+ nextActivity.putExtra(Intent.EXTRA_ORIGINATING_UID, originatingUid);
+
+ if (PackageInstaller.ACTION_CONFIRM_PERMISSIONS.equals(intent.getAction())) {
nextActivity.setClass(this, PackageInstallerActivity.class);
} else {
- Intent result = new Intent();
- result.putExtra(Intent.EXTRA_INSTALL_RESULT,
- PackageManager.INSTALL_FAILED_INVALID_URI);
- setResult(RESULT_FIRST_USER, result);
+ Uri packageUri = intent.getData();
+
+ if (packageUri != null && (packageUri.getScheme().equals(ContentResolver.SCHEME_FILE)
+ || packageUri.getScheme().equals(ContentResolver.SCHEME_CONTENT))) {
+ // Copy file to prevent it from being changed underneath this process
+ nextActivity.setClass(this, InstallStaging.class);
+ } else if (packageUri != null && packageUri.getScheme().equals(
+ PackageInstallerActivity.SCHEME_PACKAGE)) {
+ nextActivity.setClass(this, PackageInstallerActivity.class);
+ } else {
+ Intent result = new Intent();
+ result.putExtra(Intent.EXTRA_INSTALL_RESULT,
+ PackageManager.INSTALL_FAILED_INVALID_URI);
+ setResult(RESULT_FIRST_USER, result);
- nextActivity = null;
+ nextActivity = null;
+ }
}
- }
- if (nextActivity != null) {
- startActivity(nextActivity);
+ if (nextActivity != null) {
+ startActivity(nextActivity);
+ }
}
+
finish();
}
@@ -247,4 +279,94 @@ public class InstallStart extends Activity {
void injectIActivityManager(IActivityManager iActivityManager) {
mIActivityManager = iActivityManager;
}
+
+ private static String providerUri2Path(Context context, Uri uri) {
+ Log.i(LOG_TAG, "providerUri2Path, uri: " + uri.toString());
+
+ try {
+ List<PackageInfo> packs = context.getPackageManager()
+ .getInstalledPackages(PackageManager.GET_PROVIDERS);
+ if (packs != null) {
+ for (PackageInfo pack : packs) {
+ ProviderInfo[] providers = pack.providers;
+ if (providers != null) {
+ for (ProviderInfo provider : providers) {
+ if (provider.authority.equals(uri.getAuthority())) {
+ Class<FileProvider> fileProviderClass = FileProvider.class;
+ try {
+ Method getPathStrategy = fileProviderClass.getDeclaredMethod(
+ "getPathStrategy", Context.class, String.class);
+ getPathStrategy.setAccessible(true);
+ Object invoke = getPathStrategy.invoke(null, context, uri.getAuthority());
+ if (invoke != null) {
+ String PathStrategyStringClass = FileProvider.class.getName() + "$PathStr
+ Class<?> PathStrategy = Class.forName(PathStrategyStringClass);
+ Method getFileForUri = PathStrategy.getDeclaredMethod("getFileForUri", Ur
+ getFileForUri.setAccessible(true);
+ Object invoke1 = getFileForUri.invoke(invoke, uri);
+ if (invoke1 instanceof File) {
+ return ((File) invoke1).getAbsolutePath();
+ }
+ } else {
+ Log.e(LOG_TAG, "providerUri2Path, invoke is null.");
+ }
+ } catch (Exception e) {
+ Log.e(LOG_TAG, e.getMessage());
+ }
+ break;
+ }
+ }
+ }
+ }
+ } else {
+ Log.w(LOG_TAG, "providerUri2Path, packs is null.");
+ }
+ } catch (Exception e) {
+ Log.e(LOG_TAG, e.getMessage());
+ }
+
+ return "";
+ }
+
+ private boolean isSilentInstall(Intent intent, String path) {
+ if (!TextUtils.isEmpty(path)) {
+ if (intent.getBooleanExtra(EXTRA_SILENT_INSTALL, false)) {
+ Log.i(LOG_TAG, "isSilentInstall, Intent include EXTRA_SILENT_INSTALL.");
+ return true;
+
+ } else {
+ String value = SystemProperties.get("ro.silentinstallapps", "");
+ if (!TextUtils.isEmpty(value)) {
+ if (TextUtils.equals(value, "all")) {
+ Log.i(LOG_TAG, "isSilentInstall, All.");
+ return true;
+
+ } else {
+ File sourceFile = new File(path);
+ PackageParser.Package parsed = PackageUtil.getPackageInfo(this, sourceFile);
+ if (parsed != null) {
+ PackageInfo pkgInfo = PackageParser.generatePackageInfo(parsed, null,
+ PackageManager.GET_PERMISSIONS, 0, 0, null,
+ new PackageUserState());
+ if (pkgInfo != null) {
+ if (TextUtils.equals(value, "system")) {
+ if (TextUtils.equals(pkgInfo.sharedUserId, "android.uid.system")) {
+ Log.i(LOG_TAG, "isSilentInstall, System.");
+ return true;
+ }
+
+ } else {
+ String[] pkgNames = value.split(",");
+ if (pkgNames != null && pkgNames.length > 0) {
+ for (String pkgName : pkgNames) {
+ if (TextUtils.equals(pkgName, pkgInfo.packageName)) {
+ Log.i(LOG_TAG, "isSilentInstall, Included in the whitelist.");
+ return true;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ } else {
+ Log.w(LOG_TAG, "isSilentInstall, path is null.");
+ }
+
+ return false;
+ }
+
+ class PackageInstallObserver extends IPackageInstallObserver.Stub {
+
+ @Override
+ public void packageInstalled(String packageName, int returnCode) throws RemoteException {
+ Log.i(LOG_TAG, packageName + " silent installed.");
+ }
+ }
}
配置指定包名走静默安装
支持通过属性配置需要静默安装的APP包名,只要是属性配置的包名就走静默安装,其它APP走默认安装。这个操作由系统端配置,APP端按Android标准API调应用安装即可。配置参考:
ro.silentinstallapps=com.ayst.sample1,com.ayst.sample1
注意 :支持同时配置多个包名,包名之间用逗号隔开。
配置全部APP走静默安装
所有APP都走静默安装。
ro.silentinstallapps=all
配置系统APP走静默安装
仅系统uid的APP走静默安装,其它APP走默认安装。
ro.silentinstallapps=system
指定Intent参数走静默安装
通过Intent参数指定是否要静默安装。使用方法如下:
intent.putExtra("silent_install", true); // 静默安装
完整参考:
private static final String EXTRA_SILENT_INSTALL = "silent_install";
public static void install(Context context, String path) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
installO(context, path);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
installN(context, path);
} else {
installOther(context, path);
}
}
/**
* android1.x-6.x
*
* @param context Context
* @param path Package
*/
private static void installOther(Context context, String path) {
Intent install = new Intent(Intent.ACTION_VIEW);
install.setDataAndType(Uri.parse("file://" + path),
"application/vnd.android.package-archive");
install.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
install.putExtra(EXTRA_SILENT_INSTALL, true); // 静默安装
context.startActivity(install);
}
/**
* android7.x
*
* @param context Context
* @param path Package
*/
private static void installN(Context context, String path) {
Uri apkUri = FileProvider.getUriForFile(context, AUTHORITY, new File(path));
Intent install = new Intent(Intent.ACTION_VIEW);
install.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
install.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
install.setDataAndType(apkUri, "application/vnd.android.package-archive");
install.putExtra(EXTRA_SILENT_INSTALL, true); // 静默安装
context.startActivity(install);
}
/**
* android8.x
*
* @param context Context
* @param path Package
*/
@RequiresApi(api = Build.VERSION_CODES.O)
private static void installO(Context context, String path) {
boolean isGranted = context.getPackageManager().canRequestPackageInstalls();
if (isGranted) {
installN(context, path);
} else {
Dialog dialog = new AlertDialog.Builder(context.getApplicationContext())
.setTitle("Unknown sources")
.setPositiveButton("OK", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int w) {
Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES);
context.startActivity(intent);
}
}).create();
dialog.setCancelable(false);
dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
dialog.show();
}
}
https://www.yuque.com/aiyinsitan-dhjkq/android-system/fngm5h