当前位置: 首页 > news >正文

MTK Android12-13 App卸载加锁

实现:App 卸载时候需要加一层拦截锁,客户输入密码后才能正常卸载

文章目录

  • 参考资料:
  • 实现方案
    • 涉及到修改文件
    • 修改方案
    • 实现效果
  • 源码分析- 卸载方式
    • 一) 设置界面进行卸载
      • InstalledAppDetails
      • AppInfoDashboardFragment
      • AppButtonsPreferenceController
        • handleDialogClick
        • uninstallPkg
      • 卸载程序 PackageInstaller - UninstallerActivity
        • showConfirmationDialog
      • 卸载程序 PackageInstaller UninstallAlertDialogFragment
        • startUninstallProgress
        • UninstallUninstalling
      • Framework层- PackageInstallerService
        • uninstall
    • 二) 命令 adb uninstall 卸载方式
    • 三) deletePackage 反射实现卸载app
    • 卸载方案小结
  • 实现方案避坑
  • 总结


参考资料:

android 卸载应用流程
android 应用卸载流程分析
Android PackageManagerService总结(五) APK卸载流程
MTK Android12 安装app添加密码锁限制

实现方案

涉及到修改文件


/frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java

修改方案

在 PackageManagerService 类中的 deletePackageVersioned 方法中进行加锁拦截。 具体修改如下:对应的导入类包 自己导入即可。

     @Overridepublic 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 ACTIVITYACTIVITY com.mediatek.camera/.CameraLauncher 4dfd640 pid=(not running)ACTIVITY com.android.launcher3/.uioverrides.QuickstepLauncher f787db9 pid=1523ACTIVITY 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 {@Overridepublic Intent getIntent() {Intent modIntent = new Intent(super.getIntent());modIntent.putExtra(EXTRA_SHOW_FRAGMENT, AppInfoDashboardFragment.class.getName());return modIntent;}@Overrideprotected boolean isValidFragment(String fragmentName) {return AppInfoDashboardFragment.class.getName().equals(fragmentName);}
}

AppInfoDashboardFragment

在createPreferenceControllers 创建控制器中有这样一段代码,如下:

@Overrideprotected 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 activityUri 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

分析源码就是一个弹框,点击事件如下:

    @Overridepublic 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 里面我们看到了这样一段卸载内容,如下:

@Overrideprotected 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 方法了,去看看:

    @Overridepublic 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 permissionfinal 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 userfinal 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 方法

   @Overridepublic 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().applicationContextval pkgManager = context.packageManagerval c = pkgManager.javaClassval p2 = Class.forName("android.content.pm.IPackageDeleteObserver")//不能用Int.javaClass,必须用Int::class.javaPrimitiveTypeval 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)@UnsupportedAppUsagepublic abstract void deletePackage(@NonNull String packageName,@Nullable IPackageDeleteObserver observer, @DeleteFlags int flags);

我们发现反射调用的是PackageManager类的抽象方法 deletePackage, 并且是一个 @hide 标志的隐藏方法。

再分析下 context.packageManager,方法:
getPackageManager()函数的实现在ContextImpl.java , 那么真实的PackageManager 的 子类实现就是在这个ContextImpl 中定义的。

ContextImpl  getPackageManager 方法@Overridepublic 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@UnsupportedAppUsagepublic void deletePackage(String packageName, IPackageDeleteObserver observer, int flags) {deletePackageAsUser(packageName, observer, flags, getUserId());}@Overridepublic 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();}}@UnsupportedAppUsageprotected ApplicationPackageManager(ContextImpl context, IPackageManager pm) {mContext = context;mPM = pm;}

这里不再详细追寻 PM 逻辑了,一看就是最终调用到 PackageManagerService 类的deletePackageAsUser 方法了。
PackageManagerService deletePackageAsUser 方法

    @Overridepublic 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 开发工具查看源码,提高代码阅读效率

相关文章:

MTK Android12-13 App卸载加锁

实现&#xff1a;App 卸载时候需要加一层拦截锁&#xff0c;客户输入密码后才能正常卸载 文章目录 参考资料&#xff1a;实现方案涉及到修改文件修改方案实现效果 源码分析- 卸载方式一&#xff09; 设置界面进行卸载InstalledAppDetailsAppInfoDashboardFragmentAppButtonsPre…...

文号验证-同时对两个输入框验证

文号验证-同时对两个输入框验证 效果&#xff1a; 一、如果有多个文号&#xff1a; <div v-for"(item, index) in approvalForm.productApprovalTypeEvents" :key"index"> <el-form-itemlabel"文号":prop"productApprovalTypeEv…...

嵌入式面试八股文(十二)·FreeRTOS中·堆和栈

目录 1. 堆和栈 1.1 堆 1.2 栈 1.3 堆和栈的区别 1.3.1 分配方式 1.3.2 分配效率 1.3.3 生长方向 1.3.4 空间管理 1.3.5 存放内容 1. 堆和栈 1.1 堆 堆是一块用于动态分配内存的区域&#xff0c;用于存储程序运行时动态创建的对象。堆的大小可以在程序运行…...

表的增删改查

目录 1、增删改查&#xff08;CRUD&#xff09; 2、新增&#xff08;C&#xff09; 3、查询&#xff08;R&#xff09; 3.1、全列查询 3.2、指定列查询 3.3、查询字段为表达式 3.4、别名 3.5、去重&#xff1a;distinct 3.6、查询时排序 1、增删改查&#xff08;CRUD&…...

软考-软件设计师中级备考 4、数据结构

1、数据结构三要素 要素定义分类特点逻辑结构数据元素之间的逻辑关系&#xff0c;是从具体问题抽象出来的数学模型&#xff0c;与数据存储无关1. 集合结构&#xff1a;数据元素同属一个集合&#xff0c;无其他特殊关系 如一盒麦丽素豆子 2. 线性结构&#xff1a;元素存在一对…...

CMake:设置编译C++的版本

CMake提供了CMAKE_CXX_STANDARD用于设置C++的版本,比如可以通过如下方式设置使用C++23 set(CMAKE_CXX_STANDARD 23) 同时也提供了CMAKE_CXX_STANDARD_REQUIRED可以要求强制使用某个C++版本,比如: set(CMAKE_CXX_STANDARD_REQUIRED ON) 如果设置为 `ON`,那么 CMake 会要求编…...

【angular19】入门基础教程(一):项目的搭建与启动

angular现在发展的越来越能完善了&#xff0c;在vue和react的强势竞争下&#xff0c;它迎来了自己的巨大变革。项目工程化越来越好&#xff0c;也开始拥抱了vite这种高效的构建方式。所以&#xff0c;我们有必要来学习这么一个框架了。 项目实现效果 nodejs环境 Node.js - v^…...

在使用Python的Selenium库打卡网页后,通过CDP命令获取所有cookies(包括Httponly和Secure的cookies)

通过 Chrome DevTools Protocol (CDP) 结合 Selenium 可以直接获取浏览器存储的所有 Cookies&#xff08;包括所有域名下的 Cookies&#xff09;&#xff0c;以下是详细步骤&#xff1a; 步骤 1&#xff1a;配置 Chrome 启用 CDP启动 Chrome 浏览器时需启用远程调试端口&#…...

Deepseek-v3+cline+vscode java自动化编程

1、Deepseek DeepSeek 充值后&#xff0c;创建apikey 2、vscode Visual Studio Code - Code Editing. Redefined 3、下载插件cline 4、配置deepeseek-v3 的密钥到cline 5、不可用 在开始的几次调用能正常使用起来&#xff0c;用了几次后&#xff0c;不能使用了&#xff0c;请求…...

5G育种技术之植物性状订制

行业展望 我国农作物种业市场规模逐年增长&#xff0c;其中以粮食作物种子市场规模较大。目前我国育种产业发展仍处于初级阶段&#xff0c;存在龙头企业市场占有率和行业集中度不高、企业育种技术和水平落后于发达国家、种企研发投入不足等问题。虽然基因编辑技术的出现有望改…...

12前端项目----添加购物车1.0

商品添加购物车 商品数量添加购物车浏览器本地存储localStoragesessionStorage添加成功页面 商品数量 输入为数字&#xff0c;最少为1<div class"cartWrap"><div class"controls"><input autocomplete"off" class"itxt&quo…...

Elastic Platform 8.18 和 9.0:ES|QL Lookup Joins 功能现已推出,Lucene 10!

作者&#xff1a;来自 Elastic Elastic Platform Team Elastic 最新发布的 8.18 和 9.0 版本包含了强大的更新&#xff0c;将显著提升你的体验、增强查询性能并优化日志管理。无论你是在处理搜索、可观察性还是安全用例&#xff0c;本次发布都带来了大量新特性&#xff0c;旨在…...

两地三中心

1. 什么是同城备份 ✅ 定义&#xff1a; 主机房和备份机房都在同一座城市&#xff0c;通常距离几十公里以内&#xff08;比如10-50公里&#xff09;。 ✅ 特点&#xff1a; 网络延迟很低&#xff08;毫秒级&#xff09;&#xff0c;可以做到实时同步/热备份。 恢复速度快&am…...

数据结构第七章(一)-顺序查找和折半查找

数据结构第七章&#xff08;一&#xff09; 顺序查找和折半查找一、查找1.平均查找长度&#xff08;ASL&#xff09; 二、顺序查找1.实现2.算法优化 三、折半查找1.实现2.查找判定树 四、分块查找1.算法思想2.查找效率分析&#xff08;ASL&#xff09; 总结 顺序查找和折半查找…...

springboot项目之websocket的坑:spring整合websocket后进行单元测试后报错的解决方案

前排提醒&#xff1a;还是博主菜&#xff0c;见识短浅&#xff0c;没遇到过这个问题。。。 起因 前段时间学习websocket和sse&#xff0c;写demo用了spring框架。后来又写了新的spring单元测试类demo去测试&#xff0c;结果启动后报错&#xff0c;报错信息提示websocket的相关…...

在单片机编程中充分使用抽象工厂模式,确保对象创建的限制,多使用抽象接口避免多变具体实现类

背景 在软件架构设计上追求稳定,就必须多使用稳定的抽象接口,少依赖多变的实现;具体编码时可以充分使用抽象工厂模式 举例进行详细讲解和说明抽象工厂模式在单片机开发中的应用 抽象工厂模式是一种创建型设计模式,它提供了一种方式,可以将一组相关的对象创建封装到一个…...

喷泉码技术在现代物联网中的应用的总结和参考文献

总结 物联网与 5G 技术高速发展,数据传输对可靠性和实时性提出严苛要求。前向纠错码是增强通信鲁棒性的关键,但平衡冗余资源开销与编解码效率的矛盾是核心难题。LT 码和 Raptor 码是无率码典型。理论上它们能达渐进最优性能,然而实际系统受数据包规模、计算资源等限制,其工…...

vuex与vuex-persistedstate 插件固化数据

一&#xff0c;vuex与vuex-persistedstate 插件固化数据 的小案例 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><title>Vuex基础案例</title></head><body><div id"app"&…...

如何在WordPress网站中设置双重验证,提升安全性

随着互联网的不断进步&#xff0c;网站的安全问题越来越受到重视。尤其对于WordPress这样常用的建站平台&#xff0c;安全性显得尤为重要。尽管WordPress自带一定的安全性&#xff0c;但仅依靠用户名和密码的登录方式仍然存在风险。因此&#xff0c;启用“双重验证”便成为了提…...

2025系统架构师---基于规则的系统架构风格‌

引言 在业务规则频繁变更、决策逻辑高度动态化的现代企业环境中&#xff0c;‌基于规则的系统架构风格‌&#xff08;Rule-Based System Architecture Style&#xff09;通过将核心业务逻辑抽象为‌可配置规则‌、‌规则引擎‌与‌决策服务‌的分离&#xff0c;实现了业务敏捷…...

Python排序中lambda函数详解

在 Python 中&#xff0c;lambda 函数是一种匿名函数&#xff0c;通常用于需要一个函数但又不想为其定义一个正式名称的情况。在排序操作中&#xff0c;lambda 函数用于指定排序的依据。 lambda 函数的基本语法 lambda 函数的基本语法如下&#xff1a; lambda arguments: ex…...

web 基础与 http 协议

目录 一 Web 基础 1. 域名和 DNS 1.1 域名的概念 1.2 DNS 2. 网页与HTML 2.1 HTML 概述 2.2 HTML 基本标签 2.3 网站和主页 3. 静态网页与动态网页 3.1 静态网页 3.2 动态网页 二 HTTP 协议 1. HTTP 协议概述 2. HTTP 方法 3. HTTP 状态码 4. HTTP 请求流程分析…...

记一次奇妙的Oracle注入绕WAF之旅

0x01 一个登陆框 上班时遇到了一个登陆框 看着这个复古的界面&#xff0c;于是上手除了admin123456之外顺手点了个 于是弹出了一条有意思的报错 这就有意思了&#xff0c;毕竟已经很久没在登陆框遇到sql注入了&#xff0c;当我想当然的认为万能密码可以秒时&#xff0c;事情出…...

python裁剪小说封面标题

一张矩形图片 比如50*100 大小 中心点的坐标是是(0,0) 左上角是(-25,50) 右上角是(25,50) 左下角是(-25,-50) 右下角是(25,-50) 我希望你能用python,帮我对本地指定图片切割大小&#xff0c;计算出该图片的中心坐标&#xff0c;然后按照我输入的长宽具体值&#xff0c;比…...

高性价比手机如何挑选?

这四个关键点&#xff0c;助你找到心仪机~ 一、性能强者&#xff1a;游戏娱乐畅快到底 处理器相当于手机的 “大脑”&#xff0c;处理速度快、能力强&#xff0c;运行大型游戏毫无压力。 搭配上大容量运存&#xff0c;多任务切换也能秒速完成&#xff0c;再也不怕游戏卡顿啦。…...

Java面试场景深度解析

Java面试场景深度解析 在互联网大厂Java求职者的面试中&#xff0c;经常会被问到关于Java项目中的各种技术场景题。本文通过一个故事场景来展示这些问题的实际解决方案。 第一轮提问 面试官&#xff1a;马架构&#xff0c;欢迎来到我们公司的面试现场。请问您对Java内存模型…...

【DeepSeek认证】最好的MODBUS调试工具

根据搜索结果&#xff0c;MThings 和 Modbus Poll 是当前被广泛推荐且功能强大的MODBUS调试工具。以下是两者的详细对比及推荐理由&#xff1a; 1. MThings 核心优势&#xff1a; 主从一体化&#xff1a;支持同时模拟MODBUS主站和从站&#xff0c;无需切换工具即可完成双向调…...

欧莱雅集团:利用 Google Maps Platform Environment API 提供个性化护肤推荐

在欧莱雅集团&#xff0c;美丽绝不仅仅停留在表面。如今&#xff0c;这一点比以往任何时候都更加真实&#xff0c;因为公司将其深厚的科学专业知识与尖端技术相结合&#xff0c;以重塑美丽的未来。其成功的关键在于承诺不为科技而使用科技。其所有创新都满足了消费者的明确需求…...

2025三掌柜赠书活动第十五期:高并发系统:设计原理与实践

目录 前言 什么是高并发&#xff1f; 高并发系统的挑战 设计原理 1、分布式架构 2、缓存与异步处理 3、数据库优化 4、弹性扩展 实践方法 1、性能监控与分析 2、压力测试 3、故障排查与容错机制 关于《高并发系统&#xff1a;设计原理与实践》 编辑推荐 内容简介…...

【Spark入门】Spark架构解析:组件与运行机制深度剖析

1 Spark架构全景图 Apache Spark作为当今最流行的大数据处理框架之一&#xff0c;其卓越性能的背后是一套精心设计的分布式架构。理解Spark的架构组成和运行机制&#xff0c;对于性能调优和故障排查至关重要。 1.1 核心组件架构 组件交互流程&#xff1a; Driver初始化&#xf…...

电子电器架构 -- 汽车零部件DV试验与PV试验的定义及关键差异

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 钝感力的“钝”,不是木讷、迟钝,而是直面困境的韧劲和耐力,是面对外界噪音的通透淡然。 生活中有两种人,一种人格外在意别人的眼光;另一种人无论…...

交换机配置DHCP

交换机配置DHCP 背景先关闭路由器的DHCPconsole口连接到交换机配置交换机 背景 路由器的dhcp分配IP地址变慢&#xff0c;怎么处理 先关闭路由器的DHCP 查看路由器中DHCP地址池范围; 关闭路由器的DHCP console口连接到交换机 协议Serial端口COMX波特率9600流控无 配置交换机…...

【人工智能】边缘智能的突破:Ollama模型压缩技术与DeepSeek部署实践

《Python OpenCV从菜鸟到高手》带你进入图像处理与计算机视觉的大门! 解锁Python编程的无限可能:《奇妙的Python》带你漫游代码世界 随着边缘计算的兴起,将大型语言模型(LLM)部署到资源受限的边缘设备成为研究热点。Ollama凭借其高效的模型压缩技术和轻量级推理框架,为…...

基于STM32、HAL库的DS2401P安全验证及加密芯片驱动程序设计

一、简介&#xff1a; DS2401P是Maxim Integrated&#xff08;现为Analog Devices&#xff09;生产的一款1-Wire硅序列号芯片&#xff0c;具有以下特点&#xff1a; 64位唯一ROM编码&#xff08;包括8位家族码、48位序列号和8位CRC校验码&#xff09; 单总线接口&#xff0c;…...

日志收集之 logback使用

一 简介 1.1 Logback 是一个用于 Java 应用程序的强大日志框架&#xff0c;广泛应用于企业级应用中。它是由 SLF4J 的创始人开发的&#xff0c;旨在成为 Log4j 的替代品。Logback 提供更高级、更灵活的功能&#xff0c;同时还支持与 SLF4J 的无缝集成。 2.2 Logback分为下面几…...

Linux学习笔记(一):Linux下的基本指令

文章目录 Linux下的基本指令1. ls指令2. pwd指令3. cd指令4. touch指令5. mkdir指令&#xff08;牢记&#xff09;6. rmdir指令 && rm 指令&#xff08;牢记&#xff09;7. man指令&#xff08;牢记&#xff09;8. echo指令9. cp指令&#xff08;牢记&#xff09;10. m…...

Unity AI-使用Ollama本地大语言模型运行框架运行本地Deepseek等模型实现聊天对话(二)

一、使用介绍 官方网页&#xff1a;Ollama官方网址 中文文档参考&#xff1a;Ollama中文文档 相关教程&#xff1a;Ollama教程 使用版本&#xff1a;Unity 2022.3.53f1c1、Ollama 0.6.2 示例模型&#xff1a;llama3.2 二、运行示例 三、使用步骤 1、创建Canvas面板 具体…...

SDC命令详解:使用get_clocks命令进行查询

相关阅读 SDC命令详解https://blog.csdn.net/weixin_45791458/category_12931432.html?spm1001.2014.3001.5482 get_clocks命令用于创建一个时钟对象集合&#xff0c;关于设计对象和集合的更详细介绍&#xff0c;可以参考下面的博客。需要注意的是&#xff0c;在有些工具中还…...

git 修改用户名和邮箱

在 Git 中修改用户名和邮箱地址是常见的任务&#xff0c;这可以确保你的提交记录使用正确的身份信息。你可以通过简单的命令来完成这一操作。 全局配置 修改全局用户名 要修改全局的用户名&#xff0c;请执行以下命令&#xff1a; git config --global user.name "New…...

pg数据库删除模式

不能直接使用 DROP SCHEMA "app_sys" 删除&#xff0c; 这样会报错 cannot drop schema app sys 20250416 because other objects depend on it DETAlL: extension uuid-ossp depends on schema app sys 20250416sequence app sys 20250416.app sys id seq depend…...

【C++】Googletest应用

Googletest 1 配置 使用cmake配置&#xff1a; 具体文件后面上传补充 ./test.out --gtest_filterXXXTest.xxx 2 gdb 为了跟踪流程&#xff0c;可以使用gdb&#xff1b; gdb ./xxx.out gdb --args ./gtest --gtest_filterxxx.xxx设置运行参数 set args --gtest_filterxxx.…...

QgraphicsView异步线程加载地图瓦片

本节主要记录一下qt开发过程中离线地图瓦片的加载方式&#xff0c;瓦片加载选择graphicsView控件&#xff0c;同时为了不影响主线程事件和其他操作&#xff0c;这里采用了异步线程的操作&#xff0c;将地图瓦片加载的步骤放到了异步子线程之中。注&#xff1a;本记录仅为本人笔…...

机器学习day2

使用KNN算法实现机器学习 给我一个苹果的图片 我能预测出这个是一个苹果 代码&#xff1a; # 导入需要的库 # 读图 import os import cv2 # 绘图 import matplotlib.pyplot as plt import seaborn as sns # 数组 import numpy as np from skimage.feature import hog from sk…...

jquery解决谷歌浏览器自动保存加密密码是乱码

添加一个隐形的input框&#xff0c;提交隐藏input框里的数据&#xff0c;展示框展现的还是明文密码&#xff0c;并且不提交展示框的值 <formid"loginForm"class"form-signin newForm-signin"action"${ctx}/login"method"post"onsub…...

Python 如何操作数据库,让你使用 DeepSeek 开发数据库应用更加快 (Orm Bee)

Python 如何操作数据库&#xff0c;让你使用 DeepSeek 开发数据库应用更加快 操作数据库最好用 ORM 工具&#xff0c;可以提高开发效率. ORM 就是实体与数据库表的映射&#xff0c;让我们可以用面向对象的方式来操作数据库. 简单易用&#xff0c;直接上代码. 使用Orm Bee操作…...

如何解决 Linux 文件系统挂载失败的问题

以下是解决Linux文件系统挂载失败问题的系统性排查与解决方案&#xff1a; 一、设备基础检查 确认设备识别状态‌ 执行 lsblk 或 fdisk -l 查看磁盘设备列表&#xff0c;验证目标设备&#xff08;如 /dev/sdb1&#xff09;是否被系统识别。 若设备未显示&#xff0c;需排查&a…...

JVM——引入

什么是JVM&#xff1f;它与JDK、JRE的关系&#xff1f; JVM、JRE 和 JDK 是 Java 平台的三个核心组件&#xff0c;各自承担着不同的职责&#xff0c;它们之间的关系密不可分。理解它们的区别和联系有助于更好地开发、部署和运行 Java 应用程序。对于 Java 开发者来说&#xff…...

Golang|工厂模式

工厂模式是一种创建型设计模式&#xff0c;它的核心思想是&#xff1a;把对象的创建过程封装起来&#xff0c;不直接在代码中 new 一个对象&#xff0c;而是通过一个“工厂”来生成对象。这样做的好处是&#xff1a; 降低了代码之间的耦合&#xff08;依赖具体类减少&#xff0…...

Transformer数学推导——Q29 推导语音识别中流式注意力(Streaming Attention)的延迟约束优化

该问题归类到Transformer架构问题集——注意力机制——跨模态与多模态。请参考LLM数学推导——Transformer架构问题集。 在语音识别任务中&#xff0c;实时性是核心需求 —— 想象你使用语音助手时&#xff0c;每说完一个词就希望即时看到文字反馈&#xff0c;而不是等整句话说…...

dx11 龙书学习 第四章 dx11 准备工作

4.1 准备工作 Direct3D的初始化过程要求我们熟悉一些基本的Direct3D类型和基本绘图概念&#xff1b;本章第一节会向读者介绍些必要的基础知识。然后我们会详细讲解Direct3D初始化过程中的每一个必要步骤&#xff0c;并顺便介绍一下实时绘图应用程序必须使用的精确计时和时间测…...