dc73fcc63fb56ae4dd1a60c0ae889288aed8dc98
[pub/Android/ownCloud.git] / src / com / owncloud / android / ui / activity / StorageMigrationActivity.java
1 /**
2 * ownCloud Android client application
3 *
4 * @author Bartosz Przybylski
5 * Copyright (C) 2015 ownCloud Inc.
6 * Copyright (C) 2015 Bartosz Przybylski
7 *
8 * This program is free software: you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License version 2,
10 * as published by the Free Software Foundation.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program. If not, see <http://www.gnu.org/licenses/>.
19 *
20 */
21 package com.owncloud.android.ui.activity;
22
23 import android.accounts.Account;
24 import android.accounts.AccountManager;
25 import android.content.ContentResolver;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.os.AsyncTask;
29 import android.os.Bundle;
30 import android.support.v7.app.AppCompatActivity;
31 import android.view.View;
32 import android.widget.Button;
33 import android.widget.ProgressBar;
34 import android.widget.TextView;
35
36 import com.owncloud.android.MainApp;
37 import com.owncloud.android.R;
38 import com.owncloud.android.datamodel.FileDataStorageManager;
39 import com.owncloud.android.lib.common.utils.Log_OC;
40
41 import java.io.File;
42 import java.io.FileInputStream;
43 import java.io.FileOutputStream;
44 import java.io.IOException;
45 import java.io.InputStream;
46 import java.io.OutputStream;
47
48 /**
49 * Created by Bartosz Przybylski on 07.11.2015.
50 */
51 public class StorageMigrationActivity extends AppCompatActivity {
52 private static final String TAG = StorageMigrationActivity.class.getName();
53 public static final String KEY_MIGRATION_TARGET_DIR = "MIGRATION_TARGET";
54 public static final String KEY_MIGRATION_SOURCE_DIR = "MIGRATION_SOURCE";
55
56 private ProgressBar mProgressBar;
57 private Button mFinishButton;
58 private TextView mFeedbackText;
59
60 @Override
61 public void onCreate(Bundle savedInstanceState) {
62 super.onCreate(savedInstanceState);
63
64 setContentView(R.layout.migration_layout);
65 mProgressBar = (ProgressBar)findViewById(R.id.migrationProgress);
66 mFinishButton = (Button)findViewById(R.id.finishButton);
67 mFeedbackText = (TextView)findViewById(R.id.migrationText);
68
69 mProgressBar.setProgress(0);
70 mFinishButton.setVisibility(View.INVISIBLE);
71 mFeedbackText.setText(R.string.file_migration_preparing);
72
73 mFinishButton.setOnClickListener(new View.OnClickListener() {
74 @Override
75 public void onClick(View view) {
76 setResult(RESULT_CANCELED);
77 finish();
78 }
79 });
80
81 String source = getIntent().getStringExtra(KEY_MIGRATION_SOURCE_DIR);
82 String destination = getIntent().getStringExtra(KEY_MIGRATION_TARGET_DIR);
83
84 if (source == null || destination == null) {
85 Log_OC.e(TAG, "source or destination is null");
86 finish();
87 }
88
89 new FileMigrationTask().execute(source, destination);
90 }
91
92 private class FileMigrationTask extends AsyncTask<String, Integer, Integer> {
93
94 private String mStorageTarget;
95 private String mStorageSource;
96 private int mProgress;
97
98 private static final int mProgressCopyUpperBound = 98;
99
100 private class MigrationException extends Exception {
101 private int mResId;
102 /*
103 * @param resId resource identifier to use for displaying error
104 */
105 MigrationException(int resId) {
106 super();
107 this.mResId = resId;
108 }
109
110 int getResId() { return mResId; }
111 }
112
113 private class MigrationCleanupException extends Exception {
114 MigrationCleanupException() {}
115 }
116
117 @Override
118 protected Integer doInBackground(String... args) {
119
120 mStorageSource = args[0];
121 mStorageTarget = args[1];
122 mProgress = 0;
123
124 publishProgress(mProgress++, R.string.file_migration_preparing);
125
126 Context context = StorageMigrationActivity.this;
127 String ocAuthority = context.getString(R.string.authority);
128
129 Account[] ocAccounts = AccountManager.get(context).getAccountsByType(MainApp.getAccountType());
130 boolean[] oldAutoSync = new boolean[ocAccounts.length];
131
132 try {
133 publishProgress(mProgress++, R.string.file_migration_checking_destination);
134
135 checkDestinationAvailability();
136
137 publishProgress(mProgress++, R.string.file_migration_saving_accounts_configuration);
138 saveAccountsSyncStatus(ocAuthority, ocAccounts, oldAutoSync);
139
140 publishProgress(mProgress++, R.string.file_migration_waiting_for_unfinished_sync);
141 stopAccountsSyncing(ocAuthority, ocAccounts);
142 waitForUnfinishedSynchronizations(ocAuthority, ocAccounts);
143
144 publishProgress(mProgress++, R.string.file_migration_migrating);
145 copyFiles();
146
147 publishProgress(mProgress++, R.string.file_migration_updating_index);
148 updateIndex(context);
149
150 publishProgress(mProgress++, R.string.file_migration_cleaning);
151 cleanup();
152
153 } catch (MigrationException e) {
154 rollback();
155 return e.getResId();
156 } catch (MigrationCleanupException e) {
157 Log_OC.w(TAG, "Migration mleanup step failed");
158 return 0;
159 } finally {
160 publishProgress(mProgress++, R.string.file_migration_restoring_accounts_configuration);
161 restoreAccountsSyncStatus(ocAuthority, ocAccounts, oldAutoSync);
162 }
163
164 publishProgress(mProgress++, R.string.file_migration_ok_finished);
165
166 return 0;
167 }
168
169 @Override
170 protected void onProgressUpdate(Integer... progress) {
171 mProgressBar.setProgress(progress[0]);
172 if (progress.length > 1)
173 mFeedbackText.setText(progress[1]);
174 }
175
176 @Override
177 protected void onPostExecute(Integer code) {
178 mFinishButton.setVisibility(View.VISIBLE);
179 if (code != 0) {
180 mFeedbackText.setText(code);
181 } else {
182 mFeedbackText.setText(R.string.file_migration_ok_finished);
183 mFinishButton.setOnClickListener(new View.OnClickListener() {
184 @Override
185 public void onClick(View view) {
186 Intent resultIntent = new Intent();
187 resultIntent.putExtra(KEY_MIGRATION_TARGET_DIR, mStorageTarget);
188 setResult(RESULT_OK, resultIntent);
189 finish();
190 }
191 });
192 }
193 }
194
195 void checkDestinationAvailability() throws MigrationException {
196 File srcFile = new File(mStorageSource);
197 File dstFile = new File(mStorageTarget);
198
199 if (!dstFile.canRead() || !srcFile.canRead())
200 throw new MigrationException(R.string.file_migration_failed_not_readable);
201
202 if (!dstFile.canWrite() || !srcFile.canWrite())
203 throw new MigrationException(R.string.file_migration_failed_not_writable);
204
205 if (new File(dstFile, MainApp.getDataFolder()).exists())
206 throw new MigrationException(R.string.file_migration_failed_dir_already_exists);
207
208 if (dstFile.getFreeSpace() < calculateUsedSpace(new File(srcFile, MainApp.getDataFolder())))
209 throw new MigrationException(R.string.file_migration_failed_not_enough_space);
210 }
211
212 void copyFiles() throws MigrationException {
213 File srcFile = new File(mStorageSource + File.separator + MainApp.getDataFolder());
214 File dstFile = new File(mStorageTarget + File.separator + MainApp.getDataFolder());
215
216 copyDirs(srcFile, dstFile);
217 mProgress = Math.max(mProgress, mProgressCopyUpperBound);
218 publishProgress(mProgress);
219 }
220
221 private boolean copyFile(File src, File target) {
222 boolean ret = true;
223
224 InputStream in = null;
225 OutputStream out = null;
226
227 try {
228 in = new FileInputStream(src);
229 out = new FileOutputStream(target);
230 byte[] buf = new byte[1024];
231 int len;
232 while ((len = in.read(buf)) > 0) {
233 out.write(buf, 0, len);
234 }
235 } catch (IOException ex) {
236 ret = false;
237 } finally {
238 if (in != null) try {
239 in.close();
240 } catch (IOException e) {
241 e.printStackTrace(System.err);
242 }
243 if (out != null) try {
244 out.close();
245 } catch (IOException e) {
246 e.printStackTrace(System.err);
247 }
248 }
249
250 return ret;
251 }
252
253 void copyDirs(File src, File dst) throws MigrationException {
254 if (!dst.mkdirs())
255 throw new MigrationException(R.string.file_migration_failed_while_coping);
256
257 for (File f : src.listFiles()) {
258
259 mProgress = Math.min(mProgress+1, mProgressCopyUpperBound);
260 publishProgress(mProgress);
261
262 if (f.isDirectory())
263 copyDirs(f, new File(dst, f.getName()));
264 else if (!copyFile(f, new File(dst, f.getName())))
265 throw new MigrationException(R.string.file_migration_failed_while_coping);
266 }
267
268 }
269
270 void updateIndex(Context context) throws MigrationException {
271 FileDataStorageManager manager = new FileDataStorageManager(null, context.getContentResolver());
272
273 try {
274 manager.migrateStoredFiles(mStorageSource, mStorageTarget);
275 } catch (Exception e) {
276 throw new MigrationException(R.string.file_migration_failed_while_updating_index);
277 }
278 }
279
280 void cleanup() throws MigrationCleanupException {
281 File srcFile = new File(mStorageSource + File.separator + MainApp.getDataFolder());
282 if (!srcFile.delete())
283 throw new MigrationCleanupException();
284 }
285
286 void rollback() {
287 File dstFile = new File(mStorageTarget + File.separator + MainApp.getDataFolder());
288 if (dstFile.exists())
289 if (!dstFile.delete())
290 Log_OC.w(TAG, "Rollback step failed");
291 }
292
293 long calculateUsedSpace(File dir) {
294 long result = 0;
295
296 for (File f : dir.listFiles()) {
297 if (f.isDirectory())
298 result += calculateUsedSpace(f);
299 else
300 result += f.length();
301 }
302
303 return result;
304 }
305
306 void saveAccountsSyncStatus(String authority, Account accounts[], boolean syncs[]) {
307 for (int i = 0; i < accounts.length; ++i)
308 syncs[i] = ContentResolver.getSyncAutomatically(accounts[i], authority);
309 }
310
311 void stopAccountsSyncing(String authority, Account accounts[]) {
312 for (int i = 0; i < accounts.length; ++i)
313 ContentResolver.setSyncAutomatically(accounts[i], authority, false);
314 }
315
316 void waitForUnfinishedSynchronizations(String authority, Account accounts[]) {
317 for (int i = 0; i < accounts.length; ++i)
318 while (ContentResolver.isSyncActive(accounts[i], authority))
319 try {
320 Thread.sleep(1000);
321 } catch (InterruptedException e) {
322 Log_OC.w(TAG, "Thread interrupted while waiting for account to end syncing");
323 Thread.currentThread().interrupt();
324 }
325 }
326
327 void restoreAccountsSyncStatus(String authority, Account accounts[], boolean oldSync[]) {
328 for (int i = 0; i < accounts.length; ++i)
329 ContentResolver.setSyncAutomatically(accounts[i], authority, oldSync[i]);
330 }
331
332 }
333 }