Browse Source

Merge pull request #92 from nextcloud/externalSD2

External SD support
Andy Scherzinger 8 years ago
parent
commit
9f8fdaeac0
22 changed files with 1483 additions and 103 deletions
  1. 5 0
      AndroidManifest.xml
  2. 52 0
      res/layout/migration_layout.xml
  3. 28 0
      res/values/strings.xml
  4. 5 0
      res/xml/preferences.xml
  5. 20 3
      src/com/owncloud/android/MainApp.java
  6. 53 33
      src/com/owncloud/android/datamodel/FileDataStorageManager.java
  7. 96 0
      src/com/owncloud/android/datastorage/DataStorageProvider.java
  8. 43 0
      src/com/owncloud/android/datastorage/StoragePoint.java
  9. 54 0
      src/com/owncloud/android/datastorage/UniqueStorageList.java
  10. 64 0
      src/com/owncloud/android/datastorage/providers/AbstractCommandLineStoragePoint.java
  11. 42 0
      src/com/owncloud/android/datastorage/providers/AbstractStoragePointProvider.java
  12. 58 0
      src/com/owncloud/android/datastorage/providers/EnvironmentStoragePointProvider.java
  13. 56 0
      src/com/owncloud/android/datastorage/providers/HardcodedStoragePointProvider.java
  14. 48 0
      src/com/owncloud/android/datastorage/providers/IStoragePointProvider.java
  15. 69 0
      src/com/owncloud/android/datastorage/providers/MountCommandStoragePointProvider.java
  16. 52 0
      src/com/owncloud/android/datastorage/providers/SystemDefaultStoragePointProvider.java
  17. 79 0
      src/com/owncloud/android/datastorage/providers/VDCStoragePointProvider.java
  18. 54 0
      src/com/owncloud/android/ui/activity/LocalDirectorySelectorActivity.java
  19. 119 39
      src/com/owncloud/android/ui/activity/Preferences.java
  20. 424 0
      src/com/owncloud/android/ui/activity/StorageMigration.java
  21. 2 2
      src/com/owncloud/android/ui/activity/UploadFilesActivity.java
  22. 60 26
      src/com/owncloud/android/utils/FileStorageUtils.java

+ 5 - 0
AndroidManifest.xml

@@ -56,6 +56,9 @@
     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
     <uses-permission android:name="android.permission.WAKE_LOCK" />
 
+    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+
     <application
         android:name=".MainApp"
         android:icon="@mipmap/ic_launcher"
@@ -79,6 +82,8 @@
                   android:taskAffinity=""
                   android:excludeFromRecents="true"
                   android:theme="@style/Theme.ownCloud.NoActionBar">
+        <activity android:name=".ui.activity.LocalDirectorySelectorActivity" />
+        <activity android:name=".ui.activity.StorageMigrationActivity" />
             <intent-filter>
                 <action android:name="android.intent.action.SEND" />
 

+ 52 - 0
res/layout/migration_layout.xml

@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Nextcloud Android client application
+
+  Copyright (C) 2016 Bartosz Przybylski
+  Copyright (C) 2016 Nextcloud
+
+  This program is free software; you can redistribute it and/or
+  modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+  License as published by the Free Software Foundation; either
+  version 3 of the License, or any later version.
+
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+
+  You should have received a copy of the GNU Affero General Public
+  License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:layout_width="match_parent"
+              android:layout_height="match_parent"
+              android:gravity="center_vertical"
+              android:orientation="vertical">
+
+    <ProgressBar
+        android:id="@+id/migrationProgress"
+        style="?android:attr/progressBarStyleHorizontal"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_horizontal"
+        android:paddingLeft="30dp"
+        android:paddingRight="30dp"
+        android:progress="50"/>
+
+    <TextView
+        android:id="@+id/migrationText"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_horizontal"
+        android:text=""
+        android:textAppearance="?android:attr/textAppearanceMedium"/>
+
+    <Button
+        android:id="@+id/finishButton"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_horizontal"
+        android:text="@string/drawer_close"/>
+</LinearLayout>

+ 28 - 0
res/values/strings.xml

@@ -356,6 +356,28 @@
     <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_dialog_title">Updating storage path</string>
+    <string name="file_migration_finish_button">Finish</string>
+    <string name="file_migration_preparing">Preparing for migration&#8230;</string>
+    <string name="file_migration_checking_destination">Checking destination&#8230;</string>
+    <string name="file_migration_saving_accounts_configuration">Saving accounts configuration&#8230;</string>
+    <string name="file_migration_waiting_for_unfinished_sync">Waiting for unfinished synchronizations&#8230;</string>
+    <string name="file_migration_migrating">Moving data&#8230;</string>
+    <string name="file_migration_updating_index">Updating index&#8230;</string>
+    <string name="file_migration_cleaning">Cleaning&#8230;</string>
+    <string name="file_migration_restoring_accounts_configuration">Restoring accounts configuration&#8230;</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: Nextcloud 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="file_migration_directory_already_exists">Data folder already exists, what to do?</string>
+    <string name="file_migration_override_data_folder">Override</string>
+    <string name="file_migration_use_data_folder">Use existing</string>
+
     <string name="prefs_category_accounts">Accounts</string>
     <string name="prefs_add_account">Add account</string>
     <string name="drawer_manage_accounts">Manage accounts</string>
@@ -422,6 +444,8 @@
     <string name="pref_behaviour_entries_keep_file">kept in original folder</string>
     <string name="pref_behaviour_entries_move">moved to app folder</string>
     <string name="pref_behaviour_entries_delete_file">deleted</string>
+    <string name="prefs_storage_path">Storage path</string>
+    <string name="prefs_common">Common</string>
 
     <string name="share_dialog_title">Sharing</string>
     <string name="share_file">Share %1$s</string>
@@ -506,4 +530,8 @@
         <item quantity="other">%d selected</item>
     </plurals>
 
+    <string name="storage_description_default">Default</string>
+    <string name="storage_description_sd_no">SD card %1$d</string>
+    <string name="storage_description_unknown">Unknown</string>
+
 </resources>

+ 5 - 0
res/xml/preferences.xml

@@ -18,6 +18,11 @@
   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 -->
 <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" >
+	<PreferenceCategory android:title="@string/prefs_category_general">
+		<ListPreference
+			android:title="@string/prefs_storage_path"
+			android:key="storage_path" />
+	</PreferenceCategory>
 
     <PreferenceCategory android:title="@string/prefs_category_instant_uploading" android:key="instant_uploading_category">
 		<com.owncloud.android.ui.CheckBoxPreferenceWithLongTitle android:key="instant_uploading"

+ 20 - 3
src/com/owncloud/android/MainApp.java

@@ -23,16 +23,19 @@ package com.owncloud.android;
 import android.app.Activity;
 import android.app.Application;
 import android.content.Context;
+import android.content.SharedPreferences;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.os.Bundle;
 import android.os.Environment;
+import android.preference.PreferenceManager;
 
 import com.owncloud.android.authentication.PassCodeManager;
 import com.owncloud.android.datamodel.ThumbnailsCacheManager;
 import com.owncloud.android.lib.common.OwnCloudClientManagerFactory;
 import com.owncloud.android.lib.common.OwnCloudClientManagerFactory.Policy;
 import com.owncloud.android.lib.common.utils.Log_OC;
+import com.owncloud.android.ui.activity.Preferences;
 
 
 /**
@@ -54,13 +57,20 @@ public class MainApp extends Application {
 
     private static Context mContext;
 
+    private static String storagePath;
+
     private static boolean mOnlyOnDevice = false;
 
     
     public void onCreate(){
         super.onCreate();
         MainApp.mContext = getApplicationContext();
-        
+
+        SharedPreferences appPrefs =
+                PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
+        MainApp.storagePath = appPrefs.getString(Preferences.Keys.STORAGE_PATH, Environment.
+                              getExternalStorageDirectory().getAbsolutePath());
+
         boolean isSamlAuth = AUTH_ON.equals(getString(R.string.auth_method_saml_web_sso));
 
         OwnCloudClientManagerFactory.setUserAgent(getUserAgent());
@@ -80,8 +90,7 @@ public class MainApp extends Application {
             // Set folder for store logs
             Log_OC.setLogDataFolder(dataFolder);
 
-            //TODO: to be changed/fixed whenever SD card support gets merged.
-            Log_OC.startLogging(Environment.getExternalStorageDirectory().getAbsolutePath());
+            Log_OC.startLogging(MainApp.storagePath);
             Log_OC.d("Debug", "start logging");
         }
 
@@ -132,6 +141,14 @@ public class MainApp extends Application {
         return MainApp.mContext;
     }
 
+    public static String getStoragePath(){
+        return MainApp.storagePath;
+    }
+
+    public static void setStoragePath(String path){
+        MainApp.storagePath = path;
+    }
+
     // Methods to obtain Strings referring app_name 
     //   From AccountAuthenticator 
     //   public static final String ACCOUNT_TYPE = "owncloud";    

+ 53 - 33
src/com/owncloud/android/datamodel/FileDataStorageManager.java

@@ -46,11 +46,6 @@ import com.owncloud.android.utils.MimeType;
 import com.owncloud.android.utils.MimeTypeUtil;
 
 import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -727,44 +722,69 @@ public class FileDataStorageManager {
                 if (!targetFolder.exists()) {
                     targetFolder.mkdirs();
                 }
-                copied = copyFile(localFile, targetFile);
+                copied = FileStorageUtils.copyFile(localFile, targetFile);
             }
             Log_OC.d(TAG, "Local file COPIED : " + copied);
         }
     }
 
-    private boolean copyFile(File src, File target) {
-        boolean ret = true;
-
-        InputStream in = null;
-        OutputStream out = null;
+    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);
 
-        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);
+        } 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;
             }
         }
 
-        return ret;
-    }
+        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
+        if (getContentResolver() != null) {
+            getContentResolver().applyBatch(MainApp.getAuthority(), operations);
+        } else {
+            getContentProviderClient().applyBatch(operations);
+        }
+    }
 
     private Vector<OCFile> getFolderContent(long parentId, boolean onlyOnDevice) {
 

+ 96 - 0
src/com/owncloud/android/datastorage/DataStorageProvider.java

@@ -0,0 +1,96 @@
+/**
+ *   Nextcloud Android client application
+ *
+ *   @author Bartosz Przybylski
+ *   Copyright (C) 2016 Nextcloud
+ *   Copyright (C) 2016 Bartosz Przybylski
+ *
+ *   This program is free software; you can redistribute it and/or
+ *   modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ *   License as published by the Free Software Foundation; either
+ *   version 3 of the License, or any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ *   You should have received a copy of the GNU Affero General Public
+ *   License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.datastorage;
+
+import android.os.Build;
+
+import com.owncloud.android.MainApp;
+import com.owncloud.android.R;
+import com.owncloud.android.datastorage.providers.EnvironmentStoragePointProvider;
+import com.owncloud.android.datastorage.providers.HardcodedStoragePointProvider;
+import com.owncloud.android.datastorage.providers.IStoragePointProvider;
+import com.owncloud.android.datastorage.providers.MountCommandStoragePointProvider;
+import com.owncloud.android.datastorage.providers.SystemDefaultStoragePointProvider;
+import com.owncloud.android.datastorage.providers.VDCStoragePointProvider;
+
+import java.io.File;
+import java.util.Vector;
+
+/**
+ * @author Bartosz Przybylski
+ */
+public class DataStorageProvider {
+
+    private static final Vector<IStoragePointProvider> mStorageProviders = new Vector<>();
+    private static final UniqueStorageList mCachedStoragePoints = new UniqueStorageList();
+    private static final DataStorageProvider sInstance = new DataStorageProvider() {{
+        // There is no system wide way to get usb storage so we need to provide multiple
+        // handcrafted ways to add those.
+        addStoragePointProvider(new SystemDefaultStoragePointProvider());
+        addStoragePointProvider(new EnvironmentStoragePointProvider());
+        addStoragePointProvider(new VDCStoragePointProvider());
+        addStoragePointProvider(new MountCommandStoragePointProvider());
+        addStoragePointProvider(new HardcodedStoragePointProvider());
+    }};
+
+
+    public static DataStorageProvider getInstance() {
+        return sInstance;
+    }
+
+    private DataStorageProvider() {}
+
+    public StoragePoint[] getAvailableStoragePoints() {
+        if (mCachedStoragePoints.size() != 0)
+            return mCachedStoragePoints.toArray(new StoragePoint[mCachedStoragePoints.size()]);
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            for (File f : MainApp.getAppContext().getExternalFilesDirs(null)) {
+                if (f != null) {
+                    mCachedStoragePoints.add(new StoragePoint(f.getAbsolutePath(), f.getAbsolutePath()));
+                }
+            }
+        } else {
+            for (IStoragePointProvider p : mStorageProviders)
+                if (p.canProvideStoragePoints()) {
+                    mCachedStoragePoints.addAll(p.getAvailableStoragePoint());
+                }
+        }
+
+        return mCachedStoragePoints.toArray(new StoragePoint[mCachedStoragePoints.size()]);
+    }
+
+    public String getStorageDescriptionByPath(String path) {
+        for (StoragePoint s : getAvailableStoragePoints())
+            if (s.getPath().equals(path))
+                return s.getDescription();
+        return MainApp.getAppContext().getString(R.string.storage_description_unknown);
+    }
+
+    public void addStoragePointProvider(IStoragePointProvider provider) {
+        mStorageProviders.add(provider);
+    }
+
+    public void removeStoragePointProvider(IStoragePointProvider provider) {
+        mStorageProviders.remove(provider);
+    }
+}

+ 43 - 0
src/com/owncloud/android/datastorage/StoragePoint.java

@@ -0,0 +1,43 @@
+/**
+ *   Nextcloud Android client application
+ *
+ *   @author Bartosz Przybylski
+ *   Copyright (C) 2016 Nextcloud
+ *   Copyright (C) 2016 Bartosz Przybylski
+ *
+ *   This program is free software; you can redistribute it and/or
+ *   modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ *   License as published by the Free Software Foundation; either
+ *   version 3 of the License, or any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ *   You should have received a copy of the GNU Affero General Public
+ *   License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.datastorage;
+
+/**
+ * @author Bartosz Przybylski
+ */
+public class StoragePoint implements Comparable<StoragePoint> {
+    private String mDescription;
+    private String mPath;
+
+    public StoragePoint(String description, String path) {
+        mDescription = description;
+        mPath = path;
+    }
+
+    public String getPath() { return mPath; }
+    public String getDescription() { return mDescription; }
+
+    @Override
+    public int compareTo(StoragePoint another) {
+        return mPath.compareTo(another.getPath());
+    }
+}

+ 54 - 0
src/com/owncloud/android/datastorage/UniqueStorageList.java

@@ -0,0 +1,54 @@
+/**
+ *   Nextcloud Android client application
+ *
+ *   @author Bartosz Przybylski
+ *   Copyright (C) 2016 Nextcloud
+ *   Copyright (C) 2016 Bartosz Przybylski
+ *
+ *   This program is free software; you can redistribute it and/or
+ *   modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ *   License as published by the Free Software Foundation; either
+ *   version 3 of the License, or any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ *   You should have received a copy of the GNU Affero General Public
+ *   License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.datastorage;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Vector;
+
+/**
+ * @author Bartosz Przybylski
+ */
+public class UniqueStorageList extends Vector<StoragePoint> {
+    @Override
+    public boolean add(StoragePoint sp) {
+        try {
+            for (StoragePoint s : this) {
+                String thisCanonPath = new File(s.getPath()).getCanonicalPath();
+                String otherCanonPath = new File(sp.getPath()).getCanonicalPath();
+                if (thisCanonPath.equals(otherCanonPath))
+                    return true;
+            }
+        } catch (IOException e) {
+            return false;
+        }
+        return super.add(sp);
+    }
+
+    @Override
+    public synchronized boolean addAll(Collection<? extends StoragePoint> collection) {
+        for (StoragePoint sp : collection)
+            add(sp);
+        return true;
+    }
+}

+ 64 - 0
src/com/owncloud/android/datastorage/providers/AbstractCommandLineStoragePoint.java

@@ -0,0 +1,64 @@
+/**
+ *   Nextcloud Android client application
+ *
+ *   @author Bartosz Przybylski
+ *   Copyright (C) 2016 Nextcloud
+ *   Copyright (C) 2016 Bartosz Przybylski
+ *
+ *   This program is free software; you can redistribute it and/or
+ *   modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ *   License as published by the Free Software Foundation; either
+ *   version 3 of the License, or any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ *   You should have received a copy of the GNU Affero General Public
+ *   License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.datastorage.providers;
+
+import java.io.InputStream;
+import java.util.Arrays;
+
+/**
+ * @author Bartosz Przybylski
+ */
+abstract public class AbstractCommandLineStoragePoint extends AbstractStoragePointProvider {
+
+    static protected final int sCommandLineOKReturnValue = 0;
+
+    protected abstract String[] getCommand();
+
+    @Override
+    public boolean canProvideStoragePoints() {
+        Process process;
+        try {
+            process = new ProcessBuilder().command(Arrays.asList(getCommand())).start();
+            process.waitFor();
+        } catch (Exception e) {
+            return false;
+        }
+        return process != null && process.exitValue() == sCommandLineOKReturnValue;
+    }
+
+    protected String getCommandLineResult() {
+        String s = "";
+        try {
+            final Process process = new ProcessBuilder().command(getCommand())
+                    .redirectErrorStream(true).start();
+
+            process.waitFor();
+            final InputStream is = process.getInputStream();
+            final byte buffer[] = new byte[1024];
+            while (is.read(buffer) != -1)
+                s += new String(buffer);
+            is.close();
+        } catch (final Exception e) { }
+        return s;
+    }
+
+}

+ 42 - 0
src/com/owncloud/android/datastorage/providers/AbstractStoragePointProvider.java

@@ -0,0 +1,42 @@
+/**
+ *   Nextcloud Android client application
+ *
+ *   @author Bartosz Przybylski
+ *   Copyright (C) 2016 Nextcloud
+ *   Copyright (C) 2016 Bartosz Przybylski
+ *
+ *   This program is free software; you can redistribute it and/or
+ *   modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ *   License as published by the Free Software Foundation; either
+ *   version 3 of the License, or any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ *   You should have received a copy of the GNU Affero General Public
+ *   License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.datastorage.providers;
+
+import com.owncloud.android.datastorage.StoragePoint;
+
+import java.io.File;
+import java.util.Vector;
+
+/**
+ * @author Bartosz Przybylski
+ */
+abstract public class AbstractStoragePointProvider implements IStoragePointProvider {
+
+    protected boolean canBeAddedToAvailableList(Vector<StoragePoint> currentList, String path) {
+        if (path == null) return false;
+        for (StoragePoint storage : currentList)
+            if (storage.getPath().equals(path))
+                return false;
+        File f = new File(path);
+        return f.exists() && f.isDirectory() && f.canRead() && f.canWrite();
+    }
+}

+ 58 - 0
src/com/owncloud/android/datastorage/providers/EnvironmentStoragePointProvider.java

@@ -0,0 +1,58 @@
+/**
+ *   Nextcloud Android client application
+ *
+ *   @author Bartosz Przybylski
+ *   Copyright (C) 2016 Nextcloud
+ *   Copyright (C) 2016 Bartosz Przybylski
+ *
+ *   This program is free software; you can redistribute it and/or
+ *   modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ *   License as published by the Free Software Foundation; either
+ *   version 3 of the License, or any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ *   You should have received a copy of the GNU Affero General Public
+ *   License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.datastorage.providers;
+
+import android.text.TextUtils;
+
+import com.owncloud.android.datastorage.StoragePoint;
+
+import java.util.Vector;
+
+/**
+ * @author Bartosz Przybylski
+ */
+public class EnvironmentStoragePointProvider extends AbstractStoragePointProvider {
+
+    private static final String sSecondaryStorageEnvName = "SECONDARY_STORAGE";
+
+    @Override
+    public boolean canProvideStoragePoints() {
+        return !TextUtils.isEmpty(System.getenv(sSecondaryStorageEnvName));
+    }
+
+    @Override
+    public Vector<StoragePoint> getAvailableStoragePoint() {
+        Vector<StoragePoint> result = new Vector<>();
+
+        addEntriesFromEnv(result, sSecondaryStorageEnvName);
+
+        return result;
+    }
+
+    private void addEntriesFromEnv(Vector<StoragePoint> result, String envName) {
+        String env = System.getenv(envName);
+        if (env != null)
+            for (String p : env.split(":"))
+                if (canBeAddedToAvailableList(result, p))
+                    result.add(new StoragePoint(p, p));
+    }
+}

+ 56 - 0
src/com/owncloud/android/datastorage/providers/HardcodedStoragePointProvider.java

@@ -0,0 +1,56 @@
+/**
+ *   Nextcloud Android client application
+ *
+ *   @author Bartosz Przybylski
+ *   Copyright (C) 2016 Nextcloud
+ *   Copyright (C) 2016 Bartosz Przybylski
+ *
+ *   This program is free software; you can redistribute it and/or
+ *   modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ *   License as published by the Free Software Foundation; either
+ *   version 3 of the License, or any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ *   You should have received a copy of the GNU Affero General Public
+ *   License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.datastorage.providers;
+
+import com.owncloud.android.datastorage.StoragePoint;
+
+import java.util.Vector;
+
+/**
+ * @author Bartosz Przybylski
+ */
+public class HardcodedStoragePointProvider extends AbstractStoragePointProvider {
+
+    static private final String[] sPaths = {
+            "/mnt/external_sd/",
+            "/mnt/extSdCard/",
+            "/storage/extSdCard",
+            "/storage/sdcard1/",
+            "/storage/usbcard1/"
+    };
+
+    @Override
+    public boolean canProvideStoragePoints() {
+        return true;
+    }
+
+    @Override
+    public Vector<StoragePoint> getAvailableStoragePoint() {
+        Vector<StoragePoint> result = new Vector<>();
+
+        for (String s : sPaths)
+            if (canBeAddedToAvailableList(result, s))
+                result.add(new StoragePoint(s, s));
+
+        return result;
+    }
+}

+ 48 - 0
src/com/owncloud/android/datastorage/providers/IStoragePointProvider.java

@@ -0,0 +1,48 @@
+/**
+ *   Nextcloud Android client application
+ *
+ *   @author Bartosz Przybylski
+ *   Copyright (C) 2016 Nextcloud
+ *   Copyright (C) 2016 Bartosz Przybylski
+ *
+ *   This program is free software; you can redistribute it and/or
+ *   modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ *   License as published by the Free Software Foundation; either
+ *   version 3 of the License, or any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ *   You should have received a copy of the GNU Affero General Public
+ *   License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.datastorage.providers;
+
+import com.owncloud.android.datastorage.StoragePoint;
+
+import java.util.Vector;
+
+/**
+ * @author Bartosz Przybylski
+ */
+public interface IStoragePointProvider {
+
+    /**
+     *  This method is used for querying storage provider to check if it can provide
+     *  usable and reliable data storage places.
+     *
+     *  @return true if provider can reliably return storage path
+     */
+    boolean canProvideStoragePoints();
+
+
+    /**
+     *
+     * @return available storage points
+     */
+    Vector<StoragePoint> getAvailableStoragePoint();
+
+}

+ 69 - 0
src/com/owncloud/android/datastorage/providers/MountCommandStoragePointProvider.java

@@ -0,0 +1,69 @@
+/**
+ *   Nextcloud Android client application
+ *
+ *   @author Bartosz Przybylski
+ *   Copyright (C) 2016 Nextcloud
+ *   Copyright (C) 2016 Bartosz Przybylski
+ *
+ *   This program is free software; you can redistribute it and/or
+ *   modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ *   License as published by the Free Software Foundation; either
+ *   version 3 of the License, or any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ *   You should have received a copy of the GNU Affero General Public
+ *   License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.datastorage.providers;
+
+import com.owncloud.android.datastorage.StoragePoint;
+
+import java.util.Locale;
+import java.util.Vector;
+import java.util.regex.Pattern;
+
+/**
+ * @author Bartosz Przybylski
+ */
+public class MountCommandStoragePointProvider extends AbstractCommandLineStoragePoint {
+
+    static private final String[] sCommand = new String[] { "mount" };
+
+    private static Pattern sPattern = Pattern.compile("(?i).*vold.*(vfat|ntfs|exfat|fat32|ext3|ext4).*rw.*");
+
+    @Override
+    protected String[] getCommand() {
+        return sCommand;
+    }
+
+    @Override
+    public Vector<StoragePoint> getAvailableStoragePoint() {
+        Vector<StoragePoint> result = new Vector<>();
+
+        for (String p : getPotentialPaths(getCommandLineResult()))
+            if (canBeAddedToAvailableList(result, p))
+                result.add(new StoragePoint(p, p));
+
+        return result;
+    }
+
+    private Vector<String> getPotentialPaths(String mounted) {
+        final Vector<String> result = new Vector<>();
+
+        for (String line : mounted.split("\n"))
+            if (!line.toLowerCase(Locale.US).contains("asec") && sPattern.matcher(line).matches()) {
+                String parts[] = line.split(" ");
+                for (String path : parts) {
+                    if (path.startsWith("/") &&
+                            !path.toLowerCase(Locale.US).contains("vold"))
+                        result.add(path);
+                }
+            }
+        return result;
+    }
+}

+ 52 - 0
src/com/owncloud/android/datastorage/providers/SystemDefaultStoragePointProvider.java

@@ -0,0 +1,52 @@
+/**
+ *   Nextcloud Android client application
+ *
+ *   @author Bartosz Przybylski
+ *   Copyright (C) 2016 Nextcloud
+ *   Copyright (C) 2016 Bartosz Przybylski
+ *
+ *   This program is free software; you can redistribute it and/or
+ *   modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ *   License as published by the Free Software Foundation; either
+ *   version 3 of the License, or any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ *   You should have received a copy of the GNU Affero General Public
+ *   License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.datastorage.providers;
+
+import android.os.Environment;
+
+import com.owncloud.android.MainApp;
+import com.owncloud.android.R;
+import com.owncloud.android.datastorage.StoragePoint;
+
+import java.util.Vector;
+
+/**
+ * @author Bartosz Przybylski
+ */
+public class SystemDefaultStoragePointProvider extends AbstractStoragePointProvider {
+    @Override
+    public boolean canProvideStoragePoints() {
+        return true;
+    }
+
+    @Override
+    public Vector<StoragePoint> getAvailableStoragePoint() {
+        Vector<StoragePoint> result = new Vector<>();
+
+        final String defaultStringDesc =
+                MainApp.getAppContext().getString(R.string.storage_description_default);
+        final String path = Environment.getExternalStorageDirectory().getAbsolutePath();
+        result.add(new StoragePoint(defaultStringDesc, path));
+
+        return result;
+    }
+}

+ 79 - 0
src/com/owncloud/android/datastorage/providers/VDCStoragePointProvider.java

@@ -0,0 +1,79 @@
+/**
+ *   Nextcloud Android client application
+ *
+ *   @author Bartosz Przybylski
+ *   Copyright (C) 2016 Nextcloud
+ *   Copyright (C) 2016 Bartosz Przybylski
+ *
+ *   This program is free software; you can redistribute it and/or
+ *   modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ *   License as published by the Free Software Foundation; either
+ *   version 3 of the License, or any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ *   You should have received a copy of the GNU Affero General Public
+ *   License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.datastorage.providers;
+
+import com.owncloud.android.datastorage.StoragePoint;
+import com.owncloud.android.lib.common.utils.Log_OC;
+
+import java.util.Vector;
+
+/**
+ * @author Bartosz Przybylski
+ */
+public class VDCStoragePointProvider extends AbstractCommandLineStoragePoint {
+
+    static private final String TAG = VDCStoragePointProvider.class.getSimpleName();
+
+    static private final String[] sVDCVolListCommand = new String[]{ "/system/bin/vdc", "volume", "list" };
+    static private final int sVDCVolumeList = 110;
+
+
+    @Override
+    public Vector<StoragePoint> getAvailableStoragePoint() {
+        Vector<StoragePoint> result = new Vector<>();
+
+        result.addAll(getPaths(getCommandLineResult()));
+
+        return result;
+    }
+
+    @Override
+    protected String[] getCommand() {
+        return sVDCVolListCommand;
+    }
+
+    private Vector<StoragePoint> getPaths(String vdcResources) {
+        Vector<StoragePoint> result = new Vector<>();
+
+        for (String line : vdcResources.split("\n")) {
+            String vdcLine[] = line.split(" ");
+            try {
+                int status = Integer.parseInt(vdcLine[0]);
+                if (status != sVDCVolumeList)
+                    continue;
+                final String description = vdcLine[1];
+                final String path = vdcLine[2];
+
+                if (canBeAddedToAvailableList(result, path))
+                    result.add(new StoragePoint(description, path));
+
+            } catch (NumberFormatException e) {
+                Log_OC.e(TAG, "Incorrect VDC output format " + e);
+            } catch (Exception e) {
+                Log_OC.e(TAG, "Unexpected exception on VDC parsing " + e);
+            }
+        }
+
+        return result;
+    }
+
+}

+ 54 - 0
src/com/owncloud/android/ui/activity/LocalDirectorySelectorActivity.java

@@ -0,0 +1,54 @@
+/**
+ *   Nextcloud Android client application
+ *
+ *   @author Bartosz Przybylski
+ *   Copyright (C) 2016 ownCloud Inc.
+ *   Copyright (C) 2016 Nextcloud
+ *   Copyright (C) 2016 Bartosz Przybylski
+ *
+ *   This program is free software; you can redistribute it and/or
+ *   modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ *   License as published by the Free Software Foundation; either
+ *   version 3 of the License, or any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ *   You should have received a copy of the GNU Affero General Public
+ *   License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.owncloud.android.ui.activity;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+
+import com.owncloud.android.R;
+
+/**
+ * Created by Bartosz Przybylski on 07.11.2015.
+ */
+public class LocalDirectorySelectorActivity extends UploadFilesActivity {
+
+	@Override
+	public void onCreate(Bundle savedInstanceState) {
+		super.onCreate(savedInstanceState);
+		mUploadBtn.setText(R.string.folder_picker_choose_button_text);
+	}
+
+	@Override
+	public void onClick(View v) {
+		if (v.getId() == R.id.upload_files_btn_cancel) {
+			setResult(RESULT_CANCELED);
+			finish();
+
+		} else if (v.getId() == R.id.upload_files_btn_upload) {
+			Intent resultIntent = new Intent();
+			resultIntent.putExtra(EXTRA_CHOSEN_FILES, getInitialDirectory().getAbsolutePath());
+			setResult(RESULT_OK, resultIntent);
+			finish();
+		}
+	}
+}

+ 119 - 39
src/com/owncloud/android/ui/activity/Preferences.java

@@ -31,7 +31,9 @@ import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.res.Configuration;
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.Environment;
 import android.preference.CheckBoxPreference;
+import android.preference.ListPreference;
 import android.preference.Preference;
 import android.preference.Preference.OnPreferenceChangeListener;
 import android.preference.Preference.OnPreferenceClickListener;
@@ -55,21 +57,24 @@ import com.owncloud.android.MainApp;
 import com.owncloud.android.R;
 import com.owncloud.android.authentication.AccountUtils;
 import com.owncloud.android.datamodel.OCFile;
+import com.owncloud.android.datastorage.DataStorageProvider;
+import com.owncloud.android.datastorage.StoragePoint;
 import com.owncloud.android.lib.common.OwnCloudAccount;
 import com.owncloud.android.lib.common.OwnCloudClientManagerFactory;
 import com.owncloud.android.lib.common.utils.Log_OC;
+import com.owncloud.android.ui.PreferenceWithLongSummary;
 import com.owncloud.android.utils.DisplayUtils;
 
 import java.io.IOException;
 
-
 /**
  * An Activity that allows the user to change the application's settings.
  *
  * It proxies the necessary calls via {@link android.support.v7.app.AppCompatDelegate} to be used
  * with AppCompat.
  */
-public class Preferences extends PreferenceActivity {
+public class Preferences extends PreferenceActivity
+        implements StorageMigration.StorageMigrationProgressListener {
     
     private static final String TAG = Preferences.class.getSimpleName();
 
@@ -90,7 +95,6 @@ public class Preferences extends PreferenceActivity {
     private Preference pAboutApp;
     private AppCompatDelegate mDelegate;
 
-    private PreferenceCategory mAccountsPrefCategory = null;
     private String mUploadPath;
     private PreferenceCategory mPrefInstantUploadCategory;
     private Preference mPrefInstantUpload;
@@ -105,6 +109,14 @@ public class Preferences extends PreferenceActivity {
     private Preference mPrefInstantVideoUploadPathWiFi;
     private Preference mPrefInstantVideoUploadOnlyOnCharging;
     private String mUploadVideoPath;
+    private ListPreference mPrefStoragePath;
+    private String mStoragePath;
+
+    public static class Keys {
+        public static final String STORAGE_PATH = "storage_path";
+        public static final String INSTANT_UPLOAD_PATH = "instant_upload_path";
+        public static final String INSTANT_VIDEO_UPLOAD_PATH = "instant_video_upload_path";
+    }
 
     @SuppressWarnings("deprecation")
     @Override
@@ -128,7 +140,6 @@ public class Preferences extends PreferenceActivity {
             getWindow().getDecorView().findViewById(actionBarTitleId).
                     setContentDescription(getString(R.string.actionbar_settings));
         }
-        
         // Load package info
         String temp;
         try {
@@ -137,9 +148,9 @@ public class Preferences extends PreferenceActivity {
         } catch (NameNotFoundException e) {
             temp = "";
             Log_OC.e(TAG, "Error while showing about dialog", e);
-        } 
+        }
         final String appVersion = temp;
-       
+
         // Register context menu for list of preferences.
         registerForContextMenu(getListView());
 
@@ -206,7 +217,7 @@ public class Preferences extends PreferenceActivity {
         }
         
         boolean helpEnabled = getResources().getBoolean(R.bool.help_enabled);
-        Preference pHelp =  findPreference("help");
+        Preference pHelp = findPreference("help");
         if (pHelp != null ){
             if (helpEnabled) {
                 pHelp.setOnPreferenceClickListener(new OnPreferenceClickListener() {
@@ -225,7 +236,7 @@ public class Preferences extends PreferenceActivity {
                 preferenceCategory.removePreference(pHelp);
             }
         }
-        
+
        boolean recommendEnabled = getResources().getBoolean(R.bool.recommend_enabled);
        Preference pRecommend =  findPreference("recommend");
         if (pRecommend != null){
@@ -234,11 +245,11 @@ public class Preferences extends PreferenceActivity {
                     @Override
                     public boolean onPreferenceClick(Preference preference) {
 
-                        Intent intent = new Intent(Intent.ACTION_SENDTO); 
+                        Intent intent = new Intent(Intent.ACTION_SENDTO);
                         intent.setType("text/plain");
-                        intent.setData(Uri.parse(getString(R.string.mail_recommend))); 
-                        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 
-                        
+                        intent.setData(Uri.parse(getString(R.string.mail_recommend)));
+                        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
                         String appName = getString(R.string.app_name);
                         String downloadUrl = getString(R.string.url_app_download);
 
@@ -247,12 +258,12 @@ public class Preferences extends PreferenceActivity {
                                 appName);
                         String recommendText = String.format(getString(R.string.recommend_text),
                                 appName, downloadUrl);
-                        
+
                         intent.putExtra(Intent.EXTRA_SUBJECT, recommendSubject);
                         intent.putExtra(Intent.EXTRA_TEXT, recommendText);
                         startActivity(intent);
 
-                        return(true);
+                        return true;
 
                     }
                 });
@@ -260,7 +271,7 @@ public class Preferences extends PreferenceActivity {
                 preferenceCategory.removePreference(pRecommend);
             }
         }
-        
+
         boolean feedbackEnabled = getResources().getBoolean(R.bool.feedback_enabled);
         Preference pFeedback =  findPreference("feedback");
         if (pFeedback != null){
@@ -274,11 +285,11 @@ public class Preferences extends PreferenceActivity {
                         Intent intent = new Intent(Intent.ACTION_SENDTO); 
                         intent.setType("text/plain");
                         intent.putExtra(Intent.EXTRA_SUBJECT, feedback);
-                        
-                        intent.setData(Uri.parse(feedbackMail)); 
-                        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 
+
+                        intent.setData(Uri.parse(feedbackMail));
+                        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                         startActivity(intent);
-                        
+
                         return true;
                     }
                 });
@@ -327,7 +338,38 @@ public class Preferences extends PreferenceActivity {
             }
         }
 
-        mPrefInstantUploadPath =  findPreference("instant_upload_path");
+        mPrefStoragePath =  (ListPreference) findPreference(Keys.STORAGE_PATH);
+        if (mPrefStoragePath != null) {
+            StoragePoint[] storageOptions = DataStorageProvider.getInstance().getAvailableStoragePoints();
+            String[] entries = new String[storageOptions.length];
+            String[] values = new String[storageOptions.length];
+            for (int i = 0; i < storageOptions.length; ++i) {
+                entries[i] = storageOptions[i].getDescription();
+                values[i] = storageOptions[i].getPath();
+            }
+            mPrefStoragePath.setEntries(entries);
+            mPrefStoragePath.setEntryValues(values);
+
+            mPrefStoragePath.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
+                    @Override
+                    public boolean onPreferenceChange(Preference preference, Object newValue) {
+                        String newPath = (String)newValue;
+                        if (mStoragePath.equals(newPath))
+                            return true;
+
+                        StorageMigration storageMigration = new StorageMigration(Preferences.this, mStoragePath, newPath);
+
+                        storageMigration.setStorageMigrationProgressListener(Preferences.this);
+
+                        storageMigration.migrate();
+
+                        return false;
+                    }
+                });
+
+        }
+
+        mPrefInstantUploadPath = (PreferenceWithLongSummary)findPreference(Keys.INSTANT_UPLOAD_PATH);
         if (mPrefInstantUploadPath != null){
 
             mPrefInstantUploadPath.setOnPreferenceClickListener(new OnPreferenceClickListener() {
@@ -343,7 +385,7 @@ public class Preferences extends PreferenceActivity {
                     }
                 });
         }
-        
+
         mPrefInstantUploadCategory =
                 (PreferenceCategory) findPreference("instant_uploading_category");
 
@@ -351,11 +393,11 @@ public class Preferences extends PreferenceActivity {
         mPrefInstantUploadPathWiFi =  findPreference("instant_upload_on_wifi");
         mPrefInstantPictureUploadOnlyOnCharging = findPreference("instant_upload_on_charging");
         mPrefInstantUpload = findPreference("instant_uploading");
-        
+
         toggleInstantPictureOptions(((CheckBoxPreference) mPrefInstantUpload).isChecked());
-        
+
         mPrefInstantUpload.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
-            
+
             @Override
             public boolean onPreferenceChange(Preference preference, Object newValue) {
                 toggleInstantPictureOptions((Boolean) newValue);
@@ -365,8 +407,8 @@ public class Preferences extends PreferenceActivity {
                 return true;
             }
         });
-       
-        mPrefInstantVideoUploadPath =  findPreference("instant_video_upload_path");
+
+        mPrefInstantVideoUploadPath =  findPreference(Keys.INSTANT_VIDEO_UPLOAD_PATH);
         if (mPrefInstantVideoUploadPath != null){
 
             mPrefInstantVideoUploadPath.setOnPreferenceClickListener(new OnPreferenceClickListener() {
@@ -389,7 +431,7 @@ public class Preferences extends PreferenceActivity {
         mPrefInstantVideoUpload = findPreference("instant_video_uploading");
         mPrefInstantVideoUploadOnlyOnCharging = findPreference("instant_video_upload_on_charging");
         toggleInstantVideoOptions(((CheckBoxPreference) mPrefInstantVideoUpload).isChecked());
-        
+
         mPrefInstantVideoUpload.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
 
             @Override
@@ -408,7 +450,7 @@ public class Preferences extends PreferenceActivity {
                 ((CheckBoxPreference)mPrefInstantUpload).isChecked());
 
         /* About App */
-       pAboutApp = (Preference) findPreference("about_app");
+       pAboutApp = findPreference("about_app");
        if (pAboutApp != null) { 
                pAboutApp.setTitle(String.format(getString(R.string.about_android),
                        getString(R.string.app_name)));
@@ -416,6 +458,7 @@ public class Preferences extends PreferenceActivity {
        }
 
        loadInstantUploadPath();
+       loadStoragePath();
        loadInstantUploadVideoPath();
     }
 
@@ -488,7 +531,7 @@ public class Preferences extends PreferenceActivity {
             mPrefInstantUploadCategory.removePreference(mPrefInstantPictureUploadOnlyOnCharging);
         }
     }
-    
+
     private void toggleInstantVideoOptions(Boolean value){
         if (value){
             mPrefInstantUploadCategory.addPreference(mPrefInstantVideoUploadPathWiFi);
@@ -550,8 +593,7 @@ public class Preferences extends PreferenceActivity {
 
         if (requestCode == ACTION_SELECT_UPLOAD_PATH && resultCode == RESULT_OK){
 
-            OCFile folderToUpload =
-                    (OCFile) data.getParcelableExtra(UploadPathActivity.EXTRA_FOLDER);
+            OCFile folderToUpload =  data.getParcelableExtra(UploadPathActivity.EXTRA_FOLDER);
 
             mUploadPath = folderToUpload.getRemotePath();
 
@@ -562,10 +604,9 @@ public class Preferences extends PreferenceActivity {
 
             saveInstantUploadPathOnPreferences();
 
-        } else if (requestCode == ACTION_SELECT_UPLOAD_VIDEO_PATH && resultCode == RESULT_OK){
+        } else if (requestCode == ACTION_SELECT_UPLOAD_VIDEO_PATH && resultCode == RESULT_OK) {
 
-            OCFile folderToUploadVideo =
-                    (OCFile) data.getParcelableExtra(UploadPathActivity.EXTRA_FOLDER);
+            OCFile folderToUploadVideo = data.getParcelableExtra(UploadPathActivity.EXTRA_FOLDER);
 
             mUploadVideoPath = folderToUploadVideo.getRemotePath();
 
@@ -599,8 +640,7 @@ public class Preferences extends PreferenceActivity {
                 Toast.makeText(this, R.string.pass_code_removed, Toast.LENGTH_LONG).show();
             }
         } 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();        }
     }
 
     public ActionBar getSupportActionBar() {
@@ -687,10 +727,38 @@ public class Preferences extends PreferenceActivity {
     private void loadInstantUploadPath() {
         SharedPreferences appPrefs =
                 PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
-        mUploadPath = appPrefs.getString("instant_upload_path", getString(R.string.instant_upload_path));
+        mUploadPath = appPrefs.getString(Keys.INSTANT_UPLOAD_PATH, getString(R.string.instant_upload_path));
         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(Keys.STORAGE_PATH, mStoragePath);
+        editor.commit();
+        String storageDescription = DataStorageProvider.getInstance().getStorageDescriptionByPath(mStoragePath);
+        mPrefStoragePath.setSummary(storageDescription);
+        mPrefStoragePath.setValue(newStoragePath);
+    }
+
+    /**
+     * Load storage path set on preferences
+     */
+    private void loadStoragePath() {
+        SharedPreferences appPrefs =
+                PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
+        mStoragePath = appPrefs.getString(Keys.STORAGE_PATH, Environment.getExternalStorageDirectory()
+                                                         .getAbsolutePath());
+        String storageDescription = DataStorageProvider.getInstance().getStorageDescriptionByPath(mStoragePath);
+        mPrefStoragePath.setSummary(storageDescription);
+    }
+
     /**
      * Save the "Instant Upload Path" on preferences
      */
@@ -698,7 +766,7 @@ public class Preferences extends PreferenceActivity {
         SharedPreferences appPrefs =
                 PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
         SharedPreferences.Editor editor = appPrefs.edit();
-        editor.putString("instant_upload_path", mUploadPath);
+        editor.putString(Keys.INSTANT_UPLOAD_PATH, mUploadPath);
         editor.commit();
     }
 
@@ -719,7 +787,19 @@ public class Preferences extends PreferenceActivity {
         SharedPreferences appPrefs =
                 PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
         SharedPreferences.Editor editor = appPrefs.edit();
-        editor.putString("instant_video_upload_path", mUploadVideoPath);
+        editor.putString(Keys.INSTANT_VIDEO_UPLOAD_PATH, mUploadVideoPath);
         editor.commit();
     }
+
+    @Override
+    public void onStorageMigrationFinished(String storagePath, boolean succeed) {
+        if (succeed)
+            saveStoragePath(storagePath);
+    }
+
+    @Override
+    public void onCancelMigration() {
+        // Migration was canceled so we don't do anything
+    }
+
 }

+ 424 - 0
src/com/owncloud/android/ui/activity/StorageMigration.java

@@ -0,0 +1,424 @@
+/**
+ *   Nextcloud Android client application
+ *
+ *   @author Bartosz Przybylski
+ *   Copyright (C) 2016 Bartosz Przybylski
+ *   Copyright (C) 2016 Nextcloud
+ *
+ *   This program is free software; you can redistribute it and/or
+ *   modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ *   License as published by the Free Software Foundation; either
+ *   version 3 of the License, or any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ *   You should have received a copy of the GNU Affero General Public
+ *   License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.owncloud.android.ui.activity;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.app.ProgressDialog;
+import android.app.AlertDialog;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.os.AsyncTask;
+import android.view.View;
+
+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 com.owncloud.android.utils.FileStorageUtils;
+
+import java.io.File;
+
+/**
+ * @author Bartosz Przybylski
+ */
+public class StorageMigration {
+    private static final String TAG = StorageMigration.class.getName();
+
+    public interface StorageMigrationProgressListener {
+        void onStorageMigrationFinished(String storagePath, boolean succeed);
+        void onCancelMigration();
+    }
+
+    private ProgressDialog mProgressDialog;
+    private Context mContext;
+    private String mSourceStoragePath;
+    private String mTargetStoragePath;
+
+    private StorageMigrationProgressListener mListener;
+
+    public StorageMigration(Context context, String sourcePath, String targetPath) {
+        mContext = context;
+        mSourceStoragePath = sourcePath;
+        mTargetStoragePath = targetPath;
+    }
+
+    public void setStorageMigrationProgressListener(StorageMigrationProgressListener listener) {
+        mListener = listener;
+    }
+
+    public void migrate() {
+        if (storageFolderAlreadyExists())
+            askToOverride();
+        else {
+            ProgressDialog progressDialog = createMigrationProgressDialog();
+            progressDialog.show();
+            new FileMigrationTask(
+                    mContext,
+                    mSourceStoragePath,
+                    mTargetStoragePath,
+                    progressDialog,
+                    mListener).execute();
+
+            progressDialog.getButton(progressDialog.BUTTON_POSITIVE).setVisibility(View.GONE);
+        }
+    }
+
+    private boolean storageFolderAlreadyExists() {
+        File f = new File(mTargetStoragePath, MainApp.getDataFolder());
+        return f.exists() && f.isDirectory();
+    }
+
+    private void askToOverride() {
+
+        new AlertDialog.Builder(mContext)
+                .setMessage(R.string.file_migration_directory_already_exists)
+                .setCancelable(true)
+                .setOnCancelListener(new DialogInterface.OnCancelListener() {
+                    @Override
+                    public void onCancel(DialogInterface dialogInterface) {
+                        if (mListener != null)
+                            mListener.onCancelMigration();
+                    }
+                })
+                .setNegativeButton(R.string.common_cancel, new OnClickListener() {
+                    @Override
+                    public void onClick(DialogInterface dialogInterface, int i) {
+                        if (mListener != null)
+                            mListener.onCancelMigration();
+                    }
+                })
+                .setNeutralButton(R.string.file_migration_use_data_folder, new OnClickListener() {
+                    @Override
+                    public void onClick(DialogInterface dialogInterface, int i) {
+                        ProgressDialog progressDialog = createMigrationProgressDialog();
+                        progressDialog.show();
+                        new StoragePathSwitchTask(
+                                mContext,
+                                mSourceStoragePath,
+                                mTargetStoragePath,
+                                progressDialog,
+                                mListener).execute();
+
+                        progressDialog.getButton(ProgressDialog.BUTTON_POSITIVE).setVisibility(View.GONE);
+
+                    }
+                })
+                .setPositiveButton(R.string.file_migration_override_data_folder, new OnClickListener() {
+                    @Override
+                    public void onClick(DialogInterface dialogInterface, int i) {
+                        ProgressDialog progressDialog = createMigrationProgressDialog();
+                        progressDialog.show();
+                        new FileMigrationTask(
+                                mContext,
+                                mSourceStoragePath,
+                                mTargetStoragePath,
+                                progressDialog,
+                                mListener).execute();
+
+                        progressDialog.getButton(ProgressDialog.BUTTON_POSITIVE).setVisibility(View.GONE);
+                    }
+                })
+                .create()
+                .show();
+    }
+
+    private ProgressDialog createMigrationProgressDialog() {
+        ProgressDialog progressDialog = new ProgressDialog(mContext);
+        progressDialog.setCancelable(false);
+        progressDialog.setTitle(R.string.file_migration_dialog_title);
+        progressDialog.setMessage(mContext.getString(R.string.file_migration_preparing));
+        progressDialog.setButton(
+                ProgressDialog.BUTTON_POSITIVE,
+                mContext.getString(R.string.drawer_close),
+                new OnClickListener() {
+                    @Override
+                    public void onClick(DialogInterface dialogInterface, int i) {
+                        dialogInterface.dismiss();
+                    }
+                });
+        return progressDialog;
+    }
+
+    abstract static private class FileMigrationTaskBase extends AsyncTask<Void, Integer, Integer> {
+        protected String mStorageSource;
+        protected String mStorageTarget;
+        protected Context mContext;
+        protected ProgressDialog mProgressDialog;
+        protected StorageMigrationProgressListener mListener;
+
+        protected String mAuthority;
+        protected Account[] mOcAccounts;
+
+        public FileMigrationTaskBase(Context context,
+                                     String source,
+                                     String target,
+                                     ProgressDialog progressDialog,
+                                     StorageMigrationProgressListener listener) {
+            mContext = context;
+            mStorageSource = source;
+            mStorageTarget = target;
+            mProgressDialog = progressDialog;
+            mListener = listener;
+
+            mAuthority = mContext.getString(R.string.authority);
+            mOcAccounts = AccountManager.get(mContext).getAccountsByType(MainApp.getAccountType());
+        }
+
+        @Override
+        protected void onProgressUpdate(Integer... progress) {
+            if (progress.length > 1 && progress[0] != 0) {
+                mProgressDialog.setMessage(mContext.getString(progress[0]));
+            }
+        }
+
+        @Override
+        protected void onPostExecute(Integer code) {
+            if (code != 0) {
+                mProgressDialog.setMessage(mContext.getString(code));
+            } else {
+                mProgressDialog.setMessage(mContext.getString(R.string.file_migration_ok_finished));
+            }
+
+            boolean succeed = code == 0;
+            if (succeed) {
+                mProgressDialog.hide();
+            } else {
+                mProgressDialog.getButton(ProgressDialog.BUTTON_POSITIVE).setVisibility(View.VISIBLE);
+                mProgressDialog.setIndeterminateDrawable(mContext.getResources().getDrawable(R.drawable.image_fail));
+            }
+
+            if (mListener != null) {
+                mListener.onStorageMigrationFinished(succeed ? mStorageTarget : mStorageSource, succeed);
+            }
+        }
+
+        protected boolean[] saveAccountsSyncStatus() {
+            boolean[] syncs = new boolean[mOcAccounts.length];
+            for (int i = 0; i < mOcAccounts.length; ++i) {
+                syncs[i] = ContentResolver.getSyncAutomatically(mOcAccounts[i], mAuthority);
+            }
+            return syncs;
+        }
+
+        protected void stopAccountsSyncing() {
+            for (int i = 0; i < mOcAccounts.length; ++i) {
+                ContentResolver.setSyncAutomatically(mOcAccounts[i], mAuthority, false);
+            }
+        }
+
+        protected void waitForUnfinishedSynchronizations() {
+            for (int i = 0; i < mOcAccounts.length; ++i) {
+                while (ContentResolver.isSyncActive(mOcAccounts[i], mAuthority)) {
+                    try {
+                        Thread.sleep(1000);
+                    } catch (InterruptedException e) {
+                        Log_OC.w(TAG, "Thread interrupted while waiting for account to end syncing");
+                        Thread.currentThread().interrupt();
+                    }
+                }
+            }
+        }
+
+        protected void restoreAccountsSyncStatus(boolean oldSync[]) {
+            for (int i = 0; i < mOcAccounts.length; ++i) {
+                ContentResolver.setSyncAutomatically(mOcAccounts[i], mAuthority, oldSync[i]);
+            }
+        }
+    }
+
+    static private class StoragePathSwitchTask extends FileMigrationTaskBase {
+
+        public StoragePathSwitchTask(Context context,
+                                     String source,
+                                     String target,
+                                     ProgressDialog progressDialog,
+                                     StorageMigrationProgressListener listener) {
+            super(context, source, target, progressDialog, listener);
+        }
+
+        @Override
+        protected Integer doInBackground(Void... voids) {
+            publishProgress(R.string.file_migration_preparing);
+
+            Log_OC.stopLogging();
+            boolean[] syncStates = new boolean[0];
+            try {
+                publishProgress(R.string.file_migration_saving_accounts_configuration);
+                syncStates = saveAccountsSyncStatus();
+
+                publishProgress(R.string.file_migration_waiting_for_unfinished_sync);
+                stopAccountsSyncing();
+                waitForUnfinishedSynchronizations();
+            } finally {
+                publishProgress(R.string.file_migration_restoring_accounts_configuration);
+                restoreAccountsSyncStatus(syncStates);
+            }
+            Log_OC.startLogging(mStorageTarget);
+
+            return 0;
+        }
+    }
+
+    static private class FileMigrationTask extends FileMigrationTaskBase {
+        private class MigrationException extends Exception {
+            private int mResId;
+
+            MigrationException(int resId) {
+                super();
+                this.mResId = resId;
+            }
+
+            int getResId() { return mResId; }
+        }
+
+        public FileMigrationTask(Context context,
+                                 String source,
+                                 String target,
+                                 ProgressDialog progressDialog,
+                                 StorageMigrationProgressListener listener) {
+            super(context, source, target, progressDialog, listener);
+        }
+
+        @Override
+        protected Integer doInBackground(Void... args) {
+            publishProgress(R.string.file_migration_preparing);
+            Log_OC.stopLogging();
+
+            boolean[] syncState = new boolean[0];
+
+            try {
+                File dstFile = new File(mStorageTarget + File.separator + MainApp.getDataFolder());
+                deleteRecursive(dstFile);
+                dstFile.delete();
+
+                File srcFile = new File(mStorageSource + File.separator + MainApp.getDataFolder());
+                srcFile.mkdirs();
+
+                publishProgress(R.string.file_migration_checking_destination);
+
+                checkDestinationAvailability();
+
+                publishProgress(R.string.file_migration_saving_accounts_configuration);
+                syncState = saveAccountsSyncStatus();
+
+                publishProgress(R.string.file_migration_waiting_for_unfinished_sync);
+                stopAccountsSyncing();
+                waitForUnfinishedSynchronizations();
+
+                publishProgress(R.string.file_migration_migrating);
+                copyFiles();
+
+                publishProgress(R.string.file_migration_updating_index);
+                updateIndex(mContext);
+
+                publishProgress(R.string.file_migration_cleaning);
+                cleanup();
+
+            } catch (MigrationException e) {
+                rollback();
+                Log_OC.startLogging(mStorageSource);
+                return e.getResId();
+            } finally {
+                publishProgress(R.string.file_migration_restoring_accounts_configuration);
+                restoreAccountsSyncStatus(syncState);
+            }
+
+            Log_OC.startLogging(mStorageTarget);
+            publishProgress(R.string.file_migration_ok_finished);
+
+            return 0;
+        }
+
+
+        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() < FileStorageUtils.getFolderSize(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);
+        }
+
+        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 (!FileStorageUtils.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) {
+                Log_OC.e(TAG,e.getMessage(),e);
+                throw new MigrationException(R.string.file_migration_failed_while_updating_index);
+            }
+        }
+
+        void cleanup() {
+            File srcFile = new File(mStorageSource + File.separator + MainApp.getDataFolder());
+            if (!deleteRecursive(srcFile))
+                Log_OC.w(TAG, "Migration cleanup step failed");
+            srcFile.delete();
+        }
+
+        boolean deleteRecursive(File f) {
+            boolean res = true;
+            if (f.isDirectory())
+                for (File c : f.listFiles())
+                    res = deleteRecursive(c) && res;
+            return f.delete() && res;
+        }
+
+        void rollback() {
+            File dstFile = new File(mStorageTarget + File.separator + MainApp.getDataFolder());
+            if (dstFile.exists())
+                if (!dstFile.delete())
+                    Log_OC.w(TAG, "Rollback step failed");
+        }
+    }
+}

+ 2 - 2
src/com/owncloud/android/ui/activity/UploadFilesActivity.java

@@ -68,7 +68,7 @@ public class UploadFilesActivity extends FileActivity implements
     private boolean mSelectAll = false;
     private LocalFileListFragment mFileListFragment;
     private Button mCancelBtn;
-    private Button mUploadBtn;
+    protected Button mUploadBtn;
     private Spinner mBehaviourSpinner;
     private Account mAccountOnCreation;
     private DialogFragment mCurrentDialog;
@@ -81,7 +81,7 @@ public class UploadFilesActivity extends FileActivity implements
     public static final int RESULT_OK_AND_DO_NOTHING = 2;
     public static final int RESULT_OK_AND_DELETE = 3;
 
-    private static final String KEY_DIRECTORY_PATH =
+    public static final String KEY_DIRECTORY_PATH =
             UploadFilesActivity.class.getCanonicalName() + ".KEY_DIRECTORY_PATH";
     private static final String KEY_ALL_SELECTED =
             UploadFilesActivity.class.getCanonicalName() + ".KEY_ALL_SELECTED";

+ 60 - 26
src/com/owncloud/android/utils/FileStorageUtils.java

@@ -21,12 +21,10 @@
 package com.owncloud.android.utils;
 
 import android.accounts.Account;
-import android.annotation.SuppressLint;
 import android.content.Context;
 import android.content.SharedPreferences;
 import android.net.Uri;
 import android.os.Environment;
-import android.os.StatFs;
 import android.preference.PreferenceManager;
 import android.webkit.MimeTypeMap;
 
@@ -37,6 +35,11 @@ import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.lib.resources.files.RemoteFile;
 
 import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -79,8 +82,11 @@ public class FileStorageUtils {
      * Get local owncloud storage path for accountName.
      */
     public static final String getSavePath(String accountName) {
-        File sdCard = Environment.getExternalStorageDirectory();
-        return sdCard.getAbsolutePath() + "/" + MainApp.getDataFolder() + "/" + Uri.encode(accountName, "@");
+        return MainApp.getStoragePath()
+                + File.separator
+                + MainApp.getDataFolder()
+                + File.separator
+                + Uri.encode(accountName, "@");
         // URL encoding is an 'easy fix' to overcome that NTFS and FAT32 don't allow ":" in file names,
         // that can be in the accountName since 0.1.190B
     }
@@ -98,32 +104,30 @@ public class FileStorageUtils {
      * Get absolute path to tmp folder inside datafolder in sd-card for given accountName.
      */
     public static final String getTemporalPath(String accountName) {
-        File sdCard = Environment.getExternalStorageDirectory();
-        return sdCard.getAbsolutePath() + "/" + MainApp.getDataFolder() + "/tmp/" + Uri.encode(accountName, "@");
-            // URL encoding is an 'easy fix' to overcome that NTFS and FAT32 don't allow ":" in file names,
-            // that can be in the accountName since 0.1.190B
+        return MainApp.getStoragePath()
+                + File.separator
+                + MainApp.getDataFolder()
+                + File.separator
+                + "tmp"
+                + File.separator
+                + Uri.encode(accountName, "@");
+        // URL encoding is an 'easy fix' to overcome that NTFS and FAT32 don't allow ":" in file names,
+        // that can be in the accountName since 0.1.190B
     }
 
     /**
      * Optimistic number of bytes available on sd-card. accountName is ignored.
+     *
      * @param accountName not used. can thus be null.
      * @return Optimistic number of available bytes (can be less)
      */
-    @SuppressLint("NewApi")
     public static final long getUsableSpace(String accountName) {
-        File savePath = Environment.getExternalStorageDirectory();
-        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.GINGERBREAD) {
-            return savePath.getUsableSpace();
-
-        } else {
-            StatFs stats = new StatFs(savePath.getAbsolutePath());
-            return stats.getAvailableBlocks() * stats.getBlockSize();
-        }
-
+        File savePath = new File(MainApp.getStoragePath());
+        return savePath.getUsableSpace();
     }
     
     public static final String getLogPath()  {
-        return Environment.getExternalStorageDirectory() + File.separator + MainApp.getDataFolder() + File.separator + "log";
+        return MainApp.getStoragePath() + File.separator + MainApp.getDataFolder() + File.separator + "log";
     }
 
     /**
@@ -441,13 +445,11 @@ public class FileStorageUtils {
     public static long getFolderSize(File dir) {
         if (dir.exists()) {
             long result = 0;
-            File[] fileList = dir.listFiles();
-            for(int i = 0; i < fileList.length; i++) {
-                if(fileList[i].isDirectory()) {
-                    result += getFolderSize(fileList[i]);
-                } else {
-                    result += fileList[i].length();
-                }
+            for (File f : dir.listFiles()) {
+                if (f.isDirectory())
+                    result += getFolderSize(f);
+                else
+                    result += f.length();
             }
             return result;
         }
@@ -497,4 +499,36 @@ public class FileStorageUtils {
         }
     }
 
+    public static 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) {
+                Log_OC.e(TAG, "Error closing input stream during copy", e);
+            }
+            if (out != null) try {
+                out.close();
+            } catch (IOException e) {
+                Log_OC.e(TAG, "Error closing output stream during copy", e);
+            }
+        }
+
+        return ret;
+    }
+
 }