diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index f0cc9ff7c33b..22da0d25b27b 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -56,6 +56,9 @@
+
+
+
+
+
diff --git a/res/layout/migration_layout.xml b/res/layout/migration_layout.xml
new file mode 100644
index 000000000000..107acbb9b337
--- /dev/null
+++ b/res/layout/migration_layout.xml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index eb98378fa249..9dd9efc6f369 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -356,6 +356,28 @@
to upload in this folder
The file is no longer available on the server
+ Updating storage path
+ Finish
+ Preparing for migration…
+ Checking destination…
+ Saving accounts configuration…
+ Waiting for unfinished synchronizations…
+ Moving data…
+ Updating index…
+ Cleaning…
+ Restoring accounts configuration…
+ Finished
+ ERROR: Not enough space
+ ERROR: File is not writable
+ ERROR: File is not readable
+ ERROR: Nextcloud directory already exists
+ ERROR: While migrating
+ ERROR: While updating index
+
+ Data folder already exists, what to do?
+ Override
+ Use existing
+
Accounts
Add account
Manage accounts
@@ -422,6 +444,8 @@
kept in original folder
moved to app folder
deleted
+ Storage path
+ Common
Sharing
Share %1$s
@@ -506,4 +530,8 @@
- %d selected
+ Default
+ SD card %1$d
+ Unknown
+
diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml
index 67b089dca4f7..66065bbbc584 100644
--- a/res/xml/preferences.xml
+++ b/res/xml/preferences.xml
@@ -18,6 +18,11 @@
along with this program. If not, see .
-->
+
+
+
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 operations =
+ new ArrayList(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 getFolderContent(long parentId, boolean onlyOnDevice) {
diff --git a/src/com/owncloud/android/datastorage/DataStorageProvider.java b/src/com/owncloud/android/datastorage/DataStorageProvider.java
new file mode 100644
index 000000000000..0b40ad39956c
--- /dev/null
+++ b/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 .
+ */
+
+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 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);
+ }
+}
diff --git a/src/com/owncloud/android/datastorage/StoragePoint.java b/src/com/owncloud/android/datastorage/StoragePoint.java
new file mode 100644
index 000000000000..b3083d7676cb
--- /dev/null
+++ b/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 .
+ */
+
+package com.owncloud.android.datastorage;
+
+/**
+ * @author Bartosz Przybylski
+ */
+public class StoragePoint implements Comparable {
+ 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());
+ }
+}
diff --git a/src/com/owncloud/android/datastorage/UniqueStorageList.java b/src/com/owncloud/android/datastorage/UniqueStorageList.java
new file mode 100644
index 000000000000..731acc7a0f82
--- /dev/null
+++ b/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 .
+ */
+
+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 {
+ @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;
+ }
+}
diff --git a/src/com/owncloud/android/datastorage/providers/AbstractCommandLineStoragePoint.java b/src/com/owncloud/android/datastorage/providers/AbstractCommandLineStoragePoint.java
new file mode 100644
index 000000000000..86052c098a4f
--- /dev/null
+++ b/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 .
+ */
+
+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;
+ }
+
+}
diff --git a/src/com/owncloud/android/datastorage/providers/AbstractStoragePointProvider.java b/src/com/owncloud/android/datastorage/providers/AbstractStoragePointProvider.java
new file mode 100644
index 000000000000..8dfa93e51d6e
--- /dev/null
+++ b/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 .
+ */
+
+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 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();
+ }
+}
diff --git a/src/com/owncloud/android/datastorage/providers/EnvironmentStoragePointProvider.java b/src/com/owncloud/android/datastorage/providers/EnvironmentStoragePointProvider.java
new file mode 100644
index 000000000000..9ca5470f1191
--- /dev/null
+++ b/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 .
+ */
+
+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 getAvailableStoragePoint() {
+ Vector result = new Vector<>();
+
+ addEntriesFromEnv(result, sSecondaryStorageEnvName);
+
+ return result;
+ }
+
+ private void addEntriesFromEnv(Vector 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));
+ }
+}
diff --git a/src/com/owncloud/android/datastorage/providers/HardcodedStoragePointProvider.java b/src/com/owncloud/android/datastorage/providers/HardcodedStoragePointProvider.java
new file mode 100644
index 000000000000..d65e8ca88940
--- /dev/null
+++ b/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 .
+ */
+
+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 getAvailableStoragePoint() {
+ Vector result = new Vector<>();
+
+ for (String s : sPaths)
+ if (canBeAddedToAvailableList(result, s))
+ result.add(new StoragePoint(s, s));
+
+ return result;
+ }
+}
diff --git a/src/com/owncloud/android/datastorage/providers/IStoragePointProvider.java b/src/com/owncloud/android/datastorage/providers/IStoragePointProvider.java
new file mode 100644
index 000000000000..e096658b653e
--- /dev/null
+++ b/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 .
+ */
+
+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 getAvailableStoragePoint();
+
+}
diff --git a/src/com/owncloud/android/datastorage/providers/MountCommandStoragePointProvider.java b/src/com/owncloud/android/datastorage/providers/MountCommandStoragePointProvider.java
new file mode 100644
index 000000000000..b5bb71e25c7a
--- /dev/null
+++ b/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 .
+ */
+
+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 getAvailableStoragePoint() {
+ Vector result = new Vector<>();
+
+ for (String p : getPotentialPaths(getCommandLineResult()))
+ if (canBeAddedToAvailableList(result, p))
+ result.add(new StoragePoint(p, p));
+
+ return result;
+ }
+
+ private Vector getPotentialPaths(String mounted) {
+ final Vector 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;
+ }
+}
diff --git a/src/com/owncloud/android/datastorage/providers/SystemDefaultStoragePointProvider.java b/src/com/owncloud/android/datastorage/providers/SystemDefaultStoragePointProvider.java
new file mode 100644
index 000000000000..2e2c04932db3
--- /dev/null
+++ b/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 .
+ */
+
+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 getAvailableStoragePoint() {
+ Vector 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;
+ }
+}
diff --git a/src/com/owncloud/android/datastorage/providers/VDCStoragePointProvider.java b/src/com/owncloud/android/datastorage/providers/VDCStoragePointProvider.java
new file mode 100644
index 000000000000..270f7af209b5
--- /dev/null
+++ b/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 .
+ */
+
+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 getAvailableStoragePoint() {
+ Vector result = new Vector<>();
+
+ result.addAll(getPaths(getCommandLineResult()));
+
+ return result;
+ }
+
+ @Override
+ protected String[] getCommand() {
+ return sVDCVolListCommand;
+ }
+
+ private Vector getPaths(String vdcResources) {
+ Vector 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;
+ }
+
+}
diff --git a/src/com/owncloud/android/ui/activity/LocalDirectorySelectorActivity.java b/src/com/owncloud/android/ui/activity/LocalDirectorySelectorActivity.java
new file mode 100644
index 000000000000..0e246b07f73a
--- /dev/null
+++ b/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 .
+ */
+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();
+ }
+ }
+}
diff --git a/src/com/owncloud/android/ui/activity/Preferences.java b/src/com/owncloud/android/ui/activity/Preferences.java
index 32ad63a818b5..d05503203d42 100644
--- a/src/com/owncloud/android/ui/activity/Preferences.java
+++ b/src/com/owncloud/android/ui/activity/Preferences.java
@@ -31,7 +31,9 @@
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.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 void onCreate(Bundle savedInstanceState) {
getWindow().getDecorView().findViewById(actionBarTitleId).
setContentDescription(getString(R.string.actionbar_settings));
}
-
// Load package info
String temp;
try {
@@ -137,9 +148,9 @@ public void onCreate(Bundle savedInstanceState) {
} 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 boolean onPreferenceClick(Preference preference) {
}
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 boolean onPreferenceClick(Preference preference) {
preferenceCategory.removePreference(pHelp);
}
}
-
+
boolean recommendEnabled = getResources().getBoolean(R.bool.recommend_enabled);
Preference pRecommend = findPreference("recommend");
if (pRecommend != null){
@@ -234,11 +245,11 @@ public boolean onPreferenceClick(Preference preference) {
@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 boolean onPreferenceClick(Preference preference) {
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 boolean onPreferenceClick(Preference preference) {
preferenceCategory.removePreference(pRecommend);
}
}
-
+
boolean feedbackEnabled = getResources().getBoolean(R.bool.feedback_enabled);
Preference pFeedback = findPreference("feedback");
if (pFeedback != null){
@@ -274,11 +285,11 @@ public boolean onPreferenceClick(Preference preference) {
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 boolean onPreferenceClick(Preference preference) {
}
}
- 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 boolean onPreferenceClick(Preference preference) {
}
});
}
-
+
mPrefInstantUploadCategory =
(PreferenceCategory) findPreference("instant_uploading_category");
@@ -351,11 +393,11 @@ public boolean onPreferenceClick(Preference preference) {
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 boolean onPreferenceChange(Preference preference, Object newValue) {
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 boolean onPreferenceClick(Preference preference) {
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 boolean onPreferenceChange(Preference preference, Object newValue) {
((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 boolean onPreferenceChange(Preference preference, Object newValue) {
}
loadInstantUploadPath();
+ loadStoragePath();
loadInstantUploadVideoPath();
}
@@ -488,7 +531,7 @@ private void toggleInstantPictureOptions(Boolean value){
mPrefInstantUploadCategory.removePreference(mPrefInstantPictureUploadOnlyOnCharging);
}
}
-
+
private void toggleInstantVideoOptions(Boolean value){
if (value){
mPrefInstantUploadCategory.addPreference(mPrefInstantVideoUploadPathWiFi);
@@ -550,8 +593,7 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
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 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
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 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
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 @@ private AppCompatDelegate getDelegate() {
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 @@ private void saveInstantUploadPathOnPreferences() {
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 @@ private void saveInstantUploadVideoPathOnPreferences() {
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
+ }
+
}
diff --git a/src/com/owncloud/android/ui/activity/StorageMigration.java b/src/com/owncloud/android/ui/activity/StorageMigration.java
new file mode 100644
index 000000000000..cf0c4d09be87
--- /dev/null
+++ b/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 .
+ */
+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 {
+ 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");
+ }
+ }
+}
diff --git a/src/com/owncloud/android/ui/activity/UploadFilesActivity.java b/src/com/owncloud/android/ui/activity/UploadFilesActivity.java
index 9cae55a6f9f4..1b87294e3b85 100644
--- a/src/com/owncloud/android/ui/activity/UploadFilesActivity.java
+++ b/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";
diff --git a/src/com/owncloud/android/utils/FileStorageUtils.java b/src/com/owncloud/android/utils/FileStorageUtils.java
index 54e26ce65dc2..704eaba3cbfe 100644
--- a/src/com/owncloud/android/utils/FileStorageUtils.java
+++ b/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.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 static final String removeDataFolderPath(String fullPath) {
* 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 static final String getDefaultSavePathFor(String accountName, OCFile file
* 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 int compare(OCFile o1, OCFile o2) {
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 static void searchForLocalFileInDefaultPath(OCFile file, Account account)
}
}
+ 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;
+ }
+
}