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