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