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