AppActionButtonPreferenceController.java revision 70aaa94bb2e02b3e8ff1c920bc93c5daf8889141
1/*
2 * Copyright (C) 2017 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
17package com.android.settings.applications.appinfo;
18
19import android.app.Activity;
20import android.app.ActivityManager;
21import android.app.admin.DevicePolicyManager;
22import android.content.BroadcastReceiver;
23import android.content.ComponentName;
24import android.content.Context;
25import android.content.Intent;
26import android.content.pm.ApplicationInfo;
27import android.content.pm.PackageInfo;
28import android.content.pm.PackageManager;
29import android.content.pm.ResolveInfo;
30import android.net.Uri;
31import android.os.Bundle;
32import android.os.RemoteException;
33import android.os.ServiceManager;
34import android.os.UserHandle;
35import android.os.UserManager;
36import android.support.annotation.VisibleForTesting;
37import android.support.v7.preference.PreferenceScreen;
38import android.util.Log;
39import android.webkit.IWebViewUpdateService;
40
41import com.android.settings.R;
42import com.android.settings.Utils;
43import com.android.settings.applications.ApplicationFeatureProvider;
44import com.android.settings.core.BasePreferenceController;
45import com.android.settings.overlay.FeatureFactory;
46import com.android.settings.widget.ActionButtonPreference;
47import com.android.settings.wrapper.DevicePolicyManagerWrapper;
48import com.android.settingslib.RestrictedLockUtils;
49import com.android.settingslib.applications.AppUtils;
50import com.android.settingslib.applications.ApplicationsState.AppEntry;
51
52import java.util.ArrayList;
53import java.util.HashSet;
54import java.util.List;
55
56public class AppActionButtonPreferenceController extends BasePreferenceController
57        implements AppInfoDashboardFragment.Callback {
58
59    private static final String TAG = "AppActionButtonControl";
60    private static final String KEY_ACTION_BUTTONS = "action_buttons";
61
62    @VisibleForTesting
63    ActionButtonPreference mActionButtons;
64    private final AppInfoDashboardFragment mParent;
65    private final String mPackageName;
66    private final HashSet<String> mHomePackages = new HashSet<>();
67    private final ApplicationFeatureProvider mApplicationFeatureProvider;
68
69    private int mUserId;
70    private DevicePolicyManagerWrapper mDpm;
71    private UserManager mUserManager;
72    private PackageManager mPm;
73
74    private final BroadcastReceiver mCheckKillProcessesReceiver = new BroadcastReceiver() {
75        @Override
76        public void onReceive(Context context, Intent intent) {
77            final boolean enabled = getResultCode() != Activity.RESULT_CANCELED;
78            Log.d(TAG, "Got broadcast response: Restart status for "
79                    + mParent.getAppEntry().info.packageName + " " + enabled);
80            updateForceStopButton(enabled);
81        }
82    };
83
84    public AppActionButtonPreferenceController(Context context, AppInfoDashboardFragment parent,
85            String packageName) {
86        super(context, KEY_ACTION_BUTTONS);
87        mParent = parent;
88        mPackageName = packageName;
89        mUserId = UserHandle.myUserId();
90        mApplicationFeatureProvider = FeatureFactory.getFactory(context)
91                .getApplicationFeatureProvider(context);
92    }
93
94    @Override
95    public int getAvailabilityStatus() {
96        return AVAILABLE;
97    }
98
99    @Override
100    public void displayPreference(PreferenceScreen screen) {
101        super.displayPreference(screen);
102        mActionButtons = ((ActionButtonPreference) screen.findPreference(KEY_ACTION_BUTTONS))
103                .setButton2Text(R.string.force_stop)
104                .setButton2Positive(false)
105                .setButton2Enabled(false);
106    }
107
108    @Override
109    public void refreshUi() {
110        if (mPm == null) {
111            mPm = mContext.getPackageManager();
112        }
113        if (mDpm == null) {
114            mDpm = new DevicePolicyManagerWrapper(
115                    (DevicePolicyManager) mContext.getSystemService(Context.DEVICE_POLICY_SERVICE));
116        }
117        if (mUserManager == null) {
118            mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
119        }
120        final AppEntry appEntry = mParent.getAppEntry();
121        final PackageInfo packageInfo = mParent.getPackageInfo();
122
123        // Get list of "home" apps and trace through any meta-data references
124        final List<ResolveInfo> homeActivities = new ArrayList<ResolveInfo>();
125        mPm.getHomeActivities(homeActivities);
126        mHomePackages.clear();
127        for (int i = 0; i< homeActivities.size(); i++) {
128            final ResolveInfo ri = homeActivities.get(i);
129            final String activityPkg = ri.activityInfo.packageName;
130            mHomePackages.add(activityPkg);
131
132            // Also make sure to include anything proxying for the home app
133            final Bundle metadata = ri.activityInfo.metaData;
134            if (metadata != null) {
135                final String metaPkg = metadata.getString(ActivityManager.META_HOME_ALTERNATE);
136                if (signaturesMatch(metaPkg, activityPkg)) {
137                    mHomePackages.add(metaPkg);
138                }
139            }
140        }
141
142        checkForceStop(appEntry, packageInfo);
143        initUninstallButtons(appEntry, packageInfo);
144    }
145
146    @VisibleForTesting
147    void initUninstallButtons(AppEntry appEntry, PackageInfo packageInfo) {
148        final boolean isBundled = (appEntry.info.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
149        boolean enabled;
150        if (isBundled) {
151            enabled = handleDisableable(appEntry, packageInfo);
152        } else {
153            enabled = initUninstallButtonForUserApp();
154        }
155        // If this is a device admin, it can't be uninstalled or disabled.
156        // We do this here so the text of the button is still set correctly.
157        if (isBundled && mDpm.packageHasActiveAdmins(packageInfo.packageName)) {
158            enabled = false;
159        }
160
161        // We don't allow uninstalling DO/PO on *any* users, because if it's a system app,
162        // "uninstall" is actually "downgrade to the system version + disable", and "downgrade"
163        // will clear data on all users.
164        if (Utils.isProfileOrDeviceOwner(mUserManager, mDpm, packageInfo.packageName)) {
165            enabled = false;
166        }
167
168        // Don't allow uninstalling the device provisioning package.
169        if (Utils.isDeviceProvisioningPackage(mContext.getResources(), appEntry.info.packageName)) {
170            enabled = false;
171        }
172
173        // If the uninstall intent is already queued, disable the uninstall button
174        if (mDpm.isUninstallInQueue(mPackageName)) {
175            enabled = false;
176        }
177
178        // Home apps need special handling.  Bundled ones we don't risk downgrading
179        // because that can interfere with home-key resolution.  Furthermore, we
180        // can't allow uninstallation of the only home app, and we don't want to
181        // allow uninstallation of an explicitly preferred one -- the user can go
182        // to Home settings and pick a different one, after which we'll permit
183        // uninstallation of the now-not-default one.
184        if (enabled && mHomePackages.contains(packageInfo.packageName)) {
185            if (isBundled) {
186                enabled = false;
187            } else {
188                ArrayList<ResolveInfo> homeActivities = new ArrayList<ResolveInfo>();
189                ComponentName currentDefaultHome  = mPm.getHomeActivities(homeActivities);
190                if (currentDefaultHome == null) {
191                    // No preferred default, so permit uninstall only when
192                    // there is more than one candidate
193                    enabled = (mHomePackages.size() > 1);
194                } else {
195                    // There is an explicit default home app -- forbid uninstall of
196                    // that one, but permit it for installed-but-inactive ones.
197                    enabled = !packageInfo.packageName.equals(currentDefaultHome.getPackageName());
198                }
199            }
200        }
201
202        if (RestrictedLockUtils.hasBaseUserRestriction(
203                mContext, UserManager.DISALLOW_APPS_CONTROL, mUserId)) {
204            enabled = false;
205        }
206
207        try {
208            final IWebViewUpdateService webviewUpdateService =
209                    IWebViewUpdateService.Stub.asInterface(
210                            ServiceManager.getService("webviewupdate"));
211            if (webviewUpdateService.isFallbackPackage(appEntry.info.packageName)) {
212                enabled = false;
213            }
214        } catch (RemoteException e) {
215            throw new RuntimeException(e);
216        }
217
218        mActionButtons.setButton1Enabled(enabled);
219        if (enabled) {
220            // Register listener
221            mActionButtons.setButton1OnClickListener(v -> mParent.handleUninstallButtonClick());
222        }
223    }
224
225    @VisibleForTesting
226    boolean initUninstallButtonForUserApp() {
227        boolean enabled = true;
228        final PackageInfo packageInfo = mParent.getPackageInfo();
229        if ((packageInfo.applicationInfo.flags & ApplicationInfo.FLAG_INSTALLED) == 0
230                && mUserManager.getUsers().size() >= 2) {
231            // When we have multiple users, there is a separate menu
232            // to uninstall for all users.
233            enabled = false;
234        } else if (AppUtils.isInstant(packageInfo.applicationInfo)) {
235            enabled = false;
236            mActionButtons.setButton1Visible(false);
237        }
238        mActionButtons.setButton1Text(R.string.uninstall_text).setButton1Positive(false);
239        return enabled;
240    }
241
242    @VisibleForTesting
243    boolean handleDisableable(AppEntry appEntry, PackageInfo packageInfo) {
244        boolean disableable = false;
245        // Try to prevent the user from bricking their phone
246        // by not allowing disabling of apps signed with the
247        // system cert and any launcher app in the system.
248        if (mHomePackages.contains(appEntry.info.packageName)
249                || Utils.isSystemPackage(mContext.getResources(), mPm, packageInfo)) {
250            // Disable button for core system applications.
251            mActionButtons
252                    .setButton1Text(R.string.disable_text)
253                    .setButton1Positive(false);
254        } else if (appEntry.info.enabled && appEntry.info.enabledSetting
255                != PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED) {
256            mActionButtons
257                    .setButton1Text(R.string.disable_text)
258                    .setButton1Positive(false);
259            disableable = !mApplicationFeatureProvider.getKeepEnabledPackages()
260                    .contains(appEntry.info.packageName);
261        } else {
262            mActionButtons
263                    .setButton1Text(R.string.enable_text)
264                    .setButton1Positive(true);
265            disableable = true;
266        }
267
268        return disableable;
269    }
270
271    private void updateForceStopButton(boolean enabled) {
272        final boolean disallowedBySystem = RestrictedLockUtils.hasBaseUserRestriction(
273                mContext, UserManager.DISALLOW_APPS_CONTROL, mUserId);
274        mActionButtons
275                .setButton2Enabled(disallowedBySystem ? false : enabled)
276                .setButton2OnClickListener(
277                        disallowedBySystem ? null : v -> mParent.handleForceStopButtonClick());
278    }
279
280    void checkForceStop(AppEntry appEntry, PackageInfo packageInfo) {
281        if (mDpm.packageHasActiveAdmins(packageInfo.packageName)) {
282            // User can't force stop device admin.
283            Log.w(TAG, "User can't force stop device admin");
284            updateForceStopButton(false);
285        } else if (AppUtils.isInstant(packageInfo.applicationInfo)) {
286            updateForceStopButton(false);
287            mActionButtons.setButton2Visible(false);
288        } else if ((appEntry.info.flags & ApplicationInfo.FLAG_STOPPED) == 0) {
289            // If the app isn't explicitly stopped, then always show the
290            // force stop button.
291            Log.w(TAG, "App is not explicitly stopped");
292            updateForceStopButton(true);
293        } else {
294            final Intent intent = new Intent(Intent.ACTION_QUERY_PACKAGE_RESTART,
295                    Uri.fromParts("package", appEntry.info.packageName, null));
296            intent.putExtra(Intent.EXTRA_PACKAGES, new String[] { appEntry.info.packageName });
297            intent.putExtra(Intent.EXTRA_UID, appEntry.info.uid);
298            intent.putExtra(Intent.EXTRA_USER_HANDLE, UserHandle.getUserId(appEntry.info.uid));
299            Log.d(TAG, "Sending broadcast to query restart status for "
300                    + appEntry.info.packageName);
301            mContext.sendOrderedBroadcastAsUser(intent, UserHandle.CURRENT, null,
302                    mCheckKillProcessesReceiver, null, Activity.RESULT_CANCELED, null, null);
303        }
304    }
305
306    private boolean signaturesMatch(String pkg1, String pkg2) {
307        if (pkg1 != null && pkg2 != null) {
308            try {
309                return mPm.checkSignatures(pkg1, pkg2) >= PackageManager.SIGNATURE_MATCH;
310            } catch (Exception e) {
311                // e.g. named alternate package not found during lookup;
312                // this is an expected case sometimes
313            }
314        }
315        return false;
316    }
317
318}
319