Add more migration stuff. cleanup step is still missing
authorBartosz Przybylski <bart.p.pl@gmail.com>
Sun, 8 Nov 2015 21:42:53 +0000 (22:42 +0100)
committerBartosz Przybylski <bart.p.pl@gmail.com>
Sun, 15 Nov 2015 19:35:13 +0000 (20:35 +0100)
AndroidManifest.xml
res/layout/migration_layout.xml [new file with mode: 0644]
res/values/strings.xml
src/com/owncloud/android/datamodel/FileDataStorageManager.java
src/com/owncloud/android/ui/activity/Preferences.java
src/com/owncloud/android/ui/activity/StorageMigrationActivity.java

index d97abbe..ac57163 100644 (file)
@@ -60,6 +60,7 @@
         </activity>
         <activity android:name=".ui.activity.UploadFilesActivity" />
         <activity android:name=".ui.activity.LocalDirectorySelectorActivity" />
         </activity>
         <activity android:name=".ui.activity.UploadFilesActivity" />
         <activity android:name=".ui.activity.LocalDirectorySelectorActivity" />
+        <activity android:name=".ui.activity.StorageMigrationActivity" />
         <activity android:name=".ui.activity.Uploader" >
             <intent-filter>
                 <action android:name="android.intent.action.SEND" />
         <activity android:name=".ui.activity.Uploader" >
             <intent-filter>
                 <action android:name="android.intent.action.SEND" />
diff --git a/res/layout/migration_layout.xml b/res/layout/migration_layout.xml
new file mode 100644 (file)
index 0000000..84b5c22
--- /dev/null
@@ -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>
\ No newline at end of file
index 0dcca4c..4be03c1 100644 (file)
     <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="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="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_add_account">Add account</string>
     <string name="auth_redirect_non_secure_connection_title">Secure connection is redirected through an unsecured route.</string>
     <string name="prefs_category_accounts">Accounts</string>
     <string name="prefs_add_account">Add account</string>
     <string name="auth_redirect_non_secure_connection_title">Secure connection is redirected through an unsecured route.</string>
index 237c846..3642d50 100644 (file)
@@ -42,6 +42,7 @@ import android.content.OperationApplicationException;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.RemoteException;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.RemoteException;
+import android.provider.BaseColumns;
 import android.provider.MediaStore;
 
 import com.owncloud.android.MainApp;
 import android.provider.MediaStore;
 
 import com.owncloud.android.MainApp;
@@ -752,6 +753,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*/) {
 
index 915e484..60687ce 100644 (file)
@@ -619,13 +619,12 @@ public class Preferences extends PreferenceActivity
                 migrationIntent.putExtra(StorageMigrationActivity.KEY_MIGRATION_SOURCE_DIR,
                         currentStorageDir.getAbsolutePath());
                 migrationIntent.putExtra(StorageMigrationActivity.KEY_MIGRATION_TARGET_DIR,
                 migrationIntent.putExtra(StorageMigrationActivity.KEY_MIGRATION_SOURCE_DIR,
                         currentStorageDir.getAbsolutePath());
                 migrationIntent.putExtra(StorageMigrationActivity.KEY_MIGRATION_TARGET_DIR,
-                        currentStorageDir.getAbsolutePath());
+                        upcomingStorageDir.getAbsolutePath());
                 startActivityForResult(migrationIntent, ACTION_PERFORM_MIGRATION);
             }
         } else if (requestCode == ACTION_PERFORM_MIGRATION && resultCode == RESULT_OK) {
             String resultStorageDir = data.getStringExtra(StorageMigrationActivity.KEY_MIGRATION_TARGET_DIR);
                 startActivityForResult(migrationIntent, ACTION_PERFORM_MIGRATION);
             }
         } else if (requestCode == ACTION_PERFORM_MIGRATION && resultCode == RESULT_OK) {
             String resultStorageDir = data.getStringExtra(StorageMigrationActivity.KEY_MIGRATION_TARGET_DIR);
-            mStoragePath = resultStorageDir;
-            mPrefStoragePath.setSummary(mStoragePath);
+            saveStoragePath(resultStorageDir);
         }
     }
 
         }
     }
 
@@ -835,6 +834,19 @@ public class Preferences extends PreferenceActivity
     }
 
     /**
     }
 
     /**
+     * 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
      */
     private void loadStoragePath() {
      * Load storage path set on preferences
      */
     private void loadStoragePath() {
@@ -842,7 +854,7 @@ public class Preferences extends PreferenceActivity
                 PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
         mStoragePath = appPrefs.getString("storage_path", Environment.getExternalStorageDirectory()
                                                          .getAbsolutePath());
                 PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
         mStoragePath = appPrefs.getString("storage_path", Environment.getExternalStorageDirectory()
                                                          .getAbsolutePath());
-        mPrefStoragePath.setSummary(mStoragePath + File.separator + MainApp.getDataFolder());
+        mPrefStoragePath.setSummary(mStoragePath);
     }
 
     /**
     }
 
     /**
index c5dc59b..5ff18c2 100644 (file)
  */
 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.
  */
 public class StorageMigrationActivity extends AppCompatActivity {
 
 /**
  * Created by Bartosz Przybylski on 07.11.2015.
  */
 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_SOURCE_DIR = "MIGRATION_SOURCE";
        public static final String KEY_MIGRATION_TARGET_DIR = "MIGRATION_TARGET";
        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]);
+               }
+
+       }
 }
 }