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