实现:App 卸载时候需要加一层拦截锁,客户输入密码后才能正常卸载
文章目录
- 参考资料:
- 实现方案
- 源码分析- 卸载方式
- 实现方案避坑
- 总结
参考资料:
android 卸载应用流程
android 应用卸载流程分析
Android PackageManagerService总结(五) APK卸载流程
MTK Android12 安装app添加密码锁限制
实现方案
涉及到修改文件
/frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java
修改方案
在 PackageManagerService 类中的 deletePackageVersioned 方法中进行加锁拦截。 具体修改如下:对应的导入类包 自己导入即可。
@Override
public void deletePackageVersioned(VersionedPackage versionedPackage,
final IPackageDeleteObserver2 observer, final int userId, final int deleteFlags) {
- deletePackageVersionedInternal(versionedPackage, observer, userId, deleteFlags, false);
+ // deletePackageVersionedInternal(versionedPackage, observer, userId, deleteFlags, false);
+
+ String tName= Thread.currentThread().getName();
+ Slog.w(TAG, "deletePackageVersioned tName:"+tName);
+
+
+ Handler handler = new Handler(Looper.getMainLooper());
+
+
+ final WindowManager.LayoutParams mLayoutParams = new WindowManager.LayoutParams();
+ mLayoutParams.width = 1000;
+ mLayoutParams.height =500;
+ mLayoutParams.dimAmount =0.5f;
+ mLayoutParams.format = PixelFormat.TRANSLUCENT;
+ mLayoutParams.type = WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG;
+ WindowManager mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
+
+ final LinearLayout parentLayout = new LinearLayout(mContext);
+ parentLayout.setOrientation(LinearLayout.VERTICAL);
+ parentLayout.setBackgroundColor(Color.WHITE);
+ LinearLayout.LayoutParams layoutParams
+ = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT);
+ parentLayout.setLayoutParams(layoutParams);
+
+ TextView titleText = new TextView(mContext);
+ LinearLayout.LayoutParams contentParams
+ = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
+ titleText.setLayoutParams(contentParams);
+ titleText.setText("check password");
+ titleText.setTextColor(Color.BLACK);
+ titleText.setTypeface(Typeface.create(titleText.getTypeface(), Typeface.NORMAL), Typeface.BOLD);
+ titleText.setPadding(10, 10, 0, 0);
+ parentLayout.addView(titleText);
+
+ EditText passEdtTxt = new EditText(mContext);
+ passEdtTxt.setLayoutParams(contentParams);
+ passEdtTxt.setHint("Please input password");
+ passEdtTxt.setTextSize(14);
+ passEdtTxt.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
+ passEdtTxt.setTextColor(Color.BLACK);
+
+ parentLayout.addView(passEdtTxt);
+ RelativeLayout reLayout = new RelativeLayout(mContext);
+ RelativeLayout.LayoutParams rightReal = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+ rightReal.addRule(RelativeLayout.ALIGN_PARENT_TOP);
+ rightReal.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, RelativeLayout.TRUE);
+ rightReal.setMargins(0,10,15,0);
+
+ Button confirmBtn = new Button(mContext);
+ LinearLayout.LayoutParams btnParams
+ = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
+ confirmBtn.setLayoutParams(btnParams);
+ confirmBtn.setText("ok");
+ confirmBtn.setTextColor(Color.parseColor("#BEBEBE"));
+ confirmBtn.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ String password = passEdtTxt.getText().toString();
+ if ("123456".equals(password)) {
+ if (parentLayout!=null){
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ mWindowManager.removeViewImmediate(parentLayout);
+ }
+ });
+ }
+ deletePackageVersionedInternal(versionedPackage, observer, userId, deleteFlags, false);
+ } else {
+
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(mContext,"PassWorld is Error !",Toast.LENGTH_SHORT).show();
+ }
+ });
+ }
+ }
+ });
+
+ reLayout.addView(confirmBtn, rightReal);
+ RelativeLayout.LayoutParams leftReal = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+ leftReal.addRule(RelativeLayout.ALIGN_PARENT_TOP);
+ leftReal.addRule(RelativeLayout.ALIGN_PARENT_LEFT, RelativeLayout.TRUE);
+ leftReal.setMargins(15,10,0,0);
+ Button cancelBtn = new Button(mContext);
+ LinearLayout.LayoutParams cancelbtnParams
+ = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
+ cancelBtn.setLayoutParams(cancelbtnParams);
+ cancelBtn.setText("cancel");
+ cancelBtn.setTextColor(Color.parseColor("#BEBEBE"));
+
+ cancelBtn.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (parentLayout != null) {
+ Slog.w(TAG, "cancelBtn ");
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ mWindowManager.removeViewImmediate(parentLayout);
+ }
+ });
+ }
+ }
+ });
+ reLayout.addView(cancelBtn, leftReal);
+ parentLayout.addView(reLayout);
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ mWindowManager.addView(parentLayout, mLayoutParams);
+ } catch (WindowManager.BadTokenException e) {
+ e.printStackTrace();
+ }
+ }
+ });
+
}
实现效果
实际测试,接下来的源码分析当中的三种方案:系统常规进入设置卸载app、adb uninstall +package 命令卸载app、静默卸载app 都可以达到想要的效果,即卸载app 会弹框输入框。输入正确后才会走接下来的卸载app 流程,达到了想要的效果。
源码分析- 卸载方式
卸载有以下几种方式
- 去设置界面 进行卸载
- 命令行 adb uninstall pkg(包名)
- 具备系统签名的应用通过反射 deletePackage 实现卸载App 功能
一) 设置界面进行卸载
InstalledAppDetails
定位界面如下:定位到卸载入口 InstalledAppDetails
DisPlay:/ $ dumpsys activity top | grep ACTIVITY
ACTIVITY com.mediatek.camera/.CameraLauncher 4dfd640 pid=(not running)
ACTIVITY com.android.launcher3/.uioverrides.QuickstepLauncher f787db9 pid=1523
ACTIVITY com.android.settings/.applications.InstalledAppDetails b8801c2 pid=3597
看Manifest.xml 定义,它的实际 Activity 是 InstalledAppDetailsTop
<!-- Keep compatibility with old shortcuts. -->
<activity-alias android:name=".applications.InstalledAppDetails"
android:label="@string/application_info_label"
android:exported="true"
android:targetActivity=".applications.InstalledAppDetailsTop">
<intent-filter android:priority="1">
<action android:name="android.settings.APPLICATION_DETAILS_SETTINGS" />
<action android:name="android.intent.action.AUTO_REVOKE_PERMISSIONS" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="package" />
</intent-filter>
</activity-alias>
源码如下;很简单的一个Activity,实际UI逻辑都在AppInfoDashboardFragment 中
public class InstalledAppDetailsTop extends SettingsActivity {
@Override
public Intent getIntent() {
Intent modIntent = new Intent(super.getIntent());
modIntent.putExtra(EXTRA_SHOW_FRAGMENT, AppInfoDashboardFragment.class.getName());
return modIntent;
}
@Override
protected boolean isValidFragment(String fragmentName) {
return AppInfoDashboardFragment.class.getName().equals(fragmentName);
}
}
AppInfoDashboardFragment
在createPreferenceControllers 创建控制器中有这样一段代码,如下:
@Override
protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
retrieveAppEntry();
..............................
// The following are controllers for preferences that don't need to refresh the preference
// state when app state changes.
mInstantAppButtonPreferenceController =
new InstantAppButtonsPreferenceController(context, this, packageName, lifecycle);
controllers.add(mInstantAppButtonPreferenceController);
mAppButtonsPreferenceController = new AppButtonsPreferenceController(
(SettingsActivity) getActivity(), this, lifecycle, packageName, mState,
REQUEST_UNINSTALL, REQUEST_REMOVE_DEVICE_ADMIN);
...............
controllers.add(mAppButtonsPreferenceController);
controllers.add(new AppBatteryPreferenceController(
context, this, packageName, getUid(), lifecycle));
controllers.add(new AppMemoryPreferenceController(context, this, lifecycle));
controllers.add(new DefaultHomeShortcutPreferenceController(context, packageName));
controllers.add(new DefaultBrowserShortcutPreferenceController(context, packageName));
controllers.add(new DefaultPhoneShortcutPreferenceController(context, packageName));
controllers.add(new DefaultEmergencyShortcutPreferenceController(context, packageName));
controllers.add(new DefaultSmsShortcutPreferenceController(context, packageName));
return controllers;
}
所以控制器大概就是 AppButtonsPreferenceController
AppButtonsPreferenceController
handleDialogClick
看源码
public void handleDialogClick(int id) {
switch (id) {
case ButtonActionDialogFragment.DialogType.DISABLE:
mMetricsFeatureProvider.action(mActivity,
SettingsEnums.ACTION_SETTINGS_DISABLE_APP);
AsyncTask.execute(new DisableChangerRunnable(mPm, mAppEntry.info.packageName,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER));
break;
case ButtonActionDialogFragment.DialogType.SPECIAL_DISABLE:
mMetricsFeatureProvider.action(mActivity,
SettingsEnums.ACTION_SETTINGS_DISABLE_APP);
uninstallPkg(mAppEntry.info.packageName, false, true);
break;
case ButtonActionDialogFragment.DialogType.FORCE_STOP:
forceStopPackage(mAppEntry.info.packageName);
break;
}
}
这里 uninstallPkg forceStopPackage 对应的不就是界面上 卸载、停止 app 对应的操作嘛
卸载点击后弹出的界面如下
uninstallPkg
看源码
void uninstallPkg(String packageName, boolean allUsers, boolean andDisable) {
stopListeningToPackageRemove();
// Create new intent to launch Uninstaller activity
Uri packageUri = Uri.parse("package:" + packageName);
Intent uninstallIntent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE, packageUri);
uninstallIntent.putExtra(Intent.EXTRA_UNINSTALL_ALL_USERS, allUsers);
mMetricsFeatureProvider.action(
mActivity, SettingsEnums.ACTION_SETTINGS_UNINSTALL_APP);
mFragment.startActivityForResult(uninstallIntent, mRequestUninstall);
mDisableAfterUninstall = andDisable;
}
这里发送了一个广播 ACTION_UNINSTALL_PACKAGE
/**
* Activity Action: Launch application uninstaller.
* <p>
* Input: The data must be a package: URI whose scheme specific part is
* the package name of the current installed package to be uninstalled.
* You can optionally supply {@link #EXTRA_RETURN_RESULT}.
* <p>
* Output: If {@link #EXTRA_RETURN_RESULT}, returns whether the uninstall
* succeeded.
* <p>
* Requires {@link android.Manifest.permission#REQUEST_DELETE_PACKAGES}
* since {@link Build.VERSION_CODES#P}.
*
* @deprecated Use {@link android.content.pm.PackageInstaller#uninstall(String, IntentSender)}
* instead
*/
@Deprecated
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String ACTION_UNINSTALL_PACKAGE = "android.intent.action.UNINSTALL_PACKAGE";
看方法注释,就是拉起 uninstaller app,进行卸载操作。 那就需要分析 PackageInstaller 应用
卸载程序 PackageInstaller - UninstallerActivity
看Manifest 配置
<activity android:name=".UninstallerActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:theme="@style/Theme.AlertDialogActivity.NoActionBar"
android:excludeFromRecents="true"
android:noHistory="true"
android:exported="true">
<intent-filter android:priority="1">
<action android:name="android.intent.action.DELETE" />
<action android:name="android.intent.action.UNINSTALL_PACKAGE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="package" />
</intent-filter>
</activity>
类注释如下:
/*
* This activity presents UI to uninstall an application. Usually launched with intent
* Intent.ACTION_UNINSTALL_PKG_COMMAND and attribute
* com.android.packageinstaller.PackageName set to the application package name
*/
public class UninstallerActivity extends Activity {
展示卸载的UI
showConfirmationDialog
最终会调用一个Fragment 来显示 卸载界面展示和业务
private void showConfirmationDialog() {
if (isTv()) {
showContentFragment(new UninstallAlertFragment(), 0, 0);
} else {
showDialogFragment(new UninstallAlertDialogFragment(), 0, 0);
}
}
这里举例一个来说明 UninstallAlertDialogFragment
卸载程序 PackageInstaller UninstallAlertDialogFragment
分析源码就是一个弹框,点击事件如下:
@Override
public void onClick(DialogInterface dialog, int which) {
if (which == Dialog.BUTTON_POSITIVE) {
((UninstallerActivity) getActivity()).startUninstallProgress(
mKeepData != null && mKeepData.isChecked());
} else {
((UninstallerActivity) getActivity()).dispatchAborted();
}
}
反向调用到了Activity 里面的 startUninstallProgress 方法。
startUninstallProgress
直接看源码
public void startUninstallProgress(boolean keepData) {
boolean returnResult = getIntent().getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false);
CharSequence label = mDialogInfo.appInfo.loadSafeLabel(getPackageManager());
if (isTv()) {
Intent newIntent = new Intent(Intent.ACTION_VIEW);
newIntent.putExtra(Intent.EXTRA_USER, mDialogInfo.user);
newIntent.putExtra(Intent.EXTRA_UNINSTALL_ALL_USERS, mDialogInfo.allUsers);
newIntent.putExtra(PackageInstaller.EXTRA_CALLBACK, mDialogInfo.callback);
newIntent.putExtra(PackageUtil.INTENT_ATTR_APPLICATION_INFO, mDialogInfo.appInfo);
if (returnResult) {
newIntent.putExtra(Intent.EXTRA_RETURN_RESULT, true);
newIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
}
newIntent.setClass(this, UninstallAppProgress.class);
startActivity(newIntent);
} else if (returnResult || mDialogInfo.callback != null || getCallingActivity() != null) {
Intent newIntent = new Intent(this, UninstallUninstalling.class);
newIntent.putExtra(Intent.EXTRA_USER, mDialogInfo.user);
newIntent.putExtra(Intent.EXTRA_UNINSTALL_ALL_USERS, mDialogInfo.allUsers);
newIntent.putExtra(PackageUtil.INTENT_ATTR_APPLICATION_INFO, mDialogInfo.appInfo);
newIntent.putExtra(UninstallUninstalling.EXTRA_APP_LABEL, label);
newIntent.putExtra(UninstallUninstalling.EXTRA_KEEP_DATA, keepData);
newIntent.putExtra(PackageInstaller.EXTRA_CALLBACK, mDialogInfo.callback);
if (returnResult) {
newIntent.putExtra(Intent.EXTRA_RETURN_RESULT, true);
}
if (returnResult || getCallingActivity() != null) {
newIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
}
startActivity(newIntent);
} else {
..........
}
}
这里又是跳转界面,传递参数 Intent newIntent = new Intent(this, UninstallUninstalling.class);
UninstallUninstalling
先看类注释:就是展示一个卸载的弹框
/**
* Start an uninstallation, show a dialog while uninstalling and return result to the caller.
*/
public class UninstallUninstalling extends Activity implements
在onCreate 里面我们看到了这样一段卸载内容,如下:
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setFinishOnTouchOutside(false);
......................
try {
if (savedInstanceState == null) {
.....................
Intent broadcastIntent = new Intent(BROADCAST_ACTION);
broadcastIntent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND);
broadcastIntent.putExtra(EventResultPersister.EXTRA_ID, mUninstallId);
broadcastIntent.setPackage(getPackageName());
PendingIntent pendingIntent = PendingIntent.getBroadcast(this, mUninstallId,
broadcastIntent, PendingIntent.FLAG_UPDATE_CURRENT
| PendingIntent.FLAG_MUTABLE);
int flags = allUsers ? PackageManager.DELETE_ALL_USERS : 0;
flags |= keepData ? PackageManager.DELETE_KEEP_DATA : 0;
try {
ActivityThread.getPackageManager().getPackageInstaller().uninstall(
new VersionedPackage(mAppInfo.packageName,
PackageManager.VERSION_CODE_HIGHEST),
getPackageName(), flags, pendingIntent.getIntentSender(),
user.getIdentifier());
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
} else {
mUninstallId = savedInstanceState.getInt(UNINSTALL_ID);
UninstallEventReceiver.addObserver(this, mUninstallId, this);
}
} catch (EventResultPersister.OutOfIdsException | IllegalArgumentException e) {
Log.e(LOG_TAG, "Fails to start uninstall", e);
onResult(PackageInstaller.STATUS_FAILURE, PackageManager.DELETE_FAILED_INTERNAL_ERROR,
null);
}
}
用到了卸载的核心方法:
ActivityThread.getPackageManager().getPackageInstaller().uninstall(
new VersionedPackage(mAppInfo.packageName,
PackageManager.VERSION_CODE_HIGHEST),
getPackageName(), flags, pendingIntent.getIntentSender(),
user.getIdentifier());
ActivityThread.getPackageManager().getPackageInstaller() 又是什么呢?
getPackageManager 指向的应该是 PackageInstaller
根据经验 就要去ApplicationPackageManager 里面找 这个getPackageInstaller 方法了,去看看:
@Override
public PackageInstaller getPackageInstaller() {
synchronized (mLock) {
if (mInstaller == null) {
try {
mInstaller = new PackageInstaller(mPM.getPackageInstaller(),
mContext.getPackageName(), mContext.getAttributionTag(), getUserId());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
return mInstaller;
}
}
mPM.getPackageInstaller() 获取的 IPackageInstaller实例的封装,而 IPackageInstaller 在系统服务端的具体实现是 PackageInstallerService。
Framework层- PackageInstallerService
路径:/frameworks/base/services/core/java/com/android/server/pm/PackageInstallerService.java
uninstall
public void uninstall(VersionedPackage versionedPackage, String callerPackageName, int flags,
IntentSender statusReceiver, int userId) {
final int callingUid = Binder.getCallingUid();
mPm.enforceCrossUserPermission(callingUid, userId, true, true, "uninstall");
if ((callingUid != Process.SHELL_UID) && (callingUid != Process.ROOT_UID)) {
mAppOps.checkPackage(callingUid, callerPackageName);
}
// Check whether the caller is device owner or affiliated profile owner, in which case we do
// it silently.
DevicePolicyManagerInternal dpmi =
LocalServices.getService(DevicePolicyManagerInternal.class);
final boolean canSilentlyInstallPackage =
dpmi != null && dpmi.canSilentlyInstallPackage(callerPackageName, callingUid);
final PackageDeleteObserverAdapter adapter = new PackageDeleteObserverAdapter(mContext,
statusReceiver, versionedPackage.getPackageName(),
canSilentlyInstallPackage, userId);
if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DELETE_PACKAGES)
== PackageManager.PERMISSION_GRANTED) {
// Sweet, call straight through!
mPm.deletePackageVersioned(versionedPackage, adapter.getBinder(), userId, flags);
} else if (canSilentlyInstallPackage) {
// Allow the device owner and affiliated profile owner to silently delete packages
// Need to clear the calling identity to get DELETE_PACKAGES permission
final long ident = Binder.clearCallingIdentity();
try {
mPm.deletePackageVersioned(versionedPackage, adapter.getBinder(), userId, flags);
} finally {
Binder.restoreCallingIdentity(ident);
}
DevicePolicyEventLogger
.createEvent(DevicePolicyEnums.UNINSTALL_PACKAGE)
.setAdmin(callerPackageName)
.write();
} else {
ApplicationInfo appInfo = mPm.getApplicationInfo(callerPackageName, 0, userId);
if (appInfo.targetSdkVersion >= Build.VERSION_CODES.P) {
mContext.enforceCallingOrSelfPermission(Manifest.permission.REQUEST_DELETE_PACKAGES,
null);
}
// Take a short detour to confirm with user
final Intent intent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE);
intent.setData(Uri.fromParts("package", versionedPackage.getPackageName(), null));
intent.putExtra(PackageInstaller.EXTRA_CALLBACK, adapter.getBinder().asBinder());
adapter.onUserActionRequired(intent);
}
}
最终调用的是PackageManagerService deletePackageVersioned 方法
@Override
public void deletePackageVersioned(VersionedPackage versionedPackage,
final IPackageDeleteObserver2 observer, final int userId, final int deleteFlags) {
deletePackageVersionedInternal(versionedPackage, observer, userId, deleteFlags, false);
}
二) 命令 adb uninstall 卸载方式
这里暂不讲解,实际客需过程中,客户使用工程中不会涉及到这个需求的。如果硬是需要这个功能,防止 adb 卸载,一般都会直接屏蔽掉adb 功能。 实则保护的就不仅仅是卸载这个业务了。
三) deletePackage 反射实现卸载app
关联到的源码类:
/frameworks/base/core/java/android/content/pm/PackageManager.java
/frameworks/base/core/java/android/app/ContextImpl.java
/frameworks/base/core/java/android/app/ApplicationPackageManager.java
如下常用的反射代码 应用端实现:
private fun deleteByProxy(packageName: String) {
try {
val context = ContextProvider.get().applicationContext
val pkgManager = context.packageManager
val c = pkgManager.javaClass
val p2 = Class.forName("android.content.pm.IPackageDeleteObserver")
//不能用Int.javaClass,必须用Int::class.javaPrimitiveType
val method =
c.getMethod("deletePackage", *arrayOf(String::class.java, p2, Int::class.javaPrimitiveType))
method.invoke(pkgManager, packageName, null, 0)
} catch (e: Exception) {
e.printStackTrace()
}
}
反射通过调用 PackageManager 类的 deletePackage 方法:如下源码
类声明如下:
/**
* Class for retrieving various kinds of information related to the application
* packages that are currently installed on the device.
*
* You can find this class through {@link Context#getPackageManager}.
*
* <p class="note"><strong>Note: </strong>If your app targets Android 11 (API level 30) or
* higher, the methods in this class each return a filtered list of apps. Learn more about how to
* <a href="/training/basics/intents/package-visibility">manage package visibility</a>.
* </p>
*/
public abstract class PackageManager {
deletePackage 方法如下:
/**
* Attempts to delete a package. Since this may take a little while, the
* result will be posted back to the given observer. A deletion will fail if
* the calling context lacks the
* {@link android.Manifest.permission#DELETE_PACKAGES} permission, if the
* named package cannot be found, or if the named package is a system
* package.
*
* @param packageName The name of the package to delete
* @param observer An observer callback to get notified when the package
* deletion is complete.
* {@link android.content.pm.IPackageDeleteObserver#packageDeleted}
* will be called when that happens. observer may be null to
* indicate that no callback is desired.
* @hide
*/
@SuppressWarnings("HiddenAbstractMethod")
@RequiresPermission(Manifest.permission.DELETE_PACKAGES)
@UnsupportedAppUsage
public abstract void deletePackage(@NonNull String packageName,
@Nullable IPackageDeleteObserver observer, @DeleteFlags int flags);
我们发现反射调用的是PackageManager类的抽象方法 deletePackage, 并且是一个 @hide 标志的隐藏方法。
再分析下 context.packageManager,方法:
getPackageManager()函数的实现在ContextImpl.java , 那么真实的PackageManager 的 子类实现就是在这个ContextImpl 中定义的。
ContextImpl getPackageManager 方法
@Override
public PackageManager getPackageManager() {
if (mPackageManager != null) {
return mPackageManager;
}
final IPackageManager pm = ActivityThread.getPackageManager();
if (pm != null) {
// Doesn't matter if we make more than one instance.
return (mPackageManager = new ApplicationPackageManager(this, pm));
}
return null;
}
ApplicationPackageManager
看类定义:
/** @hide */
public class ApplicationPackageManager extends PackageManager {
private static final String TAG = "ApplicationPackageManager";
果然是 PackageManager 类的子类,实现 上面 deletePackage 方法
@Override
@UnsupportedAppUsage
public void deletePackage(String packageName, IPackageDeleteObserver observer, int flags) {
deletePackageAsUser(packageName, observer, flags, getUserId());
}
@Override
public void deletePackageAsUser(String packageName, IPackageDeleteObserver observer,
int flags, int userId) {
try {
mPM.deletePackageAsUser(packageName, VERSION_CODE_HIGHEST,
observer, userId, flags);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
@UnsupportedAppUsage
protected ApplicationPackageManager(ContextImpl context, IPackageManager pm) {
mContext = context;
mPM = pm;
}
这里不再详细追寻 PM 逻辑了,一看就是最终调用到 PackageManagerService 类的deletePackageAsUser 方法了。
PackageManagerService deletePackageAsUser 方法
@Override
public void deletePackageAsUser(String packageName, int versionCode,
IPackageDeleteObserver observer, int userId, int flags) {
Slog.w(TAG, "deletePackageAsUser packageName:"+packageName);
deletePackageVersioned(new VersionedPackage(packageName, versionCode),
new LegacyPackageDeleteObserver(observer).getBinder(), userId, flags);
}
卸载方案小结
上面源码分析,无论是通过反射静默卸载还是设置界面进行卸载 最终调用到PMS的 deletePackageVersioned方法,我们一步一步分析了整个流程。需要处理的就是在 deletePackageVersioned 方法中实现拦截的方法。
实现方案避坑
- 无论何种方案,卸载回调拦截的方法deletePackageVersioned 是 Binder 机制的回调方法,并不是UI线程,所以处理 UI 时候需要回到主线程
- 卸载和安装存在一些公共的区域,比如安装更新app时候也有卸载过程,记得验证是否OK,不要冲突导致安装更新app不成功
- 上面分析了卸载流程走到了 卸载器 PackageInstaller 中 UninstallUninstalling 卸载并监听进度显示UI,那么拦截后 取消过程就需要自行处理下,不然会有个安装弹框一直显示,可以用广播或者系统自带回调实现。 如下这个弹框
总结
- 多分析源码,看流程看业务
- 卸载加锁和安装加锁,在 demo 中的 UI一样的,可以参考下:MTK Android12 安装app添加密码锁限制
- PMS 本身功能比较多,代码量大,多打日志看流程。 用 IDE 开发工具查看源码,提高代码阅读效率