Browse Source

Add more migration stuff. cleanup step is still missing

Bartosz Przybylski 9 years ago
parent
commit
b38b3276e2

+ 1 - 0
AndroidManifest.xml

@@ -80,6 +80,7 @@
                   android:excludeFromRecents="true"
                   android:excludeFromRecents="true"
                   android:theme="@style/Theme.ownCloud.NoActionBar">
                   android:theme="@style/Theme.ownCloud.NoActionBar">
         <activity android:name=".ui.activity.LocalDirectorySelectorActivity" />
         <activity android:name=".ui.activity.LocalDirectorySelectorActivity" />
+        <activity android:name=".ui.activity.StorageMigrationActivity" />
             <intent-filter>
             <intent-filter>
                 <action android:name="android.intent.action.SEND" />
                 <action android:name="android.intent.action.SEND" />
 
 

+ 32 - 0
res/layout/migration_layout.xml

@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:orientation="vertical"
+              android:layout_width="match_parent"
+              android:layout_height="match_parent"
+              android:gravity="center_vertical">
+
+    <ProgressBar
+        style="?android:attr/progressBarStyleHorizontal"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:id="@+id/migrationProgress"
+        android:layout_gravity="center_horizontal"
+        android:progress="50"
+        android:paddingLeft="30dp"
+        android:paddingRight="30dp"/>
+
+    <TextView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textAppearance="?android:attr/textAppearanceMedium"
+        android:text=""
+        android:id="@+id/migrationText"
+        android:layout_gravity="center_horizontal"/>
+
+    <Button
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/drawer_close"
+        android:id="@+id/finishButton"
+        android:layout_gravity="center_horizontal"/>
+</LinearLayout>

+ 17 - 0
res/values/strings.xml

@@ -356,6 +356,23 @@
     <string name="uploader_upload_forbidden_permissions">to upload in this folder</string>
     <string name="uploader_upload_forbidden_permissions">to upload in this folder</string>
     <string name="downloader_download_file_not_found">The file is no longer available on the server</string>
     <string name="downloader_download_file_not_found">The file is no longer available on the server</string>
 
 
+    <string name="file_migration_finish_button">Finish</string>
+    <string name="file_migration_preparing">Preparing for migration...</string>
+    <string name="file_migration_checking_destination">Checking destination...</string>
+    <string name="file_migration_saving_accounts_configuration">Saving accounts configuration...</string>
+    <string name="file_migration_waiting_for_unfinished_sync">Waiting for unfinished synchronizations...</string>
+    <string name="file_migration_migrating">Moving data...</string>
+    <string name="file_migration_updating_index">Updating index...</string>
+    <string name="file_migration_cleaning">Cleaning...</string>
+    <string name="file_migration_restoring_accounts_configuration">Restoring accounts configuration...</string>
+    <string name="file_migration_ok_finished">Finished</string>
+    <string name="file_migration_failed_not_enough_space">ERROR: Not enough space</string>
+    <string name="file_migration_failed_not_writable">ERROR: File is not writable</string>
+    <string name="file_migration_failed_not_readable">ERROR: File is not readable</string>
+    <string name="file_migration_failed_dir_already_exists">ERROR: owncloud directory already exists</string>
+    <string name="file_migration_failed_while_coping">ERROR: While migrating</string>
+    <string name="file_migration_failed_while_updating_index">ERROR: While updating index</string>
+
     <string name="prefs_category_accounts">Accounts</string>
     <string name="prefs_category_accounts">Accounts</string>
     <string name="prefs_add_account">Add account</string>
     <string name="prefs_add_account">Add account</string>
     <string name="drawer_manage_accounts">Manage accounts</string>
     <string name="drawer_manage_accounts">Manage accounts</string>

+ 64 - 0
src/com/owncloud/android/datamodel/FileDataStorageManager.java

@@ -32,6 +32,7 @@ import android.content.OperationApplicationException;
 import android.database.Cursor;
 import android.database.Cursor;
 import android.net.Uri;
 import android.net.Uri;
 import android.os.RemoteException;
 import android.os.RemoteException;
+import android.provider.BaseColumns;
 import android.provider.MediaStore;
 import android.provider.MediaStore;
 
 
 import com.owncloud.android.MainApp;
 import com.owncloud.android.MainApp;
@@ -765,6 +766,69 @@ public class FileDataStorageManager {
         return ret;
         return ret;
     }
     }
 
 
+    public void migrateStoredFiles(String srcPath, String dstPath) throws Exception {
+        Cursor c = null;
+        if (getContentResolver() != null) {
+            c = getContentResolver().query(ProviderTableMeta.CONTENT_URI_FILE,
+                    null,
+                    ProviderTableMeta.FILE_STORAGE_PATH  + " IS NOT NULL",
+                    null,
+                    null);
+
+        } else {
+            try {
+                c = getContentProviderClient().query(ProviderTableMeta.CONTENT_URI_FILE,
+                        new String[]{ProviderTableMeta._ID, ProviderTableMeta.FILE_STORAGE_PATH},
+                        ProviderTableMeta.FILE_STORAGE_PATH + " IS NOT NULL",
+                        null,
+                        null);
+            } catch (RemoteException e) {
+                Log_OC.e(TAG, e.getMessage());
+                throw e;
+            }
+        }
+
+        ArrayList<ContentProviderOperation> operations =
+                new ArrayList<ContentProviderOperation>(c.getCount());
+        if (c.moveToFirst()) {
+            do {
+                ContentValues cv = new ContentValues();
+                long fileId = c.getLong(c.getColumnIndex(ProviderTableMeta._ID));
+                String oldFileStoragePath = c.getString(c.getColumnIndex(ProviderTableMeta.FILE_STORAGE_PATH));
+
+                if (oldFileStoragePath.startsWith(srcPath)) {
+
+                    cv.put(
+                            ProviderTableMeta.FILE_STORAGE_PATH,
+                            oldFileStoragePath.replaceFirst(srcPath, dstPath));
+
+                    operations.add(
+                            ContentProviderOperation.newUpdate(ProviderTableMeta.CONTENT_URI).
+                                    withValues(cv).
+                                    withSelection(
+                                            ProviderTableMeta._ID + "=?",
+                                            new String[]{String.valueOf(fileId)}
+                                    )
+                                    .build());
+                }
+
+            } while (c.moveToNext());
+        }
+        c.close();
+
+        /// 3. apply updates in batch
+        try {
+            if (getContentResolver() != null) {
+                getContentResolver().applyBatch(MainApp.getAuthority(), operations);
+
+            } else {
+                getContentProviderClient().applyBatch(operations);
+            }
+
+        } catch (Exception e) {
+            throw e;
+        }
+    }
 
 
     private Vector<OCFile> getFolderContent(long parentId, boolean onlyOnDevice) {
     private Vector<OCFile> getFolderContent(long parentId, boolean onlyOnDevice) {
 
 

+ 16 - 4
src/com/owncloud/android/ui/activity/Preferences.java

@@ -642,13 +642,12 @@ public class Preferences extends PreferenceActivity {
                 migrationIntent.putExtra(StorageMigrationActivity.KEY_MIGRATION_SOURCE_DIR,
                 migrationIntent.putExtra(StorageMigrationActivity.KEY_MIGRATION_SOURCE_DIR,
                         currentStorageDir.getAbsolutePath());
                         currentStorageDir.getAbsolutePath());
                 migrationIntent.putExtra(StorageMigrationActivity.KEY_MIGRATION_TARGET_DIR,
                 migrationIntent.putExtra(StorageMigrationActivity.KEY_MIGRATION_TARGET_DIR,
-                        currentStorageDir.getAbsolutePath());
+                        upcomingStorageDir.getAbsolutePath());
                 startActivityForResult(migrationIntent, ACTION_PERFORM_MIGRATION);
                 startActivityForResult(migrationIntent, ACTION_PERFORM_MIGRATION);
             }
             }
         } else if (requestCode == ACTION_PERFORM_MIGRATION && resultCode == RESULT_OK) {
         } else if (requestCode == ACTION_PERFORM_MIGRATION && resultCode == RESULT_OK) {
             String resultStorageDir = data.getStringExtra(StorageMigrationActivity.KEY_MIGRATION_TARGET_DIR);
             String resultStorageDir = data.getStringExtra(StorageMigrationActivity.KEY_MIGRATION_TARGET_DIR);
-            mStoragePath = resultStorageDir;
-            mPrefStoragePath.setSummary(mStoragePath);
+            saveStoragePath(resultStorageDir);
         } else if (requestCode == ACTION_REQUEST_CODE_DAVDROID_SETUP && resultCode == RESULT_OK) {
         } else if (requestCode == ACTION_REQUEST_CODE_DAVDROID_SETUP && resultCode == RESULT_OK) {
             Toast.makeText(this, R.string.prefs_calendar_contacts_sync_setup_successful, Toast.LENGTH_LONG).show();        }
             Toast.makeText(this, R.string.prefs_calendar_contacts_sync_setup_successful, Toast.LENGTH_LONG).show();        }
     }
     }
@@ -741,6 +740,19 @@ public class Preferences extends PreferenceActivity {
         mPrefInstantUploadPath.setSummary(mUploadPath);
         mPrefInstantUploadPath.setSummary(mUploadPath);
     }
     }
 
 
+    /**
+     * Save storage path
+     */
+    private void saveStoragePath(String newStoragePath) {
+        SharedPreferences appPrefs =
+                PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
+        mStoragePath = newStoragePath;
+        MainApp.setStoragePath(mStoragePath);
+        SharedPreferences.Editor editor = appPrefs.edit();
+        editor.putString("storage_path", mStoragePath);
+        mPrefStoragePath.setSummary(mStoragePath);
+    }
+
     /**
     /**
      * Load storage path set on preferences
      * Load storage path set on preferences
      */
      */
@@ -749,7 +761,7 @@ public class Preferences extends PreferenceActivity {
                 PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
                 PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
         mStoragePath = appPrefs.getString("storage_path", Environment.getExternalStorageDirectory()
         mStoragePath = appPrefs.getString("storage_path", Environment.getExternalStorageDirectory()
                                                          .getAbsolutePath());
                                                          .getAbsolutePath());
-        mPrefStoragePath.setSummary(mStoragePath + File.separator + MainApp.getDataFolder());
+        mPrefStoragePath.setSummary(mStoragePath);
     }
     }
 
 
     /**
     /**

+ 278 - 0
src/com/owncloud/android/ui/activity/StorageMigrationActivity.java

@@ -20,12 +20,290 @@
  */
  */
 package com.owncloud.android.ui.activity;
 package com.owncloud.android.ui.activity;
 
 
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.AsyncTask;
+import android.os.Bundle;
 import android.support.v7.app.AppCompatActivity;
 import android.support.v7.app.AppCompatActivity;
+import android.view.View;
+import android.widget.Button;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import com.owncloud.android.MainApp;
+import com.owncloud.android.R;
+import com.owncloud.android.datamodel.FileDataStorageManager;
+import com.owncloud.android.lib.common.utils.Log_OC;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
 
 
 /**
 /**
  * Created by Bartosz Przybylski on 07.11.2015.
  * Created by Bartosz Przybylski on 07.11.2015.
  */
  */
 public class StorageMigrationActivity extends AppCompatActivity {
 public class StorageMigrationActivity extends AppCompatActivity {
+	private static final String TAG = StorageMigrationActivity.class.getName();
 	public static final String KEY_MIGRATION_TARGET_DIR = "MIGRATION_TARGET";
 	public static final String KEY_MIGRATION_TARGET_DIR = "MIGRATION_TARGET";
 	public static final String KEY_MIGRATION_SOURCE_DIR = "MIGRATION_SOURCE";
 	public static final String KEY_MIGRATION_SOURCE_DIR = "MIGRATION_SOURCE";
+
+	private ProgressBar mProgressBar;
+	private Button mFinishButton;
+	private TextView mFeedbackText;
+
+	@Override
+	public void onCreate(Bundle savedInstanceState) {
+		super.onCreate(savedInstanceState);
+
+		setContentView(R.layout.migration_layout);
+		mProgressBar = (ProgressBar)findViewById(R.id.migrationProgress);
+		mFinishButton = (Button)findViewById(R.id.finishButton);
+		mFeedbackText = (TextView)findViewById(R.id.migrationText);
+
+		mProgressBar.setProgress(0);
+		mFinishButton.setVisibility(View.INVISIBLE);
+		mFeedbackText.setText(R.string.file_migration_preparing);
+
+		mFinishButton.setOnClickListener(new View.OnClickListener() {
+			@Override
+			public void onClick(View view) {
+				setResult(RESULT_CANCELED);
+				finish();
+			}
+		});
+
+		String source = getIntent().getStringExtra(KEY_MIGRATION_SOURCE_DIR);
+		String destination = getIntent().getStringExtra(KEY_MIGRATION_TARGET_DIR);
+
+		if (source == null || destination == null) {
+			Log_OC.e(TAG, "source or destination is null");
+			finish();
+		}
+
+		new FileMigrationTask().execute(source, destination);
+	}
+
+	private class FileMigrationTask extends AsyncTask<String, Integer, Integer> {
+
+		private String mStorageTarget;
+		private String mStorageSource;
+
+		private class MigrationException extends Exception {
+			private int mResId;
+			/*
+			 * @param resId resource identifier to use for displaying error
+			 */
+			MigrationException(int resId) {
+				super();
+				this.mResId = resId;
+			}
+
+			int getResId() { return mResId; }
+		}
+
+		@Override
+		protected Integer doInBackground(String... args) {
+
+			mStorageSource = args[0];
+			mStorageTarget = args[1];
+
+			int progress = 0;
+			publishProgress(progress++, R.string.file_migration_preparing);
+
+			Context context = StorageMigrationActivity.this;
+			String ocAuthority = context.getString(R.string.authority);
+
+			Account[] ocAccounts = AccountManager.get(context).getAccountsByType(MainApp.getAccountType());
+			boolean[] oldAutoSync = new boolean[ocAccounts.length];
+
+			try {
+				publishProgress(progress++, R.string.file_migration_checking_destination);
+
+				checkDestinationAvailability();
+
+				publishProgress(progress++, R.string.file_migration_saving_accounts_configuration);
+				saveAccountsSyncStatus(ocAuthority, ocAccounts, oldAutoSync);
+
+				publishProgress(progress++, R.string.file_migration_waiting_for_unfinished_sync);
+				stopAccountsSyncing(ocAuthority, ocAccounts);
+				waitForUnfinishedSynchronizations(ocAuthority, ocAccounts);
+
+				publishProgress(progress++, R.string.file_migration_migrating);
+				copyFiles();
+
+				publishProgress(progress++, R.string.file_migration_updating_index);
+				updateIndex(context);
+
+				publishProgress(progress++, R.string.file_migration_cleaning);
+				cleanup();
+
+			} catch (MigrationException e) {
+				return e.getResId();
+			} finally {
+				publishProgress(progress++, R.string.file_migration_restoring_accounts_configuration);
+				restoreAccountsSyncStatus(ocAuthority, ocAccounts, oldAutoSync);
+			}
+
+			publishProgress(progress++, R.string.file_migration_ok_finished);
+
+			return 0;
+		}
+
+		@Override
+		protected void onProgressUpdate(Integer... progress) {
+			mProgressBar.setProgress(progress[0]);
+			if (progress.length > 1)
+				mFeedbackText.setText(progress[1]);
+		}
+
+		@Override
+		protected void onPostExecute(Integer code) {
+			mFinishButton.setVisibility(View.VISIBLE);
+			if (code != 0) {
+				mFeedbackText.setText(code);
+			} else {
+				mFeedbackText.setText(R.string.file_migration_ok_finished);
+				mFinishButton.setOnClickListener(new View.OnClickListener() {
+					@Override
+					public void onClick(View view) {
+						Intent resultIntent = new Intent();
+						resultIntent.putExtra(KEY_MIGRATION_TARGET_DIR, mStorageTarget);
+						setResult(RESULT_OK, resultIntent);
+						finish();
+					}
+				});
+			}
+		}
+
+		void checkDestinationAvailability() throws MigrationException {
+			File srcFile = new File(mStorageSource);
+			File dstFile = new File(mStorageTarget);
+
+			if (!dstFile.canRead() || !srcFile.canRead())
+				throw new MigrationException(R.string.file_migration_failed_not_readable);
+
+			if (!dstFile.canWrite() || !srcFile.canWrite())
+				throw new MigrationException(R.string.file_migration_failed_not_writable);
+
+			if (new File(dstFile, MainApp.getDataFolder()).exists())
+				throw new MigrationException(R.string.file_migration_failed_dir_already_exists);
+
+			if (dstFile.getFreeSpace() < calculateUsedSpace(new File(srcFile, MainApp.getDataFolder())))
+				throw new MigrationException(R.string.file_migration_failed_not_enough_space);
+		}
+
+		void copyFiles() throws MigrationException {
+			File srcFile = new File(mStorageSource + File.separator + MainApp.getDataFolder());
+			File dstFile = new File(mStorageTarget + File.separator + MainApp.getDataFolder());
+
+			copyDirs(srcFile, dstFile);
+		}
+
+		private boolean copyFile(File src, File target) {
+			boolean ret = true;
+
+			InputStream in = null;
+			OutputStream out = null;
+
+			try {
+				in = new FileInputStream(src);
+				out = new FileOutputStream(target);
+				byte[] buf = new byte[1024];
+				int len;
+				while ((len = in.read(buf)) > 0) {
+					out.write(buf, 0, len);
+				}
+			} catch (IOException ex) {
+				ret = false;
+			} finally {
+				if (in != null) try {
+					in.close();
+				} catch (IOException e) {
+					e.printStackTrace(System.err);
+				}
+				if (out != null) try {
+					out.close();
+				} catch (IOException e) {
+					e.printStackTrace(System.err);
+				}
+			}
+
+			return ret;
+		}
+
+		void copyDirs(File src, File dst) throws MigrationException {
+			if (!dst.mkdirs())
+				throw new MigrationException(R.string.file_migration_failed_while_coping);
+
+			for (File f : src.listFiles()) {
+				if (f.isDirectory())
+					copyDirs(f, new File(dst, f.getName()));
+				else if (!copyFile(f, new File(dst, f.getName())))
+					throw new MigrationException(R.string.file_migration_failed_while_coping);
+			}
+
+		}
+
+		void updateIndex(Context context) throws MigrationException {
+			FileDataStorageManager manager = new FileDataStorageManager(null, context.getContentResolver());
+
+			try {
+				manager.migrateStoredFiles(mStorageSource, mStorageTarget);
+			} catch (Exception e) {
+				throw new MigrationException(R.string.file_migration_failed_while_updating_index);
+			}
+
+		}
+
+		void cleanup() {
+
+		}
+
+		long calculateUsedSpace(File dir) {
+			long result = 0;
+
+			for (File f : dir.listFiles()) {
+				if (f.isDirectory())
+					result += calculateUsedSpace(f);
+				else
+					result += f.length();
+			}
+
+			return result;
+		}
+
+		void saveAccountsSyncStatus(String authority, Account accounts[], boolean syncs[]) {
+			for (int i = 0; i < accounts.length; ++i) {
+				syncs[i] = ContentResolver.getSyncAutomatically(accounts[i], authority);
+			}
+		}
+
+		void stopAccountsSyncing(String authority, Account accounts[]) {
+			for (int i = 0; i < accounts.length; ++i)
+				ContentResolver.setSyncAutomatically(accounts[i], authority, false);
+		}
+
+		void waitForUnfinishedSynchronizations(String authority, Account accounts[]) {
+			for (int i = 0; i < accounts.length; ++i)
+				while (ContentResolver.isSyncActive(accounts[i], authority))
+					try {
+						Thread.sleep(1000);
+					} catch (InterruptedException e) {
+						Log_OC.w(TAG, "Thread interrupted while waiting for account to end syncing");
+					}
+		}
+
+
+		void restoreAccountsSyncStatus(String authority, Account accounts[], boolean oldSync[]) {
+			for (int i = 0; i < accounts.length; ++i)
+				ContentResolver.setSyncAutomatically(accounts[i], authority, oldSync[i]);
+		}
+
+	}
 }
 }