• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.car.settings.applications;
18 
19 import static android.app.Activity.RESULT_OK;
20 
21 import static com.android.car.settings.applications.ApplicationsUtils.isKeepEnabledPackage;
22 import static com.android.car.settings.applications.ApplicationsUtils.isProfileOrDeviceOwner;
23 import static com.android.car.settings.common.ActionButtonsPreference.ActionButtons;
24 
25 import android.app.Activity;
26 import android.app.ActivityManager;
27 import android.app.admin.DevicePolicyManager;
28 import android.car.drivingstate.CarUxRestrictions;
29 import android.content.BroadcastReceiver;
30 import android.content.ComponentName;
31 import android.content.Context;
32 import android.content.Intent;
33 import android.content.pm.ApplicationInfo;
34 import android.content.pm.PackageInfo;
35 import android.content.pm.PackageManager;
36 import android.content.pm.ResolveInfo;
37 import android.net.Uri;
38 import android.os.Bundle;
39 import android.os.UserHandle;
40 import android.os.UserManager;
41 import android.util.ArraySet;
42 import android.view.View;
43 
44 import androidx.annotation.Nullable;
45 import androidx.annotation.VisibleForTesting;
46 
47 import com.android.car.settings.R;
48 import com.android.car.settings.common.ActionButtonInfo;
49 import com.android.car.settings.common.ActionButtonsPreference;
50 import com.android.car.settings.common.ActivityResultCallback;
51 import com.android.car.settings.common.ConfirmationDialogFragment;
52 import com.android.car.settings.common.FragmentController;
53 import com.android.car.settings.common.Logger;
54 import com.android.car.settings.common.PreferenceController;
55 import com.android.car.settings.users.UserHelper;
56 import com.android.settingslib.Utils;
57 import com.android.settingslib.applications.ApplicationsState;
58 
59 import java.util.ArrayList;
60 import java.util.List;
61 import java.util.Set;
62 
63 /**
64  * Shows actions associated with an application, like uninstall and forceStop.
65  *
66  * <p>To uninstall an app, it must <i>not</i> be:
67  * <ul>
68  * <li>a system bundled app
69  * <li>system signed
70  * <li>managed by an active admin from a device policy
71  * <li>a device or profile owner
72  * <li>the only home app
73  * <li>the default home app
74  * <li>for a user with the {@link UserManager#DISALLOW_APPS_CONTROL} restriction
75  * <li>for a user with the {@link UserManager#DISALLOW_UNINSTALL_APPS} restriction
76  * </ul>
77  *
78  * <p>For apps that cannot be uninstalled, a disable option is shown instead (or enable if the app
79  * is already disabled).
80  */
81 public class ApplicationActionButtonsPreferenceController extends
82         PreferenceController<ActionButtonsPreference> implements ActivityResultCallback {
83     private static final Logger LOG = new Logger(
84             ApplicationActionButtonsPreferenceController.class);
85 
86     @VisibleForTesting
87     static final String DISABLE_CONFIRM_DIALOG_TAG =
88             "com.android.car.settings.applications.DisableConfirmDialog";
89     @VisibleForTesting
90     static final String FORCE_STOP_CONFIRM_DIALOG_TAG =
91             "com.android.car.settings.applications.ForceStopConfirmDialog";
92     @VisibleForTesting
93     static final int UNINSTALL_REQUEST_CODE = 10;
94 
95     private DevicePolicyManager mDpm;
96     private PackageManager mPm;
97     private UserManager mUserManager;
98     private UserHelper mUserHelper;
99     private ApplicationsState.Session mSession;
100 
101     private ApplicationsState.AppEntry mAppEntry;
102     private ApplicationsState mApplicationsState;
103     private String mPackageName;
104     private PackageInfo mPackageInfo;
105 
106     @VisibleForTesting
107     final ConfirmationDialogFragment.ConfirmListener mForceStopConfirmListener =
108             new ConfirmationDialogFragment.ConfirmListener() {
109                 @Override
110                 public void onConfirm(@Nullable Bundle arguments) {
111                     LOG.d("Stopping package " + mPackageName);
112                     getContext().getSystemService(ActivityManager.class)
113                             .forceStopPackage(mPackageName);
114                     int userId = UserHandle.getUserId(mAppEntry.info.uid);
115                     mApplicationsState.invalidatePackage(mPackageName, userId);
116                 }
117             };
118 
119     private final View.OnClickListener mForceStopClickListener = i -> {
120         ConfirmationDialogFragment dialogFragment =
121                 new ConfirmationDialogFragment.Builder(getContext())
122                         .setTitle(R.string.force_stop_dialog_title)
123                         .setMessage(R.string.force_stop_dialog_text)
124                         .setPositiveButton(android.R.string.ok,
125                                 mForceStopConfirmListener)
126                         .setNegativeButton(android.R.string.cancel, /* rejectListener= */ null)
127                         .build();
128         getFragmentController().showDialog(dialogFragment, FORCE_STOP_CONFIRM_DIALOG_TAG);
129     };
130 
131     @VisibleForTesting
132     final BroadcastReceiver mCheckKillProcessesReceiver = new BroadcastReceiver() {
133         @Override
134         public void onReceive(Context context, Intent intent) {
135             boolean enabled = getResultCode() != Activity.RESULT_CANCELED;
136             LOG.d("Got broadcast response: Restart status for " + mPackageName + " " + enabled);
137             updateForceStopButtonInner(enabled);
138         }
139     };
140 
141     @VisibleForTesting
142     final ConfirmationDialogFragment.ConfirmListener mDisableConfirmListener =
143             new ConfirmationDialogFragment.ConfirmListener() {
144                 @Override
145                 public void onConfirm(@Nullable Bundle arguments) {
146                     mPm.setApplicationEnabledSetting(mPackageName,
147                             PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER, /* flags= */ 0);
148                     updateUninstallButtonInner(false);
149                 }
150             };
151 
152     private final View.OnClickListener mDisableClickListener = i -> {
153         ConfirmationDialogFragment dialogFragment =
154                 new ConfirmationDialogFragment.Builder(getContext())
155                         .setMessage(getContext().getString(R.string.app_disable_dialog_text))
156                         .setPositiveButton(R.string.app_disable_dialog_positive,
157                                 mDisableConfirmListener)
158                         .setNegativeButton(android.R.string.cancel, /* rejectListener= */ null)
159                         .build();
160         getFragmentController().showDialog(dialogFragment, DISABLE_CONFIRM_DIALOG_TAG);
161     };
162 
163     private final View.OnClickListener mEnableClickListener = i -> {
164         mPm.setApplicationEnabledSetting(mPackageName,
165                 PackageManager.COMPONENT_ENABLED_STATE_DEFAULT, /* flags= */ 0);
166         updateUninstallButtonInner(true);
167     };
168 
169     private final View.OnClickListener mUninstallClickListener = i -> {
170         Uri packageUri = Uri.parse("package:" + mPackageName);
171         Intent uninstallIntent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE, packageUri);
172         uninstallIntent.putExtra(Intent.EXTRA_RETURN_RESULT, true);
173         getFragmentController().startActivityForResult(uninstallIntent, UNINSTALL_REQUEST_CODE,
174                 /* callback= */ this);
175     };
176 
177     private final ApplicationsState.Callbacks mApplicationStateCallbacks =
178             new ApplicationsState.Callbacks() {
179                 @Override
180                 public void onRunningStateChanged(boolean running) {
181                 }
182 
183                 @Override
184                 public void onPackageListChanged() {
185                     refreshUi();
186                 }
187 
188                 @Override
189                 public void onRebuildComplete(ArrayList<ApplicationsState.AppEntry> apps) {
190                 }
191 
192                 @Override
193                 public void onPackageIconChanged() {
194                 }
195 
196                 @Override
197                 public void onPackageSizeChanged(String packageName) {
198                 }
199 
200                 @Override
201                 public void onAllSizesComputed() {
202                 }
203 
204                 @Override
205                 public void onLauncherInfoChanged() {
206                 }
207 
208                 @Override
209                 public void onLoadEntriesCompleted() {
210                 }
211             };
212 
ApplicationActionButtonsPreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions uxRestrictions)213     public ApplicationActionButtonsPreferenceController(Context context, String preferenceKey,
214             FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
215         super(context, preferenceKey, fragmentController, uxRestrictions);
216         mDpm = context.getSystemService(DevicePolicyManager.class);
217         mPm = context.getPackageManager();
218         mUserManager = UserManager.get(context);
219         mUserHelper = UserHelper.getInstance(context);
220     }
221 
222     @Override
getPreferenceType()223     protected Class<ActionButtonsPreference> getPreferenceType() {
224         return ActionButtonsPreference.class;
225     }
226 
227     /** Sets the {@link ApplicationsState.AppEntry} which is used to load the app name and icon. */
setAppEntry( ApplicationsState.AppEntry appEntry)228     public ApplicationActionButtonsPreferenceController setAppEntry(
229             ApplicationsState.AppEntry appEntry) {
230         mAppEntry = appEntry;
231         return this;
232     }
233 
234     /** Sets the {@link ApplicationsState} which is used to load the app name and icon. */
setAppState( ApplicationsState applicationsState)235     public ApplicationActionButtonsPreferenceController setAppState(
236             ApplicationsState applicationsState) {
237         mApplicationsState = applicationsState;
238         return this;
239     }
240 
241     /**
242      * Set the packageName, which is used to perform actions on a particular package.
243      */
setPackageName(String packageName)244     public ApplicationActionButtonsPreferenceController setPackageName(String packageName) {
245         mPackageName = packageName;
246         return this;
247     }
248 
249     @Override
checkInitialized()250     protected void checkInitialized() {
251         if (mAppEntry == null || mApplicationsState == null || mPackageName == null) {
252             throw new IllegalStateException(
253                     "AppEntry, AppState, and PackageName should be set before calling this "
254                             + "function");
255         }
256     }
257 
258     @Override
onCreateInternal()259     protected void onCreateInternal() {
260         ConfirmationDialogFragment.resetListeners(
261                 (ConfirmationDialogFragment) getFragmentController().findDialogByTag(
262                         DISABLE_CONFIRM_DIALOG_TAG),
263                 mDisableConfirmListener,
264                 /* rejectListener= */ null,
265                 /* neutralListener= */ null);
266         ConfirmationDialogFragment.resetListeners(
267                 (ConfirmationDialogFragment) getFragmentController().findDialogByTag(
268                         FORCE_STOP_CONFIRM_DIALOG_TAG),
269                 mForceStopConfirmListener,
270                 /* rejectListener= */ null,
271                 /* neutralListener= */ null);
272         getPreference().getButton(ActionButtons.BUTTON2)
273                 .setText(R.string.force_stop)
274                 .setIcon(R.drawable.ic_warning)
275                 .setOnClickListener(mForceStopClickListener)
276                 .setEnabled(false);
277         mSession = mApplicationsState.newSession(mApplicationStateCallbacks);
278     }
279 
280     @Override
onStartInternal()281     protected void onStartInternal() {
282         mSession.onResume();
283     }
284 
285     @Override
onStopInternal()286     protected void onStopInternal() {
287         mSession.onPause();
288     }
289 
290     @Override
updateState(ActionButtonsPreference preference)291     protected void updateState(ActionButtonsPreference preference) {
292         refreshAppEntry();
293         if (mAppEntry == null) {
294             getFragmentController().goBack();
295             return;
296         }
297         updateForceStopButton();
298         updateUninstallButton();
299     }
300 
refreshAppEntry()301     private void refreshAppEntry() {
302         mAppEntry = mApplicationsState.getEntry(mPackageName, UserHandle.myUserId());
303         if (mAppEntry != null) {
304             try {
305                 mPackageInfo = mPm.getPackageInfo(mPackageName,
306                         PackageManager.MATCH_DISABLED_COMPONENTS | PackageManager.MATCH_ANY_USER
307                                 | PackageManager.GET_SIGNATURES | PackageManager.GET_PERMISSIONS);
308             } catch (PackageManager.NameNotFoundException e) {
309                 LOG.e("Exception when retrieving package:" + mPackageName, e);
310                 mPackageInfo = null;
311             }
312         } else {
313             mPackageInfo = null;
314         }
315     }
316 
updateForceStopButton()317     private void updateForceStopButton() {
318         if (mDpm.packageHasActiveAdmins(mPackageName)) {
319             updateForceStopButtonInner(/* enabled= */ false);
320         } else if ((mAppEntry.info.flags & ApplicationInfo.FLAG_STOPPED) == 0) {
321             // If the app isn't explicitly stopped, then always show the force stop button.
322             updateForceStopButtonInner(/* enabled= */ true);
323         } else {
324             Intent intent = new Intent(Intent.ACTION_QUERY_PACKAGE_RESTART,
325                     Uri.fromParts("package", mPackageName, /* fragment= */ null));
326             intent.putExtra(Intent.EXTRA_PACKAGES, new String[]{mPackageName});
327             intent.putExtra(Intent.EXTRA_UID, mAppEntry.info.uid);
328             intent.putExtra(Intent.EXTRA_USER_HANDLE,
329                     UserHandle.getUserId(mAppEntry.info.uid));
330             LOG.d("Sending broadcast to query restart status for " + mPackageName);
331             getContext().sendOrderedBroadcastAsUser(intent,
332                     UserHandle.CURRENT,
333                     /* receiverPermission= */ null,
334                     mCheckKillProcessesReceiver,
335                     /* scheduler= */ null,
336                     Activity.RESULT_CANCELED,
337                     /* initialData= */ null,
338                     /* initialExtras= */ null);
339         }
340     }
341 
updateForceStopButtonInner(boolean enabled)342     private void updateForceStopButtonInner(boolean enabled) {
343         getPreference().getButton(ActionButtons.BUTTON2).setEnabled(
344                 enabled && !mUserManager.hasUserRestriction(UserManager.DISALLOW_APPS_CONTROL));
345     }
346 
updateUninstallButtonInner(boolean isAppEnabled)347     private void updateUninstallButtonInner(boolean isAppEnabled) {
348         ActionButtonInfo uninstallButton = getPreference().getButton(ActionButtons.BUTTON1);
349         if (isBundledApp()) {
350             if (isAppEnabled) {
351                 uninstallButton.setText(R.string.disable_text).setIcon(
352                         R.drawable.ic_block).setOnClickListener(mDisableClickListener);
353             } else {
354                 uninstallButton.setText(R.string.enable_text).setIcon(
355                         R.drawable.ic_check_circle).setOnClickListener(mEnableClickListener);
356             }
357         } else {
358             uninstallButton.setText(R.string.uninstall_text).setIcon(
359                     R.drawable.ic_delete).setOnClickListener(mUninstallClickListener);
360         }
361 
362         uninstallButton.setEnabled(!shouldDisableUninstallButton());
363     }
364 
updateUninstallButton()365     private void updateUninstallButton() {
366         updateUninstallButtonInner(isAppEnabled());
367     }
368 
shouldDisableUninstallButton()369     private boolean shouldDisableUninstallButton() {
370         if (shouldDisableUninstallForHomeApp()) {
371             LOG.d("Uninstall disabled for home app");
372             return true;
373         }
374 
375         if (isAppEnabled() && isKeepEnabledPackage(getContext(), mPackageName)) {
376             LOG.d("Disable button disabled for keep enabled package");
377             return true;
378         }
379 
380         if (Utils.isSystemPackage(getContext().getResources(), mPm, mPackageInfo)) {
381             LOG.d("Uninstall disabled for system package");
382             return true;
383         }
384 
385         if (mDpm.packageHasActiveAdmins(mPackageName)) {
386             LOG.d("Uninstall disabled because package has active admins");
387             return true;
388         }
389 
390         // We don't allow uninstalling profile/device owner on any user because if it's a system
391         // app, "uninstall" is actually "downgrade to the system version + disable", and
392         // "downgrade" will clear data on all users.
393         if (isProfileOrDeviceOwner(mPackageName, mDpm, mUserHelper)) {
394             LOG.d("Uninstall disabled because package is profile or device owner");
395             return true;
396         }
397 
398         if (mDpm.isUninstallInQueue(mPackageName)) {
399             LOG.d("Uninstall disabled because intent is already queued");
400             return true;
401         }
402 
403         if (mUserManager.hasUserRestriction(UserManager.DISALLOW_APPS_CONTROL)) {
404             LOG.d("Uninstall disabled because user has DISALLOW_APPS_CONTROL restriction");
405             return true;
406         }
407 
408         if (mUserManager.hasUserRestriction(UserManager.DISALLOW_UNINSTALL_APPS)) {
409             LOG.d("Uninstall disabled because user has DISALLOW_UNINSTALL_APPS restriction");
410             return true;
411         }
412 
413         return false;
414     }
415 
416     /**
417      * Returns {@code true} if the package is a Home app that should not be uninstalled. We don't
418      * risk downgrading bundled home apps because that can interfere with home-key resolution. We
419      * can't allow removal of the only home app, and we don't want to allow removal of an
420      * explicitly preferred home app. The user can go to Home settings and pick a different app,
421      * after which we'll permit removal of the now-not-default app.
422      */
shouldDisableUninstallForHomeApp()423     private boolean shouldDisableUninstallForHomeApp() {
424         Set<String> homePackages = new ArraySet<>();
425         // Get list of "home" apps and trace through any meta-data references.
426         List<ResolveInfo> homeActivities = new ArrayList<>();
427         ComponentName currentDefaultHome = mPm.getHomeActivities(homeActivities);
428         for (int i = 0; i < homeActivities.size(); i++) {
429             ResolveInfo ri = homeActivities.get(i);
430             String activityPkg = ri.activityInfo.packageName;
431             homePackages.add(activityPkg);
432 
433             // Also make sure to include anything proxying for the home app.
434             Bundle metadata = ri.activityInfo.metaData;
435             if (metadata != null) {
436                 String metaPkg = metadata.getString(ActivityManager.META_HOME_ALTERNATE);
437                 if (signaturesMatch(metaPkg, activityPkg)) {
438                     homePackages.add(metaPkg);
439                 }
440             }
441         }
442 
443         if (homePackages.contains(mPackageName)) {
444             if (isBundledApp()) {
445                 // Don't risk a downgrade.
446                 return true;
447             } else if (currentDefaultHome == null) {
448                 // No preferred default. Permit uninstall only when there is more than one
449                 // candidate.
450                 return (homePackages.size() == 1);
451             } else {
452                 // Explicit default home app. Forbid uninstall of that one, but permit it for
453                 // installed-but-inactive ones.
454                 return mPackageName.equals(currentDefaultHome.getPackageName());
455             }
456         } else {
457             // Not a home app.
458             return false;
459         }
460     }
461 
signaturesMatch(String pkg1, String pkg2)462     private boolean signaturesMatch(String pkg1, String pkg2) {
463         if (pkg1 != null && pkg2 != null) {
464             try {
465                 int match = mPm.checkSignatures(pkg1, pkg2);
466                 if (match >= PackageManager.SIGNATURE_MATCH) {
467                     return true;
468                 }
469             } catch (Exception e) {
470                 // e.g. package not found during lookup. Possibly bad input.
471                 // Just return false as this isn't a reason to crash given the use case.
472             }
473         }
474         return false;
475     }
476 
isBundledApp()477     private boolean isBundledApp() {
478         return (mAppEntry.info.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
479     }
480 
isAppEnabled()481     private boolean isAppEnabled() {
482         return mAppEntry.info.enabled && !(mAppEntry.info.enabledSetting
483                 == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED);
484     }
485 
486     @Override
processActivityResult(int requestCode, int resultCode, @Nullable Intent data)487     public void processActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
488         if (requestCode == UNINSTALL_REQUEST_CODE) {
489             if (resultCode == RESULT_OK) {
490                 getFragmentController().goBack();
491             } else {
492                 LOG.e("Uninstall failed with result " + resultCode);
493             }
494         }
495     }
496 }
497