For more reliability delete old files recursively in migration cleanup
[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 import com.owncloud.android.utils.FileStorageUtils;
41
42 import java.io.File;
43
44 /**
45 * Created by Bartosz Przybylski on 07.11.2015.
46 */
47 public class StorageMigrationActivity extends AppCompatActivity {
48 private static final String TAG = StorageMigrationActivity.class.getName();
49 public static final String KEY_MIGRATION_TARGET_DIR = "MIGRATION_TARGET";
50 public static final String KEY_MIGRATION_SOURCE_DIR = "MIGRATION_SOURCE";
51
52 private ProgressBar mProgressBar;
53 private Button mFinishButton;
54 private TextView mFeedbackText;
55
56 @Override
57 public void onCreate(Bundle savedInstanceState) {
58 super.onCreate(savedInstanceState);
59
60 setContentView(R.layout.migration_layout);
61 mProgressBar = (ProgressBar)findViewById(R.id.migrationProgress);
62 mFinishButton = (Button)findViewById(R.id.finishButton);
63 mFeedbackText = (TextView)findViewById(R.id.migrationText);
64
65 mProgressBar.setProgress(0);
66 mFinishButton.setVisibility(View.INVISIBLE);
67 mFeedbackText.setText(R.string.file_migration_preparing);
68
69 mFinishButton.setOnClickListener(new View.OnClickListener() {
70 @Override
71 public void onClick(View view) {
72 setResult(RESULT_CANCELED);
73 finish();
74 }
75 });
76
77 String source = getIntent().getStringExtra(KEY_MIGRATION_SOURCE_DIR);
78 String destination = getIntent().getStringExtra(KEY_MIGRATION_TARGET_DIR);
79
80 if (source == null || destination == null) {
81 Log_OC.e(TAG, "source or destination is null");
82 finish();
83 }
84
85 new FileMigrationTask().execute(source, destination);
86 }
87
88 private class FileMigrationTask extends AsyncTask<String, Integer, Integer> {
89
90 private String mStorageTarget;
91 private String mStorageSource;
92 private int mProgress;
93
94 private static final int mProgressCopyUpperBound = 98;
95
96 private class MigrationException extends Exception {
97 private int mResId;
98 /*
99 * @param resId resource identifier to use for displaying error
100 */
101 MigrationException(int resId) {
102 super();
103 this.mResId = resId;
104 }
105
106 int getResId() { return mResId; }
107 }
108
109 @Override
110 protected Integer doInBackground(String... args) {
111
112 mStorageSource = args[0];
113 mStorageTarget = args[1];
114 mProgress = 0;
115
116 publishProgress(mProgress++, R.string.file_migration_preparing);
117
118 Context context = StorageMigrationActivity.this;
119 String ocAuthority = context.getString(R.string.authority);
120
121 Account[] ocAccounts = AccountManager.get(context).getAccountsByType(MainApp.getAccountType());
122 boolean[] oldAutoSync = new boolean[ocAccounts.length];
123
124 Log_OC.stopLogging();
125
126 try {
127 publishProgress(mProgress++, R.string.file_migration_checking_destination);
128
129 checkDestinationAvailability();
130
131 publishProgress(mProgress++, R.string.file_migration_saving_accounts_configuration);
132 saveAccountsSyncStatus(ocAuthority, ocAccounts, oldAutoSync);
133
134 publishProgress(mProgress++, R.string.file_migration_waiting_for_unfinished_sync);
135 stopAccountsSyncing(ocAuthority, ocAccounts);
136 waitForUnfinishedSynchronizations(ocAuthority, ocAccounts);
137
138 publishProgress(mProgress++, R.string.file_migration_migrating);
139 copyFiles();
140
141 publishProgress(mProgress++, R.string.file_migration_updating_index);
142 updateIndex(context);
143
144 publishProgress(mProgress++, R.string.file_migration_cleaning);
145 cleanup();
146
147 } catch (MigrationException e) {
148 rollback();
149 Log_OC.startLogging(mStorageSource);
150 return e.getResId();
151 } finally {
152 publishProgress(mProgress++, R.string.file_migration_restoring_accounts_configuration);
153 restoreAccountsSyncStatus(ocAuthority, ocAccounts, oldAutoSync);
154 }
155
156 Log_OC.startLogging(mStorageTarget);
157 publishProgress(mProgress++, R.string.file_migration_ok_finished);
158
159 return 0;
160 }
161
162 @Override
163 protected void onProgressUpdate(Integer... progress) {
164 mProgressBar.setProgress(progress[0]);
165 if (progress.length > 1)
166 mFeedbackText.setText(progress[1]);
167 }
168
169 @Override
170 protected void onPostExecute(Integer code) {
171 mFinishButton.setVisibility(View.VISIBLE);
172 if (code != 0) {
173 mFeedbackText.setText(code);
174 } else {
175 mFeedbackText.setText(R.string.file_migration_ok_finished);
176 mFinishButton.setOnClickListener(new View.OnClickListener() {
177 @Override
178 public void onClick(View view) {
179 Intent resultIntent = new Intent();
180 resultIntent.putExtra(KEY_MIGRATION_TARGET_DIR, mStorageTarget);
181 setResult(RESULT_OK, resultIntent);
182 finish();
183 }
184 });
185 }
186 }
187
188 void checkDestinationAvailability() throws MigrationException {
189 File srcFile = new File(mStorageSource);
190 File dstFile = new File(mStorageTarget);
191
192 if (!dstFile.canRead() || !srcFile.canRead())
193 throw new MigrationException(R.string.file_migration_failed_not_readable);
194
195 if (!dstFile.canWrite() || !srcFile.canWrite())
196 throw new MigrationException(R.string.file_migration_failed_not_writable);
197
198 if (new File(dstFile, MainApp.getDataFolder()).exists())
199 throw new MigrationException(R.string.file_migration_failed_dir_already_exists);
200
201 if (dstFile.getFreeSpace() < FileStorageUtils.getFolderSize(new File(srcFile, MainApp.getDataFolder())))
202 throw new MigrationException(R.string.file_migration_failed_not_enough_space);
203 }
204
205 void copyFiles() throws MigrationException {
206 File srcFile = new File(mStorageSource + File.separator + MainApp.getDataFolder());
207 File dstFile = new File(mStorageTarget + File.separator + MainApp.getDataFolder());
208
209 copyDirs(srcFile, dstFile);
210 mProgress = Math.max(mProgress, mProgressCopyUpperBound);
211 publishProgress(mProgress);
212 }
213
214 void copyDirs(File src, File dst) throws MigrationException {
215 if (!dst.mkdirs())
216 throw new MigrationException(R.string.file_migration_failed_while_coping);
217
218 for (File f : src.listFiles()) {
219
220 mProgress = Math.min(mProgress+1, mProgressCopyUpperBound);
221 publishProgress(mProgress);
222
223 if (f.isDirectory())
224 copyDirs(f, new File(dst, f.getName()));
225 else if (!FileStorageUtils.copyFile(f, new File(dst, f.getName())))
226 throw new MigrationException(R.string.file_migration_failed_while_coping);
227 }
228
229 }
230
231 void updateIndex(Context context) throws MigrationException {
232 FileDataStorageManager manager = new FileDataStorageManager(null, context.getContentResolver());
233
234 try {
235 manager.migrateStoredFiles(mStorageSource, mStorageTarget);
236 } catch (Exception e) {
237 throw new MigrationException(R.string.file_migration_failed_while_updating_index);
238 }
239 }
240
241 void cleanup() {
242 File srcFile = new File(mStorageSource + File.separator + MainApp.getDataFolder());
243 if (!deleteRecursive(srcFile))
244 Log_OC.w(TAG, "Migration cleanup step failed");
245 }
246
247 boolean deleteRecursive(File f) {
248 boolean res = true;
249 if (f.isDirectory())
250 for (File c : f.listFiles())
251 res = deleteRecursive(c) && res;
252 return f.delete() && res;
253 }
254
255
256 void rollback() {
257 File dstFile = new File(mStorageTarget + File.separator + MainApp.getDataFolder());
258 if (dstFile.exists())
259 if (!dstFile.delete())
260 Log_OC.w(TAG, "Rollback step failed");
261 }
262
263 void saveAccountsSyncStatus(String authority, Account accounts[], boolean syncs[]) {
264 for (int i = 0; i < accounts.length; ++i)
265 syncs[i] = ContentResolver.getSyncAutomatically(accounts[i], authority);
266 }
267
268 void stopAccountsSyncing(String authority, Account accounts[]) {
269 for (int i = 0; i < accounts.length; ++i)
270 ContentResolver.setSyncAutomatically(accounts[i], authority, false);
271 }
272
273 void waitForUnfinishedSynchronizations(String authority, Account accounts[]) {
274 for (int i = 0; i < accounts.length; ++i)
275 while (ContentResolver.isSyncActive(accounts[i], authority))
276 try {
277 Thread.sleep(1000);
278 } catch (InterruptedException e) {
279 Log_OC.w(TAG, "Thread interrupted while waiting for account to end syncing");
280 Thread.currentThread().interrupt();
281 }
282 }
283
284 void restoreAccountsSyncStatus(String authority, Account accounts[], boolean oldSync[]) {
285 for (int i = 0; i < accounts.length; ++i)
286 ContentResolver.setSyncAutomatically(accounts[i], authority, oldSync[i]);
287 }
288
289 }
290 }