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.deviceinfo;
18
19import android.app.AlertDialog;
20import android.app.Dialog;
21import android.app.DialogFragment;
22import android.content.Context;
23import android.content.DialogInterface;
24import android.content.Intent;
25import android.content.pm.IPackageMoveObserver;
26import android.os.AsyncTask;
27import android.os.Bundle;
28import android.os.storage.DiskInfo;
29import android.os.storage.StorageManager;
30import android.os.storage.VolumeInfo;
31import android.text.TextUtils;
32import android.util.Log;
33import android.view.View;
34import android.widget.Toast;
35
36import com.android.settings.R;
37
38import java.util.Objects;
39
40import static com.android.settings.deviceinfo.StorageSettings.TAG;
41
42public class StorageWizardFormatProgress extends StorageWizardBase {
43    private static final String TAG_SLOW_WARNING = "slow_warning";
44
45    private boolean mFormatPrivate;
46
47    private PartitionTask mTask;
48
49    @Override
50    protected void onCreate(Bundle savedInstanceState) {
51        super.onCreate(savedInstanceState);
52        if (mDisk == null) {
53            finish();
54            return;
55        }
56        setContentView(R.layout.storage_wizard_progress);
57        setKeepScreenOn(true);
58
59        mFormatPrivate = getIntent().getBooleanExtra(
60                StorageWizardFormatConfirm.EXTRA_FORMAT_PRIVATE, false);
61        setIllustrationType(
62                mFormatPrivate ? ILLUSTRATION_INTERNAL : ILLUSTRATION_PORTABLE);
63
64        setHeaderText(R.string.storage_wizard_format_progress_title, mDisk.getDescription());
65        setBodyText(R.string.storage_wizard_format_progress_body, mDisk.getDescription());
66
67        getNextButton().setVisibility(View.GONE);
68
69        mTask = (PartitionTask) getLastNonConfigurationInstance();
70        if (mTask == null) {
71            mTask = new PartitionTask();
72            mTask.setActivity(this);
73            mTask.execute();
74        } else {
75            mTask.setActivity(this);
76        }
77    }
78
79    @Override
80    public Object onRetainNonConfigurationInstance() {
81        return mTask;
82    }
83
84    public static class PartitionTask extends AsyncTask<Void, Integer, Exception> {
85        public StorageWizardFormatProgress mActivity;
86
87        private volatile int mProgress = 20;
88
89        private volatile long mPrivateBench;
90
91        @Override
92        protected Exception doInBackground(Void... params) {
93            final StorageWizardFormatProgress activity = mActivity;
94            final StorageManager storage = mActivity.mStorage;
95            try {
96                if (activity.mFormatPrivate) {
97                    storage.partitionPrivate(activity.mDisk.getId());
98                    publishProgress(40);
99
100                    final VolumeInfo privateVol = activity.findFirstVolume(VolumeInfo.TYPE_PRIVATE);
101                    mPrivateBench = storage.benchmark(privateVol.getId());
102                    mPrivateBench /= 1000000;
103
104                    // If we just adopted the device that had been providing
105                    // physical storage, then automatically move storage to the
106                    // new emulated volume.
107                    if (activity.mDisk.isDefaultPrimary()
108                            && Objects.equals(storage.getPrimaryStorageUuid(),
109                                    StorageManager.UUID_PRIMARY_PHYSICAL)) {
110                        Log.d(TAG, "Just formatted primary physical; silently moving "
111                                + "storage to new emulated volume");
112                        storage.setPrimaryStorageUuid(privateVol.getFsUuid(), new SilentObserver());
113                    }
114
115                } else {
116                    storage.partitionPublic(activity.mDisk.getId());
117                }
118                return null;
119            } catch (Exception e) {
120                return e;
121            }
122        }
123
124        @Override
125        protected void onProgressUpdate(Integer... progress) {
126            mProgress = progress[0];
127            mActivity.setCurrentProgress(mProgress);
128        }
129
130        public void setActivity(StorageWizardFormatProgress activity) {
131            mActivity = activity;
132            mActivity.setCurrentProgress(mProgress);
133        }
134
135        @Override
136        protected void onPostExecute(Exception e) {
137            final StorageWizardFormatProgress activity = mActivity;
138            if (activity.isDestroyed()) {
139                return;
140            }
141
142            if (e != null) {
143                Log.e(TAG, "Failed to partition", e);
144                Toast.makeText(activity, e.getMessage(), Toast.LENGTH_LONG).show();
145                activity.finishAffinity();
146                return;
147            }
148
149            if (activity.mFormatPrivate) {
150                // When the adoptable storage feature originally launched, we
151                // benchmarked both internal storage and the newly adopted
152                // storage and we warned if the adopted device was less than
153                // 0.25x the speed of internal. (The goal was to help set user
154                // expectations and encourage use of devices comparable to
155                // internal storage performance.)
156
157                // However, since then, internal storage has started moving from
158                // eMMC to UFS, which can significantly outperform adopted
159                // devices, causing the speed warning to always trigger. To
160                // mitigate this, we've switched to using a static threshold.
161
162                // The static threshold was derived by running the benchmark on
163                // a wide selection of SD cards from several vendors; here are
164                // some 50th percentile results from 20+ runs of each card:
165
166                // 8GB C4 40MB/s+: 3282ms
167                // 16GB C10 40MB/s+: 1881ms
168                // 32GB C10 40MB/s+: 2897ms
169                // 32GB U3 80MB/s+: 1595ms
170                // 32GB C10 80MB/s+: 1680ms
171                // 128GB U1 80MB/s+: 1532ms
172
173                // Thus a 2000ms static threshold strikes a reasonable balance
174                // to help us identify slower cards. Users can still proceed
175                // with these slower cards; we're just showing a warning.
176
177                // The above analysis was done using the "r1572:w1001:s285"
178                // benchmark, and it should be redone any time the benchmark
179                // changes.
180
181                Log.d(TAG, "New volume took " + mPrivateBench + "ms to run benchmark");
182                if (mPrivateBench > 2000) {
183                    final SlowWarningFragment dialog = new SlowWarningFragment();
184                    dialog.showAllowingStateLoss(activity.getFragmentManager(), TAG_SLOW_WARNING);
185                } else {
186                    activity.onFormatFinished();
187                }
188            } else {
189                activity.onFormatFinished();
190            }
191        }
192    }
193
194    public static class SlowWarningFragment extends DialogFragment {
195        @Override
196        public Dialog onCreateDialog(Bundle savedInstanceState) {
197            final Context context = getActivity();
198
199            final AlertDialog.Builder builder = new AlertDialog.Builder(context);
200
201            final StorageWizardFormatProgress target =
202                    (StorageWizardFormatProgress) getActivity();
203            final String descrip = target.getDiskDescription();
204            final String genericDescip = target.getGenericDiskDescription();
205            builder.setMessage(TextUtils.expandTemplate(getText(R.string.storage_wizard_slow_body),
206                    descrip, genericDescip));
207
208            builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
209                @Override
210                public void onClick(DialogInterface dialog, int which) {
211                    final StorageWizardFormatProgress target =
212                            (StorageWizardFormatProgress) getActivity();
213                    target.onFormatFinished();
214                }
215            });
216
217            return builder.create();
218        }
219    }
220
221    private String getDiskDescription() {
222        return mDisk.getDescription();
223    }
224
225    private String getGenericDiskDescription() {
226        // TODO: move this directly to DiskInfo
227        if (mDisk.isSd()) {
228            return getString(com.android.internal.R.string.storage_sd_card);
229        } else if (mDisk.isUsb()) {
230            return getString(com.android.internal.R.string.storage_usb_drive);
231        } else {
232            return null;
233        }
234    }
235
236    private void onFormatFinished() {
237        final String forgetUuid = getIntent().getStringExtra(
238                StorageWizardFormatConfirm.EXTRA_FORGET_UUID);
239        if (!TextUtils.isEmpty(forgetUuid)) {
240            mStorage.forgetVolume(forgetUuid);
241        }
242
243        final boolean offerMigrate;
244        if (mFormatPrivate) {
245            // Offer to migrate only if storage is currently internal
246            final VolumeInfo privateVol = getPackageManager()
247                    .getPrimaryStorageCurrentVolume();
248            offerMigrate = (privateVol != null
249                    && VolumeInfo.ID_PRIVATE_INTERNAL.equals(privateVol.getId()));
250        } else {
251            offerMigrate = false;
252        }
253
254        if (offerMigrate) {
255            final Intent intent = new Intent(this, StorageWizardMigrate.class);
256            intent.putExtra(DiskInfo.EXTRA_DISK_ID, mDisk.getId());
257            startActivity(intent);
258        } else {
259            final Intent intent = new Intent(this, StorageWizardReady.class);
260            intent.putExtra(DiskInfo.EXTRA_DISK_ID, mDisk.getId());
261            startActivity(intent);
262        }
263        finishAffinity();
264    }
265
266    private static class SilentObserver extends IPackageMoveObserver.Stub {
267        @Override
268        public void onCreated(int moveId, Bundle extras) {
269            // Ignored
270        }
271
272        @Override
273        public void onStatusChanged(int moveId, int status, long estMillis) {
274            // Ignored
275        }
276    }
277}
278