Merge pull request #92 from nextcloud/externalSD2

External SD support
This commit is contained in:
Andy Scherzinger 2016-11-02 21:18:34 +01:00 committed by GitHub
commit 9f8fdaeac0
22 changed files with 1483 additions and 103 deletions

View file

@ -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" />

View file

@ -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>

View file

@ -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>

View file

@ -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"

View file

@ -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";

View file

@ -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;
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);
InputStream in = null;
OutputStream out = null;
try {
in = new FileInputStream(src);
out = new FileOutputStream(target);
byte[] buf = new byte[1024];
int len;
while ((len = in.read(buf)) > 0) {
out.write(buf, 0, len);
}
} catch (IOException ex) {
ret = false;
} finally {
if (in != null) try {
in.close();
} catch (IOException e) {
e.printStackTrace(System.err);
}
if (out != null) try {
out.close();
} catch (IOException e) {
e.printStackTrace(System.err);
} 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) {

View file

@ -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);
}
}

View file

@ -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());
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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();
}
}

View file

@ -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));
}
}

View file

@ -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;
}
}

View file

@ -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();
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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();
}
}
}

View file

@ -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
}
}

View file

@ -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");
}
}
}

View file

@ -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";

View file

@ -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;
}
}