1 /*
2  * Copyright (C) 2018 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 
24 import android.app.Activity;
25 import android.app.ActivityManager;
26 import android.app.admin.DevicePolicyManager;
27 import android.car.userlib.CarUserManagerHelper;
28 import android.content.BroadcastReceiver;
29 import android.content.ComponentName;
30 import android.content.Context;
31 import android.content.Intent;
32 import android.content.pm.ApplicationInfo;
33 import android.content.pm.PackageInfo;
34 import android.content.pm.PackageManager;
35 import android.content.pm.ResolveInfo;
36 import android.net.Uri;
37 import android.os.Bundle;
38 import android.os.UserHandle;
39 import android.os.UserManager;
40 import android.util.ArraySet;
41 
42 import androidx.annotation.Nullable;
43 import androidx.annotation.VisibleForTesting;
44 import androidx.annotation.XmlRes;
45 
46 import com.android.car.settings.R;
47 import com.android.car.settings.common.ActivityResultCallback;
48 import com.android.car.settings.common.ConfirmationDialogFragment;
49 import com.android.car.settings.common.Logger;
50 import com.android.car.settings.common.SettingsFragment;
51 import com.android.car.ui.toolbar.MenuItem;
52 import com.android.settingslib.Utils;
53 import com.android.settingslib.applications.ApplicationsState;
54 
55 import java.util.ArrayList;
56 import java.util.Arrays;
57 import java.util.List;
58 import java.util.Set;
59 
60 /**
61  * Shows details about an application and action associated with that application, like uninstall,
62  * forceStop.
63  *
64  * <p>To uninstall an app, it must <i>not</i> be:
65  * <ul>
66  * <li>a system bundled app
67  * <li>system signed
68  * <li>managed by an active admin from a device policy
69  * <li>a device or profile owner
70  * <li>the only home app
71  * <li>the default home app
72  * <li>for a user with the {@link UserManager#DISALLOW_APPS_CONTROL} restriction
73  * <li>for a user with the {@link UserManager#DISALLOW_UNINSTALL_APPS} restriction
74  * </ul>
75  *
76  * <p>For apps that cannot be uninstalled, a disable option is shown instead (or enable if the app
77  * is already disabled).
78  */
79 public class ApplicationDetailsFragment extends SettingsFragment implements ActivityResultCallback {
80     private static final Logger LOG = new Logger(ApplicationDetailsFragment.class);
81     public static final String EXTRA_PACKAGE_NAME = "extra_package_name";
82 
83     @VisibleForTesting
84     static final String DISABLE_CONFIRM_DIALOG_TAG =
85             "com.android.car.settings.applications.DisableConfirmDialog";
86     @VisibleForTesting
87     static final String FORCE_STOP_CONFIRM_DIALOG_TAG =
88             "com.android.car.settings.applications.ForceStopConfirmDialog";
89     @VisibleForTesting
90     static final int UNINSTALL_REQUEST_CODE = 10;
91 
92     private DevicePolicyManager mDpm;
93     private PackageManager mPm;
94     private CarUserManagerHelper mCarUserManagerHelper;
95 
96     private String mPackageName;
97     private PackageInfo mPackageInfo;
98     private ApplicationsState mAppState;
99     private ApplicationsState.Session mSession;
100     private ApplicationsState.AppEntry mAppEntry;
101 
102     // The function of this button depends on which app is shown and the app's current state.
103     // It is an application enable/disable toggle for apps bundled with the system image.
104     private MenuItem mUninstallButton;
105     private MenuItem mForceStopButton;
106 
107     /** Creates an instance of this fragment, passing packageName as an argument. */
getInstance(String packageName)108     public static ApplicationDetailsFragment getInstance(String packageName) {
109         ApplicationDetailsFragment applicationDetailFragment = new ApplicationDetailsFragment();
110         Bundle bundle = new Bundle();
111         bundle.putString(EXTRA_PACKAGE_NAME, packageName);
112         applicationDetailFragment.setArguments(bundle);
113         return applicationDetailFragment;
114     }
115 
116     @Override
getToolbarMenuItems()117     public List<MenuItem> getToolbarMenuItems() {
118         return Arrays.asList(mUninstallButton, mForceStopButton);
119     }
120 
121     @Override
122     @XmlRes
getPreferenceScreenResId()123     protected int getPreferenceScreenResId() {
124         return R.xml.application_details_fragment;
125     }
126 
127     @Override
onAttach(Context context)128     public void onAttach(Context context) {
129         super.onAttach(context);
130         mDpm = (DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE);
131         mPm = context.getPackageManager();
132         mCarUserManagerHelper = new CarUserManagerHelper(context);
133 
134         // These should be loaded before onCreate() so that the controller operates as expected.
135         mPackageName = getArguments().getString(EXTRA_PACKAGE_NAME);
136 
137         mAppState = ApplicationsState.getInstance(requireActivity().getApplication());
138         mSession = mAppState.newSession(mApplicationStateCallbacks, getLifecycle());
139 
140         retrieveAppEntry();
141 
142         use(ApplicationPreferenceController.class,
143                 R.string.pk_application_details_app)
144                 .setAppEntry(mAppEntry).setAppState(mAppState);
145         use(NotificationsPreferenceController.class,
146                 R.string.pk_application_details_notifications).setPackageInfo(mPackageInfo);
147         use(PermissionsPreferenceController.class,
148                 R.string.pk_application_details_permissions).setPackageName(mPackageName);
149         use(StoragePreferenceController.class,
150                 R.string.pk_application_details_storage)
151                 .setAppEntry(mAppEntry).setPackageName(mPackageName);
152         use(VersionPreferenceController.class,
153                 R.string.pk_application_details_version).setPackageInfo(mPackageInfo);
154     }
155 
156     @Override
onCreate(Bundle savedInstanceState)157     public void onCreate(Bundle savedInstanceState) {
158         super.onCreate(savedInstanceState);
159         ConfirmationDialogFragment.resetListeners(
160                 (ConfirmationDialogFragment) findDialogByTag(DISABLE_CONFIRM_DIALOG_TAG),
161                 mDisableConfirmListener, /* rejectListener= */ null);
162         ConfirmationDialogFragment.resetListeners(
163                 (ConfirmationDialogFragment) findDialogByTag(FORCE_STOP_CONFIRM_DIALOG_TAG),
164                 mForceStopConfirmListener, /* rejectListener= */ null);
165 
166         mUninstallButton = new MenuItem.Builder(getContext()).build();
167         mForceStopButton = new MenuItem.Builder(getContext())
168                 .setTitle(R.string.force_stop)
169                 .setOnClickListener(mForceStopClickListener)
170                 .setEnabled(false)
171                 .build();
172     }
173 
174     @Override
onStart()175     public void onStart() {
176         super.onStart();
177         // Resume the session earlier than the lifecycle so that cached information is updated
178         // even if settings is not resumed (for example in multi-display).
179         mSession.onResume();
180         refresh();
181     }
182 
183     @Override
onStop()184     public void onStop() {
185         super.onStop();
186         // Since we resume early in onStart, make sure we clean up even if we don't receive onPause.
187         mSession.onPause();
188     }
189 
refresh()190     private void refresh() {
191         retrieveAppEntry();
192         if (mAppEntry == null) {
193             goBack();
194         }
195         updateForceStopButton();
196         updateUninstallButton();
197     }
198 
retrieveAppEntry()199     private void retrieveAppEntry() {
200         mAppEntry = mAppState.getEntry(mPackageName,
201                 mCarUserManagerHelper.getCurrentProcessUserId());
202         if (mAppEntry != null) {
203             try {
204                 mPackageInfo = mPm.getPackageInfo(mPackageName,
205                         PackageManager.MATCH_DISABLED_COMPONENTS | PackageManager.MATCH_ANY_USER
206                                 | PackageManager.GET_SIGNATURES | PackageManager.GET_PERMISSIONS);
207             } catch (PackageManager.NameNotFoundException e) {
208                 LOG.e("Exception when retrieving package:" + mPackageName, e);
209                 mPackageInfo = null;
210             }
211         } else {
212             mPackageInfo = null;
213         }
214     }
215 
updateForceStopButton()216     private void updateForceStopButton() {
217         if (mDpm.packageHasActiveAdmins(mPackageName)) {
218             updateForceStopButtonInner(/* enabled= */ false);
219         } else if ((mAppEntry.info.flags & ApplicationInfo.FLAG_STOPPED) == 0) {
220             // If the app isn't explicitly stopped, then always show the force stop button.
221             updateForceStopButtonInner(/* enabled= */ true);
222         } else {
223             Intent intent = new Intent(Intent.ACTION_QUERY_PACKAGE_RESTART,
224                     Uri.fromParts("package", mPackageName, /* fragment= */ null));
225             intent.putExtra(Intent.EXTRA_PACKAGES, new String[]{mPackageName});
226             intent.putExtra(Intent.EXTRA_UID, mAppEntry.info.uid);
227             intent.putExtra(Intent.EXTRA_USER_HANDLE, UserHandle.getUserId(mAppEntry.info.uid));
228             LOG.d("Sending broadcast to query restart status for " + mPackageName);
229             requireContext().sendOrderedBroadcastAsUser(intent,
230                     UserHandle.CURRENT,
231                     /* receiverPermission= */ null,
232                     mCheckKillProcessesReceiver,
233                     /* scheduler= */ null,
234                     Activity.RESULT_CANCELED,
235                     /* initialData= */ null,
236                     /* initialExtras= */ null);
237         }
238     }
239 
updateForceStopButtonInner(boolean enabled)240     private void updateForceStopButtonInner(boolean enabled) {
241         mForceStopButton.setEnabled(
242                 enabled && !mCarUserManagerHelper.isCurrentProcessUserHasRestriction(
243                         UserManager.DISALLOW_APPS_CONTROL));
244     }
245 
updateUninstallButton()246     private void updateUninstallButton() {
247         if (isBundledApp()) {
248             if (isAppEnabled()) {
249                 mUninstallButton.setTitle(R.string.disable_text);
250                 mUninstallButton.setOnClickListener(mDisableClickListener);
251             } else {
252                 mUninstallButton.setTitle(R.string.enable_text);
253                 mUninstallButton.setOnClickListener(mEnableClickListener);
254             }
255         } else {
256             mUninstallButton.setTitle(R.string.uninstall_text);
257             mUninstallButton.setOnClickListener(mUninstallClickListener);
258         }
259 
260         mUninstallButton.setEnabled(!shouldDisableUninstallButton());
261     }
262 
shouldDisableUninstallButton()263     private boolean shouldDisableUninstallButton() {
264         if (shouldDisableUninstallForHomeApp()) {
265             LOG.d("Uninstall disabled for home app");
266             return true;
267         }
268 
269         if (isAppEnabled() && isKeepEnabledPackage(requireContext(), mPackageName)) {
270             LOG.d("Disable button disabled for keep enabled package");
271             return true;
272         }
273 
274         if (Utils.isSystemPackage(getResources(), mPm, mPackageInfo)) {
275             LOG.d("Uninstall disabled for system package");
276             return true;
277         }
278 
279         if (mDpm.packageHasActiveAdmins(mPackageName)) {
280             LOG.d("Uninstall disabled because package has active admins");
281             return true;
282         }
283 
284         // We don't allow uninstalling profile/device owner on any user because if it's a system
285         // app, "uninstall" is actually "downgrade to the system version + disable", and
286         // "downgrade" will clear data on all users.
287         if (isProfileOrDeviceOwner(mPackageName, mDpm, mCarUserManagerHelper)) {
288             LOG.d("Uninstall disabled because package is profile or device owner");
289             return true;
290         }
291 
292         if (mDpm.isUninstallInQueue(mPackageName)) {
293             LOG.d("Uninstall disabled because intent is already queued");
294             return true;
295         }
296 
297         if (mCarUserManagerHelper.isCurrentProcessUserHasRestriction(
298                 UserManager.DISALLOW_APPS_CONTROL)) {
299             LOG.d("Uninstall disabled because user has DISALLOW_APPS_CONTROL restriction");
300             return true;
301         }
302 
303         if (mCarUserManagerHelper.isCurrentProcessUserHasRestriction(
304                 UserManager.DISALLOW_UNINSTALL_APPS)) {
305             LOG.d("Uninstall disabled because user has DISALLOW_UNINSTALL_APPS restriction");
306             return true;
307         }
308 
309         return false;
310     }
311 
312     /**
313      * Returns {@code true} if the package is a Home app that should not be uninstalled. We don't
314      * risk downgrading bundled home apps because that can interfere with home-key resolution. We
315      * can't allow removal of the only home app, and we don't want to allow removal of an
316      * explicitly preferred home app. The user can go to Home settings and pick a different app,
317      * after which we'll permit removal of the now-not-default app.
318      */
shouldDisableUninstallForHomeApp()319     private boolean shouldDisableUninstallForHomeApp() {
320         Set<String> homePackages = new ArraySet<>();
321         // Get list of "home" apps and trace through any meta-data references.
322         List<ResolveInfo> homeActivities = new ArrayList<>();
323         ComponentName currentDefaultHome = mPm.getHomeActivities(homeActivities);
324         for (int i = 0; i < homeActivities.size(); i++) {
325             ResolveInfo ri = homeActivities.get(i);
326             String activityPkg = ri.activityInfo.packageName;
327             homePackages.add(activityPkg);
328 
329             // Also make sure to include anything proxying for the home app.
330             Bundle metadata = ri.activityInfo.metaData;
331             if (metadata != null) {
332                 String metaPkg = metadata.getString(ActivityManager.META_HOME_ALTERNATE);
333                 if (signaturesMatch(metaPkg, activityPkg)) {
334                     homePackages.add(metaPkg);
335                 }
336             }
337         }
338 
339         if (homePackages.contains(mPackageName)) {
340             boolean isBundledApp = (mAppEntry.info.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
341             if (isBundledApp) {
342                 // Don't risk a downgrade.
343                 return true;
344             } else if (currentDefaultHome == null) {
345                 // No preferred default. Permit uninstall only when there is more than one
346                 // candidate.
347                 return (homePackages.size() == 1);
348             } else {
349                 // Explicit default home app. Forbid uninstall of that one, but permit it for
350                 // installed-but-inactive ones.
351                 return mPackageName.equals(currentDefaultHome.getPackageName());
352             }
353         } else {
354             // Not a home app.
355             return false;
356         }
357     }
358 
signaturesMatch(String pkg1, String pkg2)359     private boolean signaturesMatch(String pkg1, String pkg2) {
360         if (pkg1 != null && pkg2 != null) {
361             try {
362                 int match = mPm.checkSignatures(pkg1, pkg2);
363                 if (match >= PackageManager.SIGNATURE_MATCH) {
364                     return true;
365                 }
366             } catch (Exception e) {
367                 // e.g. package not found during lookup. Possibly bad input.
368                 // Just return false as this isn't a reason to crash given the use case.
369             }
370         }
371         return false;
372     }
373 
isBundledApp()374     private boolean isBundledApp() {
375         return (mAppEntry.info.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
376     }
377 
isAppEnabled()378     private boolean isAppEnabled() {
379         return mAppEntry.info.enabled && !(mAppEntry.info.enabledSetting
380                 == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED);
381     }
382 
383     @Override
processActivityResult(int requestCode, int resultCode, @Nullable Intent data)384     public void processActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
385         if (requestCode == UNINSTALL_REQUEST_CODE) {
386             if (resultCode == RESULT_OK) {
387                 goBack();
388             } else {
389                 LOG.e("Uninstall failed with result " + resultCode);
390             }
391         }
392     }
393 
394     private final ConfirmationDialogFragment.ConfirmListener mForceStopConfirmListener =
395             new ConfirmationDialogFragment.ConfirmListener() {
396                 @Override
397                 public void onConfirm(@Nullable Bundle arguments) {
398                     ActivityManager am = (ActivityManager) requireContext().getSystemService(
399                             Context.ACTIVITY_SERVICE);
400                     LOG.d("Stopping package " + mPackageName);
401                     am.forceStopPackage(mPackageName);
402                     int userId = UserHandle.getUserId(mAppEntry.info.uid);
403                     mAppState.invalidatePackage(mPackageName, userId);
404                 }
405             };
406 
407     private final MenuItem.OnClickListener mForceStopClickListener = i -> {
408         ConfirmationDialogFragment dialogFragment =
409                 new ConfirmationDialogFragment.Builder(getContext())
410                         .setTitle(R.string.force_stop_dialog_title)
411                         .setMessage(R.string.force_stop_dialog_text)
412                         .setPositiveButton(android.R.string.ok,
413                                 mForceStopConfirmListener)
414                         .setNegativeButton(android.R.string.cancel, /* rejectListener= */ null)
415                         .build();
416         showDialog(dialogFragment, FORCE_STOP_CONFIRM_DIALOG_TAG);
417     };
418 
419     private final BroadcastReceiver mCheckKillProcessesReceiver = new BroadcastReceiver() {
420         @Override
421         public void onReceive(Context context, Intent intent) {
422             boolean enabled = getResultCode() != Activity.RESULT_CANCELED;
423             LOG.d("Got broadcast response: Restart status for " + mPackageName + " " + enabled);
424             updateForceStopButtonInner(enabled);
425         }
426     };
427 
428     private final ConfirmationDialogFragment.ConfirmListener mDisableConfirmListener =
429             new ConfirmationDialogFragment.ConfirmListener() {
430                 @Override
431                 public void onConfirm(@Nullable Bundle arguments) {
432                     mPm.setApplicationEnabledSetting(mPackageName,
433                             PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER, /* flags= */ 0);
434                 }
435             };
436 
437     private final MenuItem.OnClickListener mDisableClickListener = i -> {
438         ConfirmationDialogFragment dialogFragment =
439                 new ConfirmationDialogFragment.Builder(getContext())
440                         .setMessage(getString(R.string.app_disable_dialog_text))
441                         .setPositiveButton(R.string.app_disable_dialog_positive,
442                                 mDisableConfirmListener)
443                         .setNegativeButton(android.R.string.cancel, /* rejectListener= */ null)
444                         .build();
445         showDialog(dialogFragment, DISABLE_CONFIRM_DIALOG_TAG);
446     };
447 
448     private final MenuItem.OnClickListener mEnableClickListener = i -> {
449         mPm.setApplicationEnabledSetting(mPackageName,
450                 PackageManager.COMPONENT_ENABLED_STATE_DEFAULT, /* flags= */ 0);
451     };
452 
453     private final MenuItem.OnClickListener mUninstallClickListener = i -> {
454         Uri packageUri = Uri.parse("package:" + mPackageName);
455         Intent uninstallIntent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE, packageUri);
456         uninstallIntent.putExtra(Intent.EXTRA_UNINSTALL_ALL_USERS, true);
457         uninstallIntent.putExtra(Intent.EXTRA_RETURN_RESULT, true);
458         startActivityForResult(uninstallIntent, UNINSTALL_REQUEST_CODE, /* callback= */
459                 ApplicationDetailsFragment.this);
460     };
461 
462     private final ApplicationsState.Callbacks mApplicationStateCallbacks =
463             new ApplicationsState.Callbacks() {
464                 @Override
465                 public void onRunningStateChanged(boolean running) {
466                 }
467 
468                 @Override
469                 public void onPackageListChanged() {
470                     refresh();
471                 }
472 
473                 @Override
474                 public void onRebuildComplete(ArrayList<ApplicationsState.AppEntry> apps) {
475                 }
476 
477                 @Override
478                 public void onPackageIconChanged() {
479                 }
480 
481                 @Override
482                 public void onPackageSizeChanged(String packageName) {
483                 }
484 
485                 @Override
486                 public void onAllSizesComputed() {
487                 }
488 
489                 @Override
490                 public void onLauncherInfoChanged() {
491                 }
492 
493                 @Override
494                 public void onLoadEntriesCompleted() {
495                 }
496             };
497 }
498