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