AppStorageSettings.java revision 6acc0cf3cdf3e47feb5a9c99adf4e732bfec9f2b
1/*
2 * Copyright (C) 2015 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;
18
19import static android.content.pm.ApplicationInfo.FLAG_ALLOW_CLEAR_USER_DATA;
20import static android.content.pm.ApplicationInfo.FLAG_SYSTEM;
21
22import android.app.ActivityManager;
23import android.app.AlertDialog;
24import android.app.AppGlobals;
25import android.app.GrantedUriPermission;
26import android.app.LoaderManager;
27import android.content.Context;
28import android.content.DialogInterface;
29import android.content.Intent;
30import android.content.Loader;
31import android.content.pm.ApplicationInfo;
32import android.content.pm.IPackageDataObserver;
33import android.content.pm.PackageManager;
34import android.content.pm.ProviderInfo;
35import android.os.Bundle;
36import android.os.Handler;
37import android.os.Message;
38import android.os.RemoteException;
39import android.os.UserHandle;
40import android.os.storage.StorageManager;
41import android.os.storage.VolumeInfo;
42import android.support.annotation.VisibleForTesting;
43import android.support.v7.preference.Preference;
44import android.support.v7.preference.PreferenceCategory;
45import android.util.Log;
46import android.util.MutableInt;
47import android.view.View;
48import android.view.View.OnClickListener;
49import android.widget.Button;
50
51import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
52import com.android.settings.R;
53import com.android.settings.Utils;
54import com.android.settings.deviceinfo.StorageWizardMoveConfirm;
55import com.android.settings.widget.ActionButtonPreference;
56import com.android.settingslib.RestrictedLockUtils;
57import com.android.settingslib.applications.ApplicationsState.Callbacks;
58import com.android.settingslib.applications.StorageStatsSource;
59import com.android.settingslib.applications.StorageStatsSource.AppStorageStats;
60
61import java.util.Collections;
62import java.util.List;
63import java.util.Map;
64import java.util.Objects;
65import java.util.TreeMap;
66
67public class AppStorageSettings extends AppInfoWithHeader
68        implements OnClickListener, Callbacks, DialogInterface.OnClickListener,
69        LoaderManager.LoaderCallbacks<AppStorageStats> {
70    private static final String TAG = AppStorageSettings.class.getSimpleName();
71
72    //internal constants used in Handler
73    private static final int OP_SUCCESSFUL = 1;
74    private static final int OP_FAILED = 2;
75    private static final int MSG_CLEAR_USER_DATA = 1;
76    private static final int MSG_CLEAR_CACHE = 3;
77
78    // invalid size value used initially and also when size retrieval through PackageManager
79    // fails for whatever reason
80    private static final int SIZE_INVALID = -1;
81
82    // Result code identifiers
83    public static final int REQUEST_MANAGE_SPACE = 2;
84
85    private static final int DLG_CLEAR_DATA = DLG_BASE + 1;
86    private static final int DLG_CANNOT_CLEAR_DATA = DLG_BASE + 2;
87
88    private static final String KEY_STORAGE_USED = "storage_used";
89    private static final String KEY_CHANGE_STORAGE = "change_storage_button";
90    private static final String KEY_STORAGE_SPACE = "storage_space";
91    private static final String KEY_STORAGE_CATEGORY = "storage_category";
92
93    private static final String KEY_TOTAL_SIZE = "total_size";
94    private static final String KEY_APP_SIZE = "app_size";
95    private static final String KEY_DATA_SIZE = "data_size";
96    private static final String KEY_CACHE_SIZE = "cache_size";
97
98    private static final String KEY_HEADER_BUTTONS = "header_view";
99
100    private static final String KEY_URI_CATEGORY = "uri_category";
101    private static final String KEY_CLEAR_URI = "clear_uri_button";
102
103    private static final String KEY_CACHE_CLEARED = "cache_cleared";
104    private static final String KEY_DATA_CLEARED = "data_cleared";
105
106    // Views related to cache info
107    @VisibleForTesting
108    ActionButtonPreference mButtonsPref;
109
110    private Preference mStorageUsed;
111    private Button mChangeStorageButton;
112
113    // Views related to URI permissions
114    private Button mClearUriButton;
115    private LayoutPreference mClearUri;
116    private PreferenceCategory mUri;
117
118    private boolean mCanClearData = true;
119    private boolean mCacheCleared;
120    private boolean mDataCleared;
121
122    @VisibleForTesting
123    AppStorageSizesController mSizeController;
124
125    private ClearCacheObserver mClearCacheObserver;
126    private ClearUserDataObserver mClearDataObserver;
127
128    private VolumeInfo[] mCandidates;
129    private AlertDialog.Builder mDialogBuilder;
130    private ApplicationInfo mInfo;
131
132    @Override
133    public void onCreate(Bundle savedInstanceState) {
134        super.onCreate(savedInstanceState);
135        if (savedInstanceState != null) {
136            mCacheCleared = savedInstanceState.getBoolean(KEY_CACHE_CLEARED, false);
137            mDataCleared = savedInstanceState.getBoolean(KEY_DATA_CLEARED, false);
138            mCacheCleared = mCacheCleared || mDataCleared;
139        }
140
141        addPreferencesFromResource(R.xml.app_storage_settings);
142        setupViews();
143        initMoveDialog();
144    }
145
146    @Override
147    public void onResume() {
148        super.onResume();
149        updateSize();
150    }
151
152    @Override
153    public void onSaveInstanceState(Bundle outState) {
154        super.onSaveInstanceState(outState);
155        outState.putBoolean(KEY_CACHE_CLEARED, mCacheCleared);
156        outState.putBoolean(KEY_DATA_CLEARED, mDataCleared);
157    }
158
159    private void setupViews() {
160        // Set default values on sizes
161        mSizeController = new AppStorageSizesController.Builder()
162                .setTotalSizePreference(findPreference(KEY_TOTAL_SIZE))
163                .setAppSizePreference(findPreference(KEY_APP_SIZE))
164                .setDataSizePreference(findPreference(KEY_DATA_SIZE))
165                .setCacheSizePreference(findPreference(KEY_CACHE_SIZE))
166                .setComputingString(R.string.computing_size)
167                .setErrorString(R.string.invalid_size_value)
168                .build();
169        mButtonsPref = ((ActionButtonPreference) findPreference(KEY_HEADER_BUTTONS))
170                .setButton1Positive(false)
171                .setButton2Positive(false);
172
173        mStorageUsed = findPreference(KEY_STORAGE_USED);
174        mChangeStorageButton = (Button) ((LayoutPreference) findPreference(KEY_CHANGE_STORAGE))
175                .findViewById(R.id.button);
176        mChangeStorageButton.setText(R.string.change);
177        mChangeStorageButton.setOnClickListener(this);
178
179        // Cache section
180        mButtonsPref.setButton2Text(R.string.clear_cache_btn_text);
181
182        // URI permissions section
183        mUri = (PreferenceCategory) findPreference(KEY_URI_CATEGORY);
184        mClearUri = (LayoutPreference) mUri.findPreference(KEY_CLEAR_URI);
185        mClearUriButton = (Button) mClearUri.findViewById(R.id.button);
186        mClearUriButton.setText(R.string.clear_uri_btn_text);
187        mClearUriButton.setOnClickListener(this);
188    }
189
190    @VisibleForTesting
191    void handleClearCacheClick() {
192        if (mAppsControlDisallowedAdmin != null && !mAppsControlDisallowedBySystem) {
193            RestrictedLockUtils.sendShowAdminSupportDetailsIntent(
194                    getActivity(), mAppsControlDisallowedAdmin);
195            return;
196        } else if (mClearCacheObserver == null) { // Lazy initialization of observer
197            mClearCacheObserver = new ClearCacheObserver();
198        }
199        mMetricsFeatureProvider.action(getContext(),
200                MetricsEvent.ACTION_SETTINGS_CLEAR_APP_CACHE);
201        mPm.deleteApplicationCacheFiles(mPackageName, mClearCacheObserver);
202    }
203
204    @VisibleForTesting
205    void handleClearDataClick() {
206        if (mAppsControlDisallowedAdmin != null && !mAppsControlDisallowedBySystem) {
207            RestrictedLockUtils.sendShowAdminSupportDetailsIntent(
208                    getActivity(), mAppsControlDisallowedAdmin);
209        } else if (mAppEntry.info.manageSpaceActivityName != null) {
210            if (!Utils.isMonkeyRunning()) {
211                Intent intent = new Intent(Intent.ACTION_DEFAULT);
212                intent.setClassName(mAppEntry.info.packageName,
213                        mAppEntry.info.manageSpaceActivityName);
214                startActivityForResult(intent, REQUEST_MANAGE_SPACE);
215            }
216        } else {
217            showDialogInner(DLG_CLEAR_DATA, 0);
218        }
219    }
220
221    @Override
222    public void onClick(View v) {
223        if (v == mChangeStorageButton && mDialogBuilder != null && !isMoveInProgress()) {
224            mDialogBuilder.show();
225        } else if (v == mClearUriButton) {
226            if (mAppsControlDisallowedAdmin != null && !mAppsControlDisallowedBySystem) {
227                RestrictedLockUtils.sendShowAdminSupportDetailsIntent(
228                        getActivity(), mAppsControlDisallowedAdmin);
229            } else {
230                clearUriPermissions();
231            }
232        }
233    }
234
235    private boolean isMoveInProgress() {
236        try {
237            // TODO: define a cleaner API for this
238            AppGlobals.getPackageManager().checkPackageStartable(mPackageName,
239                    UserHandle.myUserId());
240            return false;
241        } catch (RemoteException | SecurityException e) {
242            return true;
243        }
244    }
245
246    @Override
247    public void onClick(DialogInterface dialog, int which) {
248        final Context context = getActivity();
249
250        // If not current volume, kick off move wizard
251        final VolumeInfo targetVol = mCandidates[which];
252        final VolumeInfo currentVol = context.getPackageManager().getPackageCurrentVolume(
253                mAppEntry.info);
254        if (!Objects.equals(targetVol, currentVol)) {
255            final Intent intent = new Intent(context, StorageWizardMoveConfirm.class);
256            intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, targetVol.getId());
257            intent.putExtra(Intent.EXTRA_PACKAGE_NAME, mAppEntry.info.packageName);
258            startActivity(intent);
259        }
260        dialog.dismiss();
261    }
262
263    @Override
264    protected boolean refreshUi() {
265        retrieveAppEntry();
266        if (mAppEntry == null) {
267            return false;
268        }
269        updateUiWithSize(mSizeController.getLastResult());
270        refreshGrantedUriPermissions();
271
272        final VolumeInfo currentVol = getActivity().getPackageManager()
273                .getPackageCurrentVolume(mAppEntry.info);
274        final StorageManager storage = getContext().getSystemService(StorageManager.class);
275        mStorageUsed.setSummary(storage.getBestVolumeDescription(currentVol));
276
277        refreshButtons();
278
279        return true;
280    }
281
282    private void refreshButtons() {
283        initMoveDialog();
284        initDataButtons();
285    }
286
287    private void initDataButtons() {
288        final boolean appHasSpaceManagementUI = mAppEntry.info.manageSpaceActivityName != null;
289        final boolean appHasActiveAdmins = mDpm.packageHasActiveAdmins(mPackageName);
290        // Check that SYSTEM_APP flag is set, and ALLOW_CLEAR_USER_DATA is not set.
291        final boolean isNonClearableSystemApp =
292                (mAppEntry.info.flags & (FLAG_SYSTEM | FLAG_ALLOW_CLEAR_USER_DATA)) == FLAG_SYSTEM;
293        final boolean appRestrictsClearingData = isNonClearableSystemApp || appHasActiveAdmins;
294
295        final Intent intent = new Intent(Intent.ACTION_DEFAULT);
296        if (appHasSpaceManagementUI) {
297            intent.setClassName(mAppEntry.info.packageName, mAppEntry.info.manageSpaceActivityName);
298        }
299        final boolean isManageSpaceActivityAvailable =
300                getPackageManager().resolveActivity(intent, 0) != null;
301
302        if ((!appHasSpaceManagementUI && appRestrictsClearingData)
303                || !isManageSpaceActivityAvailable) {
304            mButtonsPref
305                    .setButton1Text(R.string.clear_user_data_text)
306                    .setButton1Enabled(false);
307            mCanClearData = false;
308        } else {
309            if (appHasSpaceManagementUI) {
310                mButtonsPref.setButton1Text(R.string.manage_space_text);
311            } else {
312                mButtonsPref.setButton1Text(R.string.clear_user_data_text);
313            }
314            mButtonsPref
315                    .setButton1Text(R.string.clear_user_data_text)
316                    .setButton1OnClickListener(v -> handleClearDataClick());
317        }
318
319        if (mAppsControlDisallowedBySystem) {
320            mButtonsPref.setButton1Enabled(false);
321        }
322    }
323
324    private void initMoveDialog() {
325        final Context context = getActivity();
326        final StorageManager storage = context.getSystemService(StorageManager.class);
327
328        final List<VolumeInfo> candidates = context.getPackageManager()
329                .getPackageCandidateVolumes(mAppEntry.info);
330        if (candidates.size() > 1) {
331            Collections.sort(candidates, VolumeInfo.getDescriptionComparator());
332
333            CharSequence[] labels = new CharSequence[candidates.size()];
334            int current = -1;
335            for (int i = 0; i < candidates.size(); i++) {
336                final String volDescrip = storage.getBestVolumeDescription(candidates.get(i));
337                if (Objects.equals(volDescrip, mStorageUsed.getSummary())) {
338                    current = i;
339                }
340                labels[i] = volDescrip;
341            }
342            mCandidates = candidates.toArray(new VolumeInfo[candidates.size()]);
343            mDialogBuilder = new AlertDialog.Builder(getContext())
344                    .setTitle(R.string.change_storage)
345                    .setSingleChoiceItems(labels, current, this)
346                    .setNegativeButton(R.string.cancel, null);
347        } else {
348            removePreference(KEY_STORAGE_USED);
349            removePreference(KEY_CHANGE_STORAGE);
350            removePreference(KEY_STORAGE_SPACE);
351        }
352    }
353
354    /*
355     * Private method to initiate clearing user data when the user clicks the clear data
356     * button for a system package
357     */
358    private void initiateClearUserData() {
359        mMetricsFeatureProvider.action(getContext(), MetricsEvent.ACTION_SETTINGS_CLEAR_APP_DATA);
360        mButtonsPref.setButton1Enabled(false);
361        // Invoke uninstall or clear user data based on sysPackage
362        String packageName = mAppEntry.info.packageName;
363        Log.i(TAG, "Clearing user data for package : " + packageName);
364        if (mClearDataObserver == null) {
365            mClearDataObserver = new ClearUserDataObserver();
366        }
367        ActivityManager am = (ActivityManager)
368                getActivity().getSystemService(Context.ACTIVITY_SERVICE);
369        boolean res = am.clearApplicationUserData(packageName, mClearDataObserver);
370        if (!res) {
371            // Clearing data failed for some obscure reason. Just log error for now
372            Log.i(TAG, "Couldnt clear application user data for package:" + packageName);
373            showDialogInner(DLG_CANNOT_CLEAR_DATA, 0);
374        } else {
375            mButtonsPref.setButton1Text(R.string.recompute_size);
376        }
377    }
378
379    /*
380     * Private method to handle clear message notification from observer when
381     * the async operation from PackageManager is complete
382     */
383    private void processClearMsg(Message msg) {
384        int result = msg.arg1;
385        String packageName = mAppEntry.info.packageName;
386        mButtonsPref.setButton1Text(R.string.clear_user_data_text);
387        if (result == OP_SUCCESSFUL) {
388            Log.i(TAG, "Cleared user data for package : " + packageName);
389            updateSize();
390        } else {
391            mButtonsPref.setButton1Enabled(true);
392        }
393    }
394
395    private void refreshGrantedUriPermissions() {
396        // Clear UI first (in case the activity has been resumed)
397        removeUriPermissionsFromUi();
398
399        // Gets all URI permissions from am.
400        ActivityManager am = (ActivityManager) getActivity().getSystemService(
401                Context.ACTIVITY_SERVICE);
402        List<GrantedUriPermission> perms =
403                am.getGrantedUriPermissions(mAppEntry.info.packageName).getList();
404
405        if (perms.isEmpty()) {
406            mClearUriButton.setVisibility(View.GONE);
407            return;
408        }
409
410        PackageManager pm = getActivity().getPackageManager();
411
412        // Group number of URIs by app.
413        Map<CharSequence, MutableInt> uriCounters = new TreeMap<>();
414        for (GrantedUriPermission perm : perms) {
415            String authority = perm.uri.getAuthority();
416            ProviderInfo provider = pm.resolveContentProvider(authority, 0);
417            CharSequence app = provider.applicationInfo.loadLabel(pm);
418            MutableInt count = uriCounters.get(app);
419            if (count == null) {
420                uriCounters.put(app, new MutableInt(1));
421            } else {
422                count.value++;
423            }
424        }
425
426        // Dynamically add the preferences, one per app.
427        int order = 0;
428        for (Map.Entry<CharSequence, MutableInt> entry : uriCounters.entrySet()) {
429            int numberResources = entry.getValue().value;
430            Preference pref = new Preference(getPrefContext());
431            pref.setTitle(entry.getKey());
432            pref.setSummary(getPrefContext().getResources()
433                    .getQuantityString(R.plurals.uri_permissions_text, numberResources,
434                            numberResources));
435            pref.setSelectable(false);
436            pref.setLayoutResource(R.layout.horizontal_preference);
437            pref.setOrder(order);
438            Log.v(TAG, "Adding preference '" + pref + "' at order " + order);
439            mUri.addPreference(pref);
440        }
441
442        if (mAppsControlDisallowedBySystem) {
443            mClearUriButton.setEnabled(false);
444        }
445
446        mClearUri.setOrder(order);
447        mClearUriButton.setVisibility(View.VISIBLE);
448
449    }
450
451    private void clearUriPermissions() {
452        // Synchronously revoke the permissions.
453        final ActivityManager am = (ActivityManager) getActivity().getSystemService(
454                Context.ACTIVITY_SERVICE);
455        am.clearGrantedUriPermissions(mAppEntry.info.packageName);
456
457        // Update UI
458        refreshGrantedUriPermissions();
459    }
460
461    private void removeUriPermissionsFromUi() {
462        // Remove all preferences but the clear button.
463        int count = mUri.getPreferenceCount();
464        for (int i = count - 1; i >= 0; i--) {
465            Preference pref = mUri.getPreference(i);
466            if (pref != mClearUri) {
467                mUri.removePreference(pref);
468            }
469        }
470    }
471
472    @Override
473    protected AlertDialog createDialog(int id, int errorCode) {
474        switch (id) {
475            case DLG_CLEAR_DATA:
476                return new AlertDialog.Builder(getActivity())
477                        .setTitle(getActivity().getText(R.string.clear_data_dlg_title))
478                        .setMessage(getActivity().getText(R.string.clear_data_dlg_text))
479                        .setPositiveButton(R.string.dlg_ok, new DialogInterface.OnClickListener() {
480                            public void onClick(DialogInterface dialog, int which) {
481                                // Clear user data here
482                                initiateClearUserData();
483                            }
484                        })
485                        .setNegativeButton(R.string.dlg_cancel, null)
486                        .create();
487            case DLG_CANNOT_CLEAR_DATA:
488                return new AlertDialog.Builder(getActivity())
489                        .setTitle(getActivity().getText(R.string.clear_user_data_text))
490                        .setMessage(getActivity().getText(R.string.clear_failed_dlg_text))
491                        .setNeutralButton(R.string.dlg_ok, new DialogInterface.OnClickListener() {
492                            public void onClick(DialogInterface dialog, int which) {
493                                mButtonsPref.setButton1Enabled(false);
494                                //force to recompute changed value
495                                setIntentAndFinish(false, false);
496                            }
497                        })
498                        .create();
499        }
500        return null;
501    }
502
503    @Override
504    public void onPackageSizeChanged(String packageName) {
505    }
506
507    @Override
508    public Loader<AppStorageStats> onCreateLoader(int id, Bundle args) {
509        Context context = getContext();
510        return new FetchPackageStorageAsyncLoader(
511                context, new StorageStatsSource(context), mInfo, UserHandle.of(mUserId));
512    }
513
514    @Override
515    public void onLoadFinished(Loader<AppStorageStats> loader, AppStorageStats result) {
516        mSizeController.setResult(result);
517        updateUiWithSize(result);
518    }
519
520    @Override
521    public void onLoaderReset(Loader<AppStorageStats> loader) {
522    }
523
524    private void updateSize() {
525        PackageManager packageManager = getPackageManager();
526        try {
527            mInfo = packageManager.getApplicationInfo(mPackageName, 0);
528        } catch (PackageManager.NameNotFoundException e) {
529            Log.e(TAG, "Could not find package", e);
530        }
531
532        if (mInfo == null) {
533            return;
534        }
535
536        getLoaderManager().restartLoader(1, Bundle.EMPTY, this);
537    }
538
539    @VisibleForTesting
540    void updateUiWithSize(AppStorageStats result) {
541        if (mCacheCleared) {
542            mSizeController.setCacheCleared(true);
543        }
544        if (mDataCleared) {
545            mSizeController.setDataCleared(true);
546        }
547
548        mSizeController.updateUi(getContext());
549
550        if (result == null) {
551            mButtonsPref.setButton1Enabled(false).setButton2Enabled(false);
552        } else {
553            long cacheSize = result.getCacheBytes();
554            long dataSize = result.getDataBytes() - cacheSize;
555
556            if (dataSize <= 0 || !mCanClearData || mDataCleared) {
557                mButtonsPref.setButton1Enabled(false);
558            } else {
559                mButtonsPref.setButton1Enabled(true)
560                        .setButton1OnClickListener(v -> handleClearDataClick());
561            }
562            if (cacheSize <= 0 || mCacheCleared) {
563                mButtonsPref.setButton2Enabled(false);
564            } else {
565                mButtonsPref.setButton2Enabled(true)
566                        .setButton2OnClickListener(v -> handleClearCacheClick());
567            }
568        }
569        if (mAppsControlDisallowedBySystem) {
570            mButtonsPref.setButton1Enabled(false).setButton2Enabled(false);
571        }
572    }
573
574    private final Handler mHandler = new Handler() {
575        public void handleMessage(Message msg) {
576            if (getView() == null) {
577                return;
578            }
579            switch (msg.what) {
580                case MSG_CLEAR_USER_DATA:
581                    mDataCleared = true;
582                    mCacheCleared = true;
583                    processClearMsg(msg);
584                    break;
585                case MSG_CLEAR_CACHE:
586                    mCacheCleared = true;
587                    // Refresh size info
588                    updateSize();
589                    break;
590            }
591        }
592    };
593
594    @Override
595    public int getMetricsCategory() {
596        return MetricsEvent.APPLICATIONS_APP_STORAGE;
597    }
598
599    class ClearCacheObserver extends IPackageDataObserver.Stub {
600        public void onRemoveCompleted(final String packageName, final boolean succeeded) {
601            final Message msg = mHandler.obtainMessage(MSG_CLEAR_CACHE);
602            msg.arg1 = succeeded ? OP_SUCCESSFUL : OP_FAILED;
603            mHandler.sendMessage(msg);
604        }
605    }
606
607    class ClearUserDataObserver extends IPackageDataObserver.Stub {
608        public void onRemoveCompleted(final String packageName, final boolean succeeded) {
609            final Message msg = mHandler.obtainMessage(MSG_CLEAR_USER_DATA);
610            msg.arg1 = succeeded ? OP_SUCCESSFUL : OP_FAILED;
611            mHandler.sendMessage(msg);
612        }
613    }
614}
615