diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index feade6e2..59e00ed7 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -10,25 +10,26 @@ on: jobs: build: - runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: set up JDK 1.8 + - name: set up JDK uses: actions/setup-java@v1 with: java-version: 1.8 - - name: Set up Go 1.14 + - name: Set up Go uses: actions/setup-go@v1 with: go-version: 1.14 id: go - # https://github.com/actions/virtual-environments/issues/578 - - name: Fix missing NDK dependency - run: echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;20.0.5594570" - - name: Build rclone - run: ./gradlew buildNative -p rclone + - name: Configure Android SDK + run: | + BUILD_TOOLS_VERSION="$(grep -E "^io\.github\.x0b\.rcx\.buildToolsVersion=" gradle.properties | cut -d'=' -f2)" + NDK_VERSION="$(grep -E "^io\.github\.x0b\.rcx\.ndkVersion=" gradle.properties | cut -d'=' -f2)" + + yes | sudo "${ANDROID_HOME}/tools/bin/sdkmanager" --licenses + sudo "${ANDROID_HOME}/tools/bin/sdkmanager" "build-tools;${BUILD_TOOLS_VERSION}" "ndk;${NDK_VERSION}" - name: Build app run: ./gradlew assembleOssDebug - name: Upload APK diff --git a/.gitignore b/.gitignore index 12538b7c..5a635f59 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ *.apk *.ap_ +# Built libraries +*.so + # Files for the ART/Dalvik VM *.dex diff --git a/app/build.gradle b/app/build.gradle index d412e7e3..84e111c8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,10 +1,27 @@ apply plugin: 'com.android.application' -if (!getGradle().getStartParameter().getTaskRequests().toString().toLowerCase().contains("oss")) { - apply plugin: 'com.google.gms.google-services' - apply plugin: 'com.google.firebase.crashlytics' + +for (String taskName : getGradle().getStartParameter().getTaskNames()) { + if (taskName.endsWith('RcxDebug') || taskName.endsWith('RcxRelease')) { + apply plugin: 'com.google.gms.google-services' + apply plugin: 'com.google.firebase.crashlytics' + break + } +} + +tasks.whenTaskAdded { task -> + // We defer the build of rclone just before the assemble stage of the + // main app, so that Android Studio displays a convenient clickable + // error message that allows to install the proper NDK version + // directly from the SDK manager. + if (task.name.startsWith('assemble')) { + task.dependsOn(':rclone:buildNative') + } } android { + buildToolsVersion project.properties['io.github.x0b.rcx.buildToolsVersion'] + ndkVersion project.properties['io.github.x0b.rcx.ndkVersion'] + signingConfigs { github_x0b { keyAlias 'github_x0b' @@ -18,6 +35,10 @@ android { versionCode 170 // last digit is reserved for ABI, only ever end on 0! versionName '1.11.4' testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + + def documentsAuthority = applicationId + '.documents' + manifestPlaceholders = [documentsAuthority: documentsAuthority] + buildConfigField "String", "DOCUMENTS_AUTHORITY", "\"${documentsAuthority}\"" } buildTypes { @@ -63,10 +84,11 @@ android { } project.ext.versionCodes = [ - 'armeabi-v7a': 6, - 'arm64-v8a': 7, - 'x86': 8, - 'x86_64': 9] + 'armeabi-v7a': 6, + 'arm64-v8a': 7, + 'x86': 8, + 'x86_64': 9 + ] android.applicationVariants.all { variant -> variant.outputs.each { output -> diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1fe34ab5..b7cc7720 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -63,6 +63,17 @@ android:resource="@xml/file_provider_paths" /> + + + + + + remote Set shortcutSet = new HashSet<>(); List shortcutInfoList = new ArrayList<>(); - RemoteItem.prepareDisplay(context, remotes); for (RemoteItem remoteItem : remotes) { String id = getUniqueIdFromString(remoteItem.getName()); diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/Dialogs/RemoteDestinationDialog.java b/app/src/main/java/ca/pkay/rcloneexplorer/Dialogs/RemoteDestinationDialog.java index 83539fdd..79b156af 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/Dialogs/RemoteDestinationDialog.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/Dialogs/RemoteDestinationDialog.java @@ -452,7 +452,7 @@ protected void onPreExecute() { @Override protected List doInBackground(Void... voids) { List fileItemList; - fileItemList = rclone.getDirectoryContent(remote, directoryObject.getCurrentPath(), startAtRoot); + fileItemList = rclone.ls(remote, directoryObject.getCurrentPath(), startAtRoot); return fileItemList; } diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/Fragments/FileExplorerFragment.java b/app/src/main/java/ca/pkay/rcloneexplorer/Fragments/FileExplorerFragment.java index 5e4de02b..a4c948ff 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/Fragments/FileExplorerFragment.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/Fragments/FileExplorerFragment.java @@ -6,7 +6,6 @@ import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; @@ -86,7 +85,6 @@ import java.io.File; import java.io.IOException; import java.net.ServerSocket; -import java.net.URL; import java.security.SecureRandom; import java.util.ArrayList; import java.util.Collections; @@ -1659,7 +1657,7 @@ protected void onPreExecute() { @Override protected List doInBackground(Void... voids) { List fileItemList; - fileItemList = rclone.getDirectoryContent(remote, directoryObject.getCurrentPath(), startAtRoot); + fileItemList = rclone.ls(remote, directoryObject.getCurrentPath(), startAtRoot); return fileItemList; } diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/Fragments/RemotesFragment.java b/app/src/main/java/ca/pkay/rcloneexplorer/Fragments/RemotesFragment.java index 6e35274b..f2cf02d8 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/Fragments/RemotesFragment.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/Fragments/RemotesFragment.java @@ -7,6 +7,7 @@ import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; +import android.provider.DocumentsContract; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -25,6 +26,7 @@ import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import ca.pkay.rcloneexplorer.AppShortcutsHelper; +import ca.pkay.rcloneexplorer.BuildConfig; import ca.pkay.rcloneexplorer.Dialogs.RemotePropertiesDialog; import ca.pkay.rcloneexplorer.Items.RemoteItem; import ca.pkay.rcloneexplorer.MainActivity; @@ -33,7 +35,6 @@ import ca.pkay.rcloneexplorer.RecyclerViewAdapters.RemotesRecyclerViewAdapter; import ca.pkay.rcloneexplorer.RemoteConfig.RemoteConfig; import com.leinardi.android.speeddial.SpeedDialView; -import java9.util.stream.StreamSupport; import jp.wasabeef.recyclerview.animators.LandingAnimator; import java.util.ArrayList; @@ -298,12 +299,17 @@ private void refreshRemotes() { if (null != recyclerViewAdapter) { recyclerViewAdapter.newData(remotes); } + refreshSAFRoots(); + } + + private void refreshSAFRoots() { + Uri rootsUri = DocumentsContract.buildRootsUri(BuildConfig.DOCUMENTS_AUTHORITY); + context.getContentResolver().notifyChange(rootsUri, null); } private List filterRemotes() { SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); Set hiddenRemotes = sharedPreferences.getStringSet(getString(R.string.shared_preferences_hidden_remotes), new HashSet<>()); - Set renamedRemotes = sharedPreferences.getStringSet(getString(R.string.pref_key_renamed_remotes), new HashSet<>()); remotes = rclone.getRemotes(); if (hiddenRemotes != null && !hiddenRemotes.isEmpty()) { ArrayList toBeHidden = new ArrayList<>(); @@ -315,13 +321,6 @@ private List filterRemotes() { remotes.removeAll(toBeHidden); } Collections.sort(remotes); - for(RemoteItem item : remotes) { - if(renamedRemotes.contains(item.getName())) { - String displayName = sharedPreferences.getString( - getString(R.string.pref_key_renamed_remote_prefix, item.getName()), item.getName()); - item.setDisplayName(displayName); - } - } return remotes; } @@ -470,21 +469,14 @@ private void renameRemote(final RemoteItem remoteItem) { builder = new AlertDialog.Builder(context); } - final SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context); final EditText remoteNameEdit = new EditText(context); String initialText = remoteItem.getDisplayName(); remoteNameEdit.setText(initialText); builder.setView(remoteNameEdit); builder.setNegativeButton(R.string.cancel, null); builder.setPositiveButton(R.string.select, (dialog, which) -> { - String displayName = remoteNameEdit.getText().toString(); - Set renamedRemotes = pref.getStringSet(getString(R.string.pref_key_renamed_remotes), new HashSet<>()); - renamedRemotes.add(remoteItem.getName()); - pref.edit() - .putString(getString(R.string.pref_key_renamed_remote_prefix, remoteItem.getName()), displayName) - .putStringSet(getString(R.string.pref_key_renamed_remotes), renamedRemotes) - .apply(); - remoteItem.setDisplayName(displayName); + String newName = remoteNameEdit.getText().toString(); + rclone.renameRemote(remoteItem.getName(), newName); refreshRemotes(); }); builder.setTitle(R.string.rename_remote); diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/Fragments/ShareFragment.java b/app/src/main/java/ca/pkay/rcloneexplorer/Fragments/ShareFragment.java index daecd9d5..8bdfbdf3 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/Fragments/ShareFragment.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/Fragments/ShareFragment.java @@ -157,7 +157,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c breadcrumbView = ((FragmentActivity)context).findViewById(R.id.breadcrumb_view); breadcrumbView.setOnClickListener(this); breadcrumbView.setVisibility(View.VISIBLE); - breadcrumbView.addCrumb(remote.getName(), "//" + remote.getName()); + breadcrumbView.addCrumb(remote.getDisplayName(), "//" + remote.getName()); final TypedValue accentColorValue = new TypedValue (); context.getTheme().resolveAttribute (R.attr.colorAccent, accentColorValue, true); @@ -513,7 +513,7 @@ protected void onPreExecute() { @Override protected List doInBackground(Void... voids) { List fileItemList; - fileItemList = rclone.getDirectoryContent(remote, directoryObject.getCurrentPath(), startAtRoot); + fileItemList = rclone.ls(remote, directoryObject.getCurrentPath(), startAtRoot); return fileItemList; } diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/Fragments/ShareRemotesFragment.java b/app/src/main/java/ca/pkay/rcloneexplorer/Fragments/ShareRemotesFragment.java index 52840edf..b058c765 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/Fragments/ShareRemotesFragment.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/Fragments/ShareRemotesFragment.java @@ -53,7 +53,6 @@ public void onCreate(@Nullable Bundle savedInstanceState) { ((FragmentActivity) context).setTitle(getString(R.string.remotes_toolbar_title)); Rclone rclone = new Rclone(getContext()); remotes = rclone.getRemotes(); - RemoteItem.prepareDisplay(getContext(), remotes); Collections.sort(remotes); } diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/Items/RemoteItem.java b/app/src/main/java/ca/pkay/rcloneexplorer/Items/RemoteItem.java index f9d6fe75..693d5d63 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/Items/RemoteItem.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/Items/RemoteItem.java @@ -73,8 +73,9 @@ public class RemoteItem implements Comparable, Parcelable { private boolean isDrawerPinned; private String displayName; - public RemoteItem(String name, String type) { + public RemoteItem(String name, String displayName, String type) { this.name = name; + this.displayName = displayName; this.typeReadable = type; this.type = getTypeFromString(type); } @@ -169,6 +170,8 @@ public String getName() { return name; } + public String getDisplayName() { return displayName; } + public int getType() { return type; } @@ -249,27 +252,6 @@ public boolean isRemoteType(int ...remotes) { return isSameType; } - public String getDisplayName() { - return displayName != null ? displayName : name; - } - - public void setDisplayName(String displayName) { - this.displayName = displayName; - } - - public static List prepareDisplay(Context context, List items) { - SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context); - Set renamedRemotes = pref.getStringSet(context.getString(R.string.pref_key_renamed_remotes), new HashSet<>()); - for(RemoteItem item : items) { - if(renamedRemotes.contains(item.name)) { - String displayName = pref.getString( - context.getString(R.string.pref_key_renamed_remote_prefix, item.name), item.name); - item.displayName = displayName; - } - } - return items; - } - private int getTypeFromString(String type) { switch (type) { case SafConstants.SAF_REMOTE_NAME: @@ -407,7 +389,10 @@ public int compareTo(@NonNull RemoteItem remoteItem) { } else if (!this.isPinned && remoteItem.isPinned) { return 1; } - return getDisplayName().toLowerCase().compareTo(remoteItem.getDisplayName().toLowerCase()); + + String self = getDisplayName().toLowerCase(); + String other = remoteItem.getDisplayName().toLowerCase(); + return self.compareTo(other); } @Override diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/MainActivity.java b/app/src/main/java/ca/pkay/rcloneexplorer/MainActivity.java index 76a054d8..895b1305 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/MainActivity.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/MainActivity.java @@ -11,7 +11,6 @@ import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; -import android.os.Environment; import android.os.Handler; import android.os.Looper; import android.os.Message; @@ -176,13 +175,13 @@ protected void onCreate(Bundle savedInstanceState) { AppShortcutsHelper.populateAppShortcuts(this, rclone.getRemotes()); } - startRemotesFragment(); - SharedPreferences.Editor editor = sharedPreferences.edit(); editor.putInt(getString(R.string.pref_key_version_code), currentVersionCode); editor.putString(getString(R.string.pref_key_version_name), currentVersionName); editor.apply(); - } else if (rclone.isConfigEncrypted()) { + } + + if (rclone.isConfigEncrypted()) { askForConfigPassword(); } else if (savedInstanceState != null) { fragment = getSupportFragmentManager().findFragmentByTag(FILE_EXPLORER_FRAGMENT_TAG); @@ -366,15 +365,6 @@ private void pinRemotesToDrawer() { List remoteItems = rclone.getRemotes(); Collections.sort(remoteItems); - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); - Set renamedRemotes = sharedPreferences.getStringSet(getString(R.string.pref_key_renamed_remotes), new HashSet<>()); - for(RemoteItem item : remoteItems) { - if(renamedRemotes.contains(item.getName())) { - String displayName = sharedPreferences.getString( - getString(R.string.pref_key_renamed_remote_prefix, item.getName()), item.getName()); - item.setDisplayName(displayName); - } - } for (RemoteItem remoteItem : remoteItems) { if (remoteItem.isDrawerPinned()) { MenuItem menuItem = subMenu.add(R.id.nav_pinned, availableDrawerPinnedRemoteId, Menu.NONE, remoteItem.getDisplayName()); @@ -735,9 +725,6 @@ protected void onPostExecute(Boolean success) { } private class RefreshLocalAliases extends AsyncTask { - - private String EMULATED = "5d44cd8d-397c-4107-b79b-17f2b6a071e8"; - private LoadingDialog loadingDialog; protected boolean isRequired() { @@ -756,7 +743,7 @@ protected boolean isRequired() { FLog.d(TAG, "Storage volumes not changed, no refresh required"); return false; } else { - FLog.d(TAG, "Storage volumnes changed, refresh required"); + FLog.d(TAG, "Storage volumes changed, refresh required"); externalVolumes = current; persisted = TextUtils.join("|", current); PreferenceManager.getDefaultSharedPreferences(context).edit() @@ -790,14 +777,10 @@ protected Boolean doInBackground(Void... aVoid) { } SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context); Set generated = pref.getStringSet(getString(R.string.pref_key_local_alias_remotes), new HashSet<>()); - Set renamed = pref.getStringSet(getString(R.string.pref_key_renamed_remotes), new HashSet<>()); SharedPreferences.Editor editor = pref.edit(); for(String remote : generated) { rclone.deleteRemote(remote); - renamed.remove(remote); - editor.remove(getString(R.string.pref_key_renamed_remote_prefix, remote)); } - editor.putStringSet(getString(R.string.pref_key_renamed_remotes), renamed); editor.apply(); File[] dirs = context.getExternalFilesDirs(null); for(File file : dirs) { @@ -833,13 +816,13 @@ private File getVolumeRoot(File file) { private void addLocalRemote(File root) throws IOException { SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context); String name = root.getCanonicalPath(); - String id = Environment.isExternalStorageEmulated(root) ? EMULATED : UUID.randomUUID().toString(); + String id = UUID.randomUUID().toString(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { StorageManager storageManager = (StorageManager) getSystemService(Context.STORAGE_SERVICE); StorageVolume storageVolume = storageManager.getStorageVolume(root); - name = storageVolume.getDescription(context); - if (null != storageVolume.getUuid()) { - id = storageVolume.getUuid(); + String description = storageVolume != null ? storageVolume.getDescription(context) : null; + if (description != null) { + name = description; } } @@ -849,7 +832,7 @@ private void addLocalRemote(File root) throws IOException { options.add("alias"); options.add("remote"); options.add(path); - FLog.d(TAG, "Adding local remote [%s] remote = %s", id, path); + FLog.d(TAG, "Adding local remote [%s] remote = %s", name, path); Process process = rclone.configCreate(options); try { process.waitFor(); @@ -861,15 +844,12 @@ private void addLocalRemote(File root) throws IOException { FLog.e(TAG, "addLocalRemote: process error", e); return; } - Set renamedRemotes = pref.getStringSet(getString(R.string.pref_key_renamed_remotes), new HashSet<>()); + rclone.renameRemote(id, name); Set pinnedRemotes = pref.getStringSet(getString(R.string.shared_preferences_drawer_pinned_remotes), new HashSet<>()); Set generatedRemotes = pref.getStringSet(getString(R.string.pref_key_local_alias_remotes), new HashSet<>()); - renamedRemotes.add(id); pinnedRemotes.add(id); generatedRemotes.add(id); pref.edit() - .putStringSet(getString(R.string.pref_key_renamed_remotes), renamedRemotes) - .putString(getString(R.string.pref_key_renamed_remote_prefix, id), name) .putStringSet(getString(R.string.shared_preferences_drawer_pinned_remotes), pinnedRemotes) .putStringSet(getString(R.string.pref_key_local_alias_remotes), generatedRemotes) .apply(); diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/Rclone.java b/app/src/main/java/ca/pkay/rcloneexplorer/Rclone.java index fdd1f507..8dad14f1 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/Rclone.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/Rclone.java @@ -5,6 +5,7 @@ import android.net.Uri; import android.os.Build; import android.os.Environment; +import android.util.Log; import android.webkit.MimeTypeMap; import android.widget.Toast; import androidx.annotation.NonNull; @@ -17,6 +18,7 @@ import io.github.x0b.safdav.SafAccessProvider; import io.github.x0b.safdav.SafDAVServer; import io.github.x0b.safdav.file.SafConstants; + import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -46,6 +48,9 @@ public class Rclone { public static final int SERVE_PROTOCOL_WEBDAV = 2; public static final int SERVE_PROTOCOL_FTP = 3; public static final int SERVE_PROTOCOL_DLNA = 4; + private static final String[] COMMON_TRANSFER_OPTIONS = new String[] { + "--transfers", "1", "--stats=1s", "--stats-log-level", "NOTICE" + }; private static SafDAVServer safDAVServer; private Context context; private String rclone; @@ -178,7 +183,7 @@ public void logErrorOutput(Process process) { } @Nullable - public List getDirectoryContent(RemoteItem remote, String path, boolean startAtRoot) { + public List ls(RemoteItem remote, String path, boolean startAtRoot) { String remoteAndPath = remote.getName() + ":"; if (startAtRoot) { remoteAndPath += "/"; @@ -254,7 +259,7 @@ public List getDirectoryContent(RemoteItem remote, String path, boolea FileItem fileItem = new FileItem(remote, filePath, fileName, fileSize, fileModTime, mimeType, fileIsDir); fileItemList.add(fileItem); } catch (JSONException e) { - e.printStackTrace(); + Log.e(TAG, "Runtime error.", e); return null; } } @@ -267,8 +272,18 @@ public List getRemotes() { Process process; JSONObject remotesJSON; SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); - Set pinnedRemotes = sharedPreferences.getStringSet(context.getString(R.string.shared_preferences_pinned_remotes), new HashSet<>()); - Set favoriteRemotes = sharedPreferences.getStringSet(context.getString(R.string.shared_preferences_drawer_pinned_remotes), new HashSet<>()); + Set pinnedRemotes = sharedPreferences.getStringSet( + context.getString(R.string.shared_preferences_pinned_remotes), + new HashSet<>() + ); + Set favoriteRemotes = sharedPreferences.getStringSet( + context.getString(R.string.shared_preferences_drawer_pinned_remotes), + new HashSet<>() + ); + Set renamedRemotes = sharedPreferences.getStringSet( + context.getString(R.string.pref_key_renamed_remotes), + new HashSet<>() + ); try { process = Runtime.getRuntime().exec(command); @@ -288,7 +303,7 @@ public List getRemotes() { remotesJSON = new JSONObject(output.toString()); } catch (IOException | InterruptedException | JSONException e) { - e.printStackTrace(); + Log.e(TAG, "Runtime error.", e); return new ArrayList<>(); } @@ -310,7 +325,15 @@ public List getRemotes() { } } - RemoteItem newRemote = new RemoteItem(key, type); + String displayName = key; + if (renamedRemotes.contains(key)) { + displayName = sharedPreferences.getString( + context.getString(R.string.pref_key_renamed_remote_prefix, key), + key + ); + } + + RemoteItem newRemote = new RemoteItem(key, displayName, type); if (type.equals("crypt") || type.equals("alias") || type.equals("cache")) { newRemote = getRemoteType(remotesJSON, newRemote, key, 8); if (newRemote == null) { @@ -329,7 +352,7 @@ public List getRemotes() { remoteItemList.add(newRemote); } catch (JSONException e) { - e.printStackTrace(); + Log.e(TAG, "Runtime error.", e); } } @@ -387,7 +410,7 @@ private RemoteItem getRemoteType(JSONObject remotesJSON, RemoteItem remoteItem, remoteItem.setType(type); return remoteItem; } catch (JSONException e) { - e.printStackTrace(); + Log.e(TAG, "Runtime error.", e); } } @@ -403,11 +426,10 @@ public Process configCreate(List options) { System.arraycopy(opt, 0, commandWithOptions, command.length, opt.length); - try { return Runtime.getRuntime().exec(commandWithOptions); } catch (IOException e) { - e.printStackTrace(); + Log.e(TAG, "Runtime error.", e); return null; } } @@ -426,7 +448,7 @@ public void deleteRemote(String remoteName) { process = Runtime.getRuntime().exec(command); process.waitFor(); } catch (IOException | InterruptedException e) { - e.printStackTrace(); + Log.e(TAG, "Runtime error.", e); } } @@ -444,7 +466,7 @@ public String obscure(String pass) { BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); return reader.readLine(); } catch (IOException | InterruptedException e) { - e.printStackTrace(); + Log.e(TAG, "Runtime error.", e); return null; } } @@ -501,7 +523,7 @@ public Process serve(int protocol, int port, boolean allowRemoteAccess, @Nullabl try { return Runtime.getRuntime().exec(command, env); } catch (IOException e) { - e.printStackTrace(); + Log.e(TAG, "Runtime error.", e); return null; } } @@ -516,81 +538,122 @@ public Process sync(RemoteItem remoteItem, String remote, String localPath, int String localRemotePath = (remoteItem.isRemoteType(RemoteItem.LOCAL)) ? getLocalRemotePathPrefix(remoteItem, context) + "/" : ""; String remotePath = (remote.compareTo("//" + remoteName) == 0) ? remoteName + ":" + localRemotePath : remoteName + ":" + localRemotePath + remote; + List opts = new ArrayList<>(Arrays.asList("sync")); if (syncDirection == 1) { - command = createCommandWithOptions("sync", localPath, remotePath, "--transfers", "1", "--stats=1s", "--stats-log-level", "NOTICE"); + opts.addAll(Arrays.asList(localPath, remotePath)); } else if (syncDirection == 2) { - command = createCommandWithOptions("sync", remotePath, localPath, "--transfers", "1", "--stats=1s", "--stats-log-level", "NOTICE"); + opts.addAll(Arrays.asList(remotePath, localPath)); } else { return null; } + opts.addAll(Arrays.asList(COMMON_TRANSFER_OPTIONS)); + command = createCommandWithOptions(opts.toArray(new String[0])); String[] env = getRcloneEnv(); try { return Runtime.getRuntime().exec(command, env); } catch (IOException e) { - e.printStackTrace(); + Log.e(TAG, "Runtime error.", e); return null; } } + private String buildRemoteFilePath(RemoteItem remote, String path) { + String remoteFilePath = remote.getName() + ":"; + if (remote.isRemoteType(RemoteItem.LOCAL) && !remote.isAlias() && !remote.isCrypt() && !remote.isCache()) { + remoteFilePath += getLocalRemotePathPrefix(remote, context) + "/"; + } + remoteFilePath += path; + return remoteFilePath; + } + public Process downloadFile(RemoteItem remote, FileItem downloadItem, String downloadPath) { String[] command; - String remoteFilePath; String localFilePath; - - remoteFilePath = remote.getName() + ":"; - if (remote.isRemoteType(RemoteItem.LOCAL) && (!remote.isAlias() && !remote.isCrypt() && !remote.isCache())) { - remoteFilePath += getLocalRemotePathPrefix(remote, context) + "/"; - } - remoteFilePath += downloadItem.getPath(); + String remoteFilePath = buildRemoteFilePath(remote, downloadItem.getPath()); if (downloadItem.isDir()) { localFilePath = downloadPath + "/" + downloadItem.getName(); } else { localFilePath = downloadPath; } - command = createCommandWithOptions("copy", remoteFilePath, localFilePath, "--transfers", "1", "--stats=1s", "--stats-log-level", "NOTICE"); + + List opts = new ArrayList<>( + Arrays.asList("copy", remoteFilePath, localFilePath) + ); + opts.addAll(Arrays.asList(COMMON_TRANSFER_OPTIONS)); + command = createCommandWithOptions(opts.toArray(new String[0])); String[] env = getRcloneEnv(); try { return Runtime.getRuntime().exec(command, env); } catch (IOException e) { - e.printStackTrace(); + Log.e(TAG, "Runtime error.", e); return null; } } - public Process uploadFile(RemoteItem remote, String uploadPath, String uploadFile) { - String remoteName = remote.getName(); - String path; + public Process catFile(RemoteItem remote, String path) { String[] command; - String localRemotePath; + String remoteFilePath = buildRemoteFilePath(remote, path); - if (remote.isRemoteType(RemoteItem.LOCAL) && (!remote.isAlias() && !remote.isCrypt() && !remote.isCache())) { - localRemotePath = getLocalRemotePathPrefix(remote, context) + "/"; - } else { - localRemotePath = ""; + List opts = new ArrayList<>(Arrays.asList("cat", remoteFilePath)); + opts.addAll(Arrays.asList(COMMON_TRANSFER_OPTIONS)); + command = createCommandWithOptions(opts.toArray(new String[0])); + + String[] env = getRcloneEnv(); + try { + return Runtime.getRuntime().exec(command, env); + } catch (IOException e) { + Log.e(TAG, "Runtime error.", e); + return null; } + } + + public Process uploadFile(RemoteItem remote, String uploadPath, String fileToUpload) { + String remoteName = remote.getName(); + String[] command; + String path = uploadPath.equals("//" + remoteName) + ? buildRemoteFilePath(remote, "") + : buildRemoteFilePath(remote, uploadPath); - File file = new File(uploadFile); + File file = new File(fileToUpload); if (file.isDirectory()) { - int index = uploadFile.lastIndexOf('/'); - String dirName = uploadFile.substring(index + 1); - path = (uploadPath.compareTo("//" + remoteName) == 0) ? remoteName + ":" + localRemotePath + dirName : remoteName + ":" + localRemotePath + uploadPath + "/" + dirName; - } else { - path = (uploadPath.compareTo("//" + remoteName) == 0) ? remoteName + ":" + localRemotePath : remoteName + ":" + localRemotePath + uploadPath; + int index = fileToUpload.lastIndexOf('/'); + String dirName = fileToUpload.substring(index + 1); + path += "/" + dirName; } - command = createCommandWithOptions("copy", uploadFile, path, "--transfers", "1", "--stats=1s", "--stats-log-level", "NOTICE"); + List opts = new ArrayList<>( + Arrays.asList("copy", fileToUpload, path) + ); + opts.addAll(Arrays.asList(COMMON_TRANSFER_OPTIONS)); + command = createCommandWithOptions(opts.toArray(new String[0])); String[] env = getRcloneEnv(); try { return Runtime.getRuntime().exec(command, env); } catch (IOException e) { - e.printStackTrace(); + Log.e(TAG, "Runtime error.", e); return null; } + } + + public Process rCatFile(RemoteItem remote, String uploadPath) { + String[] command; + String remoteFilePath = buildRemoteFilePath(remote, uploadPath); + + List opts = new ArrayList<>(Arrays.asList("rcat", remoteFilePath)); + opts.addAll(Arrays.asList(COMMON_TRANSFER_OPTIONS)); + command = createCommandWithOptions(opts.toArray(new String[0])); + String[] env = getRcloneEnv(); + try { + return Runtime.getRuntime().exec(command, env); + } catch (IOException e) { + Log.e(TAG, "Runtime error.", e); + return null; + } } public Process deleteItems(RemoteItem remote, FileItem deleteItem) { @@ -616,7 +679,7 @@ public Process deleteItems(RemoteItem remote, FileItem deleteItem) { try { process = Runtime.getRuntime().exec(command, env); } catch (IOException e) { - e.printStackTrace(); + Log.e(TAG, "Runtime error.", e); } return process; } @@ -641,7 +704,7 @@ public Boolean makeDirectory(RemoteItem remote, String path) { return false; } } catch (IOException | InterruptedException e) { - e.printStackTrace(); + Log.e(TAG, "Runtime error.", e); return false; } return true; @@ -668,7 +731,7 @@ public Process moveTo(RemoteItem remote, FileItem moveItem, String newLocation) try { process = Runtime.getRuntime().exec(command, env); } catch (IOException e) { - e.printStackTrace(); + Log.e(TAG, "Runtime error.", e); } return process; @@ -696,7 +759,7 @@ public Boolean moveTo(RemoteItem remote, String oldFile, String newFile) { return false; } } catch (IOException | InterruptedException e) { - e.printStackTrace(); + Log.e(TAG, "Runtime error.", e); return false; } return true; @@ -710,7 +773,7 @@ public boolean emptyTrashCan(String remote) { process = Runtime.getRuntime().exec(command, env); process.waitFor(); } catch (IOException | InterruptedException e) { - e.printStackTrace(); + Log.e(TAG, "Runtime error.", e); } return process != null && process.exitValue() == 0; @@ -738,7 +801,7 @@ public String link(RemoteItem remote, String filePath) { return reader.readLine(); } catch (IOException | InterruptedException e) { - e.printStackTrace(); + Log.e(TAG, "Runtime error.", e); if (process != null) { logErrorOutput(process); } @@ -775,7 +838,7 @@ public String calculateMD5(RemoteItem remote, FileItem fileItem) { return split[0]; } } catch (IOException | InterruptedException e) { - e.printStackTrace(); + Log.e(TAG, "Runtime error.", e); return context.getString(R.string.hash_error); } } @@ -809,7 +872,7 @@ public String calculateSHA1(RemoteItem remote, FileItem fileItem) { return split[0]; } } catch (IOException | InterruptedException e) { - e.printStackTrace(); + Log.e(TAG, "Runtime error.", e); return context.getString(R.string.hash_error); } } @@ -831,7 +894,7 @@ public String getRcloneVersion() { result.add(line); } } catch (IOException | InterruptedException e) { - e.printStackTrace(); + Log.e(TAG, "Runtime error.", e); return "-1"; } @@ -946,7 +1009,7 @@ public Boolean isConfigEncrypted() { process = Runtime.getRuntime().exec(command); process.waitFor(); } catch (IOException | InterruptedException e) { - e.printStackTrace(); + Log.e(TAG, "Runtime error.", e); return false; } return process.exitValue() != 0; @@ -960,7 +1023,7 @@ public Boolean decryptConfig(String password) { try { process = Runtime.getRuntime().exec(command, environmentalVars); } catch (IOException e) { - e.printStackTrace(); + Log.e(TAG, "Runtime error.", e); return false; } @@ -972,14 +1035,14 @@ public Boolean decryptConfig(String password) { result.add(line); } } catch (IOException e) { - e.printStackTrace(); + Log.e(TAG, "Runtime error.", e); return false; } try { process.waitFor(); } catch (InterruptedException e) { - e.printStackTrace(); + Log.e(TAG, "Runtime error.", e); return false; } @@ -1003,7 +1066,7 @@ public Boolean decryptConfig(String password) { fileOutputStream.flush(); fileOutputStream.close(); } catch (IOException e) { - e.printStackTrace(); + Log.e(TAG, "Runtime error.", e); return false; } return true; @@ -1103,6 +1166,25 @@ public void exportConfigFile(Uri uri) throws IOException { outputStream.close(); } + public void renameRemote(String remoteName, String displayName) { + final SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context); + final Set renamedRemotes = pref.getStringSet( + context.getString(R.string.pref_key_renamed_remotes), + new HashSet<>() + ); + renamedRemotes.add(remoteName); + pref.edit() + .putString( + context.getString(R.string.pref_key_renamed_remote_prefix, remoteName), + displayName + ) + .putStringSet( + context.getString(R.string.pref_key_renamed_remotes), + renamedRemotes + ) + .apply(); + } + /** * Prefixes local remotes with a base path on the primary external storage. * @param item diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/RemoteConfig/AliasConfig.java b/app/src/main/java/ca/pkay/rcloneexplorer/RemoteConfig/AliasConfig.java index 38e9d46e..d471cad8 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/RemoteConfig/AliasConfig.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/RemoteConfig/AliasConfig.java @@ -131,7 +131,6 @@ private void setRemote() { return; } - RemoteItem.prepareDisplay(context, remotes); Collections.sort(remotes, (a, b) -> a.getDisplayName().compareTo(b.getDisplayName())); String[] options = new String[remotes.size()]; int i = 0; diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/RemoteConfig/CacheConfig.java b/app/src/main/java/ca/pkay/rcloneexplorer/RemoteConfig/CacheConfig.java index 8ea2c7e8..8e95b04d 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/RemoteConfig/CacheConfig.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/RemoteConfig/CacheConfig.java @@ -286,7 +286,6 @@ private void setRemote() { Toasty.info(context, getString(R.string.no_remotes), Toast.LENGTH_SHORT, true).show(); return; } - RemoteItem.prepareDisplay(context, remotes); Collections.sort(remotes, (a, b) -> a.getDisplayName().compareTo(b.getDisplayName())); String[] options = new String[remotes.size()]; int i = 0; diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/RemoteConfig/CryptConfig.java b/app/src/main/java/ca/pkay/rcloneexplorer/RemoteConfig/CryptConfig.java index 8457938a..bbc6378f 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/RemoteConfig/CryptConfig.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/RemoteConfig/CryptConfig.java @@ -289,7 +289,6 @@ private void setRemote() { return; } - RemoteItem.prepareDisplay(context, remotes); Collections.sort(remotes, (a, b) -> a.getDisplayName().compareTo(b.getDisplayName())); String[] options = new String[remotes.size()]; int i = 0; diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/RemoteConfig/DriveConfig.java b/app/src/main/java/ca/pkay/rcloneexplorer/RemoteConfig/DriveConfig.java index 1ab42c77..5a5dc8fd 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/RemoteConfig/DriveConfig.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/RemoteConfig/DriveConfig.java @@ -2,10 +2,8 @@ import android.annotation.SuppressLint; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; -import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.view.LayoutInflater; @@ -18,7 +16,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; -import androidx.browser.customtabs.CustomTabsIntent; import androidx.fragment.app.Fragment; import androidx.preference.PreferenceManager; import ca.pkay.rcloneexplorer.MainActivity; diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/RemoteConfig/UnionConfig.java b/app/src/main/java/ca/pkay/rcloneexplorer/RemoteConfig/UnionConfig.java index fc7ef519..f04e25d2 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/RemoteConfig/UnionConfig.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/RemoteConfig/UnionConfig.java @@ -122,7 +122,6 @@ private void addRemote() { return; } - RemoteItem.prepareDisplay(context, configuredRemotes); Collections.sort(configuredRemotes, (a, b) -> a.getDisplayName().compareTo(b.getDisplayName())); String[] options = new String[configuredRemotes.size()]; int i = 0; diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/SAFProvider/BufferedTransferThread.java b/app/src/main/java/ca/pkay/rcloneexplorer/SAFProvider/BufferedTransferThread.java new file mode 100644 index 00000000..4e1dd37d --- /dev/null +++ b/app/src/main/java/ca/pkay/rcloneexplorer/SAFProvider/BufferedTransferThread.java @@ -0,0 +1,66 @@ +package ca.pkay.rcloneexplorer.SAFProvider; + +import android.os.CancellationSignal; +import android.util.Log; + +import androidx.annotation.Nullable; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +class BufferedTransferThread extends Thread { + private static String TAG = "BufferedTransferThread"; + + private InputStream is; + private OutputStream os; + private int bufferSize; + private @Nullable CancellationSignal cancellationSignal; + + public BufferedTransferThread( + InputStream is, + OutputStream os, + @Nullable CancellationSignal cancellationSignal + ) { + this(is, os, 65536, cancellationSignal); + } + + public BufferedTransferThread( + InputStream is, + OutputStream os, + int bufferSize, + @Nullable CancellationSignal cancellationSignal + ) { + this.is = is; + this.os = os; + this.bufferSize = bufferSize; + this.cancellationSignal = cancellationSignal; + } + + private boolean isCanceled() { + return cancellationSignal != null && cancellationSignal.isCanceled(); + } + + @Override + public void run() { + byte[] buf = new byte[this.bufferSize]; + int len; + + try { + while (!isCanceled() && (len = is.read(buf)) != -1) { + os.write(buf, 0, len); + os.flush(); + } + } catch (IOException e) { + Log.i(TAG, "Couldn't write file."); + } finally { + try { + is.close(); + os.close(); + } catch (IOException e) { + Log.i(TAG, "Couldn't close file."); + } + } + + } +} diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/SAFProvider/RcxUri.java b/app/src/main/java/ca/pkay/rcloneexplorer/SAFProvider/RcxUri.java new file mode 100644 index 00000000..02a55f98 --- /dev/null +++ b/app/src/main/java/ca/pkay/rcloneexplorer/SAFProvider/RcxUri.java @@ -0,0 +1,111 @@ +package ca.pkay.rcloneexplorer.SAFProvider; + +import android.net.Uri; + +import org.jetbrains.annotations.NotNull; + +import java.io.FileNotFoundException; +import java.util.List; + +import ca.pkay.rcloneexplorer.Items.FileItem; +import ca.pkay.rcloneexplorer.Items.RemoteItem; +import ca.pkay.rcloneexplorer.Rclone; + +class RcxUri { + private static final String RCX_SCHEME = "rcx://"; + + private String uri; + private Uri parsedUri; + private List pathSegments; + + public RcxUri(String uri) { + this.uri = uri; + this.parsedUri = Uri.parse(uri); + this.pathSegments = parsedUri.getPathSegments(); + } + + public RcxUri(Uri uri) { + this(uri.toString()); + } + + @NotNull + @Override + public String toString() { + return uri; + } + + public static RcxUri fromRemoteName(String remoteName) { + return new RcxUri(RCX_SCHEME + Uri.encode(remoteName)); + } + + public String getPathForRClone() { + StringBuilder sb = new StringBuilder(); + for (final String s : pathSegments) { + sb.append("/"); + sb.append(Uri.decode(s)); + } + return sb.toString(); + } + + public String getRemoteName() { + return Uri.decode(parsedUri.getHost()); + } + + public RemoteItem getRemoteItem(Rclone rclone) { + final String remoteName = this.getRemoteName(); + for (final RemoteItem remote : rclone.getRemotes()) { + if (remote.getName().equals(remoteName)) { + return remote; + } + } + return null; + } + + public RcxUri getParentRcxUri() { + if (pathSegments.size() == 0) { + return null; + } + StringBuilder sb = new StringBuilder( + parsedUri.getScheme() + "://" + Uri.encode(parsedUri.getHost()) + ); + for (String segment : pathSegments.subList(0, pathSegments.size() - 1)) { + sb.append("/" + segment); + } + return new RcxUri(sb.toString()); + } + + public RcxUri getChildRcxUri(String unencodedFilename) { + return new RcxUri( + Uri.withAppendedPath(parsedUri, Uri.encode(unencodedFilename)) + ); + } + + public String getFileName() { + return pathSegments.get(pathSegments.size() - 1); + } + + public FileItem getFileItem(Rclone rclone) throws FileNotFoundException { + // Unfortunately, rclone has no equivalent for ls' "--directory" option + // that lists directories and not their content. + // As a workaround, we query the parent directory of the requested document + // and find the corresponding item within it. + RcxUri parentRcxUri = getParentRcxUri(); + + final List directoryContent = rclone.ls( + getRemoteItem(rclone), + parentRcxUri.getPathForRClone(), + false + ); + if (directoryContent == null) { + throw new FileNotFoundException("Couldn't query document document."); + } + + final String fileName = getFileName(); + for (final FileItem fileItem : directoryContent) { + if (fileItem.getName().equals(fileName)) { + return fileItem; + } + } + throw new FileNotFoundException("Couldn't find document in remote document."); + } +} diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/SAFProvider/SAFProvider.java b/app/src/main/java/ca/pkay/rcloneexplorer/SAFProvider/SAFProvider.java new file mode 100644 index 00000000..4a9517e0 --- /dev/null +++ b/app/src/main/java/ca/pkay/rcloneexplorer/SAFProvider/SAFProvider.java @@ -0,0 +1,317 @@ +package ca.pkay.rcloneexplorer.SAFProvider; + +import android.content.Context; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.os.Bundle; +import android.os.CancellationSignal; +import android.os.ParcelFileDescriptor; +import android.provider.DocumentsContract; +import android.provider.DocumentsProvider; +import android.util.Log; + +import androidx.annotation.Nullable; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; + +import ca.pkay.rcloneexplorer.Items.FileItem; +import ca.pkay.rcloneexplorer.Items.RemoteItem; +import ca.pkay.rcloneexplorer.Rclone; + +public final class SAFProvider extends DocumentsProvider { + private static final String TAG = "SAFProvider"; + + private static final String[] DEFAULT_ROOT_PROJECTION = new String[]{ + DocumentsContract.Root.COLUMN_ROOT_ID, + DocumentsContract.Root.COLUMN_MIME_TYPES, + DocumentsContract.Root.COLUMN_FLAGS, + DocumentsContract.Root.COLUMN_ICON, + DocumentsContract.Root.COLUMN_TITLE, + DocumentsContract.Root.COLUMN_SUMMARY, + DocumentsContract.Root.COLUMN_DOCUMENT_ID, + DocumentsContract.Root.COLUMN_AVAILABLE_BYTES, + }; + private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[]{ + DocumentsContract.Document.COLUMN_DOCUMENT_ID, + DocumentsContract.Document.COLUMN_MIME_TYPE, + DocumentsContract.Document.COLUMN_DISPLAY_NAME, + DocumentsContract.Document.COLUMN_LAST_MODIFIED, + DocumentsContract.Document.COLUMN_FLAGS, + DocumentsContract.Document.COLUMN_SIZE, + }; + private static final int SUPPORTED_DOCUMENT_FLAGS = + DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE + | DocumentsContract.Document.FLAG_SUPPORTS_DELETE + | DocumentsContract.Document.FLAG_SUPPORTS_MOVE + | DocumentsContract.Document.FLAG_SUPPORTS_RENAME + | DocumentsContract.Document.FLAG_SUPPORTS_WRITE; + + + private Rclone rclone; + private Context context; + + @Override + public boolean onCreate() { + context = this.getContext(); + if (context == null) { + return false; + } + rclone = new Rclone(context); + return true; + } + + @Override + public Cursor queryRoots(String[] projection) { + if (projection == null) { + projection = DEFAULT_ROOT_PROJECTION; + } + + List remotes = rclone.getRemotes(); + + final MatrixCursor result = new MatrixCursor(projection, remotes.size()); + + if (remotes.size() == 0) { + return result; + } + + for (RemoteItem remote : remotes) { + if (remote.isRemoteType(RemoteItem.LOCAL)) { + continue; + } + + RcxUri rcxUri = RcxUri.fromRemoteName(remote.getName()); + Log.d(TAG, "Adding root " + rcxUri.toString() + "."); + + final MatrixCursor.RowBuilder row = result.newRow(); + row.add(DocumentsContract.Root.COLUMN_ROOT_ID, rcxUri); + row.add(DocumentsContract.Root.COLUMN_SUMMARY, remote.getDisplayName()); + row.add( + DocumentsContract.Root.COLUMN_FLAGS, + DocumentsContract.Root.FLAG_SUPPORTS_CREATE + ); + row.add(DocumentsContract.Root.COLUMN_TITLE, "RClone"); + row.add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, rcxUri); + row.add(DocumentsContract.Root.COLUMN_MIME_TYPES, null); + row.add(DocumentsContract.Root.COLUMN_AVAILABLE_BYTES, null); + row.add(DocumentsContract.Root.COLUMN_ICON, remote.getRemoteIcon()); + } + + return result; + } + + private void includeFileItem(MatrixCursor result, FileItem file, RcxUri parentRcxUri) { + final String fileName = file.getName(); + final RcxUri rcxUri = parentRcxUri.getChildRcxUri(fileName); + Log.d(TAG, "Adding document " + rcxUri.toString()); + + final MatrixCursor.RowBuilder row = result.newRow(); + row.add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, rcxUri.toString()); + row.add( + DocumentsContract.Document.COLUMN_MIME_TYPE, + file.isDir() ? DocumentsContract.Document.MIME_TYPE_DIR : file.getMimeType() + ); + row.add(DocumentsContract.Document.COLUMN_DISPLAY_NAME, file.getName()); + row.add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, file.getModTime()); + row.add(DocumentsContract.Document.COLUMN_FLAGS, SUPPORTED_DOCUMENT_FLAGS); + row.add(DocumentsContract.Document.COLUMN_SIZE, file.isDir() ? null : file.getSize()); + } + + @Override + public Cursor queryChildDocuments(String parentUri, String[] projection, String sortOrder) throws FileNotFoundException { + if (projection == null) { + projection = DEFAULT_DOCUMENT_PROJECTION; + } + + final MatrixCursor result = new MatrixCursor(projection) { + @Override + public Bundle getExtras() { + Bundle bundle = new Bundle(); + bundle.putBoolean(DocumentsContract.EXTRA_LOADING, true); + return bundle; + } + }; + + RcxUri parentRcxUri = new RcxUri(parentUri); + + Log.d(TAG, "Querying child documents from URI " + parentRcxUri.toString()); + final List fileItems = rclone.ls( + parentRcxUri.getRemoteItem(rclone), + parentRcxUri.getPathForRClone(), + false + ); + if (fileItems == null) { + throw new FileNotFoundException("rclone call failed."); + } + + for (final FileItem file : fileItems) { + includeFileItem(result, file, parentRcxUri); + } + + return result; + } + + @Override + public Cursor queryDocument(String uri, String[] projection) throws FileNotFoundException { + if (projection == null) { + projection = DEFAULT_DOCUMENT_PROJECTION; + } + + final MatrixCursor result = new MatrixCursor(projection, 0); + + RcxUri rcxUri = new RcxUri(uri); + RcxUri parentUri = rcxUri.getParentRcxUri(); + if (parentUri == null) { + // Special case: we're at root + final MatrixCursor.RowBuilder row = result.newRow(); + row.add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, uri); + row.add( + DocumentsContract.Document.COLUMN_MIME_TYPE, + DocumentsContract.Document.MIME_TYPE_DIR + ); + row.add(DocumentsContract.Document.COLUMN_DISPLAY_NAME, rcxUri.getRemoteName()); + row.add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, null); + row.add(DocumentsContract.Document.COLUMN_FLAGS, SUPPORTED_DOCUMENT_FLAGS); + row.add(DocumentsContract.Document.COLUMN_SIZE, null); + } else { + includeFileItem(result, rcxUri.getFileItem(rclone), parentUri); + } + + return result; + } + + @Override + public String createDocument(String parentUri, String mimeType, String displayName) throws FileNotFoundException { + RcxUri rcxUri = new RcxUri(parentUri).getChildRcxUri(displayName); + + if ( + DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType) + && rclone.makeDirectory(rcxUri.getRemoteItem(rclone), rcxUri.getPathForRClone()) + ) { + return rcxUri.toString(); + } + + final Process proc = rclone.rCatFile( + rcxUri.getRemoteItem(rclone), + rcxUri.getPathForRClone() + ); + final OutputStream stdin = proc.getOutputStream(); + try { + stdin.close(); + proc.waitFor(); + } catch (IOException | InterruptedException e) { + Log.e(TAG, "Got exception during document creation.", e); + } + + if (proc.exitValue() == 0) { + return rcxUri.toString(); + } + + throw new FileNotFoundException( + "Couldn't create document at URI " + rcxUri.toString() + "." + ); + } + + @Override + public ParcelFileDescriptor openDocument( + String uri, + String mode, + @Nullable CancellationSignal cs + ) throws FileNotFoundException { + Log.d(TAG, "openDocument, mode: " + mode); + RcxUri rcxUri = new RcxUri(uri); + + ParcelFileDescriptor[] pipe; + try { + pipe = ParcelFileDescriptor.createPipe(); + } catch (IOException e) { + Log.e(TAG, "Couldn't create pipe for document " + uri + ".", e); + throw new FileNotFoundException(); + } + + if ("r".equals(mode)) { + final Process proc = rclone.catFile( + rcxUri.getRemoteItem(rclone), + rcxUri.getPathForRClone() + ); + final InputStream is = proc.getInputStream(); + final OutputStream os = new ParcelFileDescriptor.AutoCloseOutputStream(pipe[1]); + new BufferedTransferThread(is, os, cs).start(); + return pipe[0]; + } + else if ("w".equals(mode)) { + final Process proc = rclone.rCatFile( + rcxUri.getRemoteItem(rclone), + rcxUri.getPathForRClone() + ); + final OutputStream os = proc.getOutputStream(); + final InputStream is = new ParcelFileDescriptor.AutoCloseInputStream(pipe[0]); + new BufferedTransferThread(is, os, cs).start(); + return pipe[1]; + } + + throw new FileNotFoundException( + "Cannot open uri " + uri + " in mode " + mode + "." + ); + } + + @Override + public void deleteDocument(String uri) throws FileNotFoundException { + RcxUri rcxUri = new RcxUri(uri); + Process p = rclone.deleteItems( + rcxUri.getRemoteItem(rclone), + rcxUri.getFileItem(rclone) + ); + try { + p.waitFor(); + } catch (InterruptedException e) { + Log.e(TAG, "Delete process was interupted.", e); + return; + } + + if (p.exitValue() != 0) { + Log.e(TAG, "Couldn't delete file at URI " + uri + "."); + } + } + + private String mvDocument(RcxUri sourceRcxUri, RcxUri targetRcxUri) throws FileNotFoundException { + if (!sourceRcxUri.getRemoteName().equals( + targetRcxUri.getRemoteName() + )) { + throw new FileNotFoundException("Can't move remote document to another remote."); + } + + if (!rclone.moveTo( + sourceRcxUri.getRemoteItem(rclone), + sourceRcxUri.getPathForRClone(), + targetRcxUri.getPathForRClone() + )) { + throw new FileNotFoundException( + "Couldn't move item file at URI " + sourceRcxUri.toString() + "." + ); + } + + return targetRcxUri.toString(); + } + + @Override + public String renameDocument(String sourceUri, String displayName) throws FileNotFoundException { + RcxUri sourceRcxUri = new RcxUri(sourceUri); + RcxUri targetRcxUri = sourceRcxUri.getParentRcxUri().getChildRcxUri(displayName); + return mvDocument(sourceRcxUri, targetRcxUri); + } + + @Override + public String moveDocument(String sourceUri, String sourceParentUri, String targetParentUri) throws FileNotFoundException { + RcxUri sourceRcxUri = new RcxUri(sourceUri); + + RcxUri targetParentRcxUrl = new RcxUri(targetParentUri); + String fileName = sourceRcxUri.getFileName(); + RcxUri targetRcxUri = targetParentRcxUrl.getChildRcxUri(fileName); + + return mvDocument(sourceRcxUri, targetRcxUri); + } +} diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/Settings/GeneralSettingsFragment.java b/app/src/main/java/ca/pkay/rcloneexplorer/Settings/GeneralSettingsFragment.java index f2c7b29c..40b1fa06 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/Settings/GeneralSettingsFragment.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/Settings/GeneralSettingsFragment.java @@ -215,7 +215,6 @@ private void showAppShortcutDialog() { Rclone rclone = new Rclone(context); final ArrayList remotes = new ArrayList<>(rclone.getRemotes()); - RemoteItem.prepareDisplay(context, remotes); Collections.sort(remotes, (a, b) -> a.getDisplayName().compareTo(b.getDisplayName())); final CharSequence[] options = new CharSequence[remotes.size()]; int i = 0; @@ -227,10 +226,10 @@ private void showAppShortcutDialog() { boolean[] checkedItems = new boolean[options.length]; i = 0; for (RemoteItem item : remotes) { - String s = item.getName().toString(); + String s = item.getName(); String hash = AppShortcutsHelper.getUniqueIdFromString(s); if (appShortcuts.contains(hash)) { - userSelected.add(item.getName().toString()); + userSelected.add(item.getName()); checkedItems[i] = true; } i++; diff --git a/build.gradle b/build.gradle index 77912987..8af4fed9 100644 --- a/build.gradle +++ b/build.gradle @@ -1,17 +1,20 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - repositories { google() jcenter() } + dependencies { - classpath 'com.android.tools.build:gradle:3.6.0' + classpath 'com.android.tools.build:gradle:4.0.1' - if (!getGradle().getStartParameter().getTaskRequests().toString().toLowerCase().contains("oss")) { - classpath 'com.google.gms:google-services:4.3.3' // google-services plugin - classpath 'com.google.firebase:firebase-crashlytics-gradle:2.0.0-beta03' + for (String taskName : getGradle().getStartParameter().getTaskNames()) { + if (taskName.endsWith('RcxDebug') || taskName.endsWith('RcxRelease')) { + classpath 'com.google.gms:google-services:4.3.3' + classpath 'com.google.firebase:firebase-crashlytics-gradle:2.0.0-beta03' + break + } } // NOTE: Do not place your application dependencies here; they belong diff --git a/gradle.properties b/gradle.properties index 347c1725..ccb5d7ec 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,6 +14,10 @@ android.useAndroidX=true # android.enableR8=true org.gradle.jvmargs=-Xmx1536m +io.github.x0b.rcx.rCloneVersion=1.52.3 +io.github.x0b.rcx.buildToolsVersion=29.0.3 +io.github.x0b.rcx.ndkVersion=21.3.6528147 + # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index aa62ffe1..57c85fd0 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ +#Sun Jul 12 14:10:53 CEST 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-bin.zip -distributionSha256Sum=1f3067073041bc44554d0efe5d402a33bc3d3c93cc39ab684f308586d732a80d zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip diff --git a/rclone/.gitignore b/rclone/.gitignore index 71a8716d..14d86ad6 100644 --- a/rclone/.gitignore +++ b/rclone/.gitignore @@ -1 +1 @@ -/gopath +/cache diff --git a/rclone/build.gradle b/rclone/build.gradle index 5015e0cb..1349b105 100644 --- a/rclone/build.gradle +++ b/rclone/build.gradle @@ -1,106 +1,151 @@ -// Rclone version - any git reference (tag, branch, hash) should work -def buildTag = 'v1.51.0' - -// -// DO NOT EDIT ANYTHING BELOW -// import java.nio.file.Paths -def repository = 'github.com/rclone/rclone' -def goPath = Paths.get(projectDir.absolutePath, 'gopath').toAbsolutePath().toString() -def osName = System.properties['os.name'].toLowerCase() -def osArch = System.properties['os.arch'] -def String os -if (osName.contains('windows')) { - if(osArch.equals('amd64')) { - os = "windows-x86_64" - } else if (osArch.equals('x86')) { - os = "windows" + +ext { + NDK_VERSION = project.properties['io.github.x0b.rcx.ndkVersion'] + RCLONE_VERSION = project.properties['io.github.x0b.rcx.rCloneVersion'] + RCLONE_MODULE = 'github.com/rclone/rclone' + CACHE_PATH = Paths.get(projectDir.absolutePath, 'cache').toAbsolutePath().toString() + GOPATH = Paths.get(CACHE_PATH, "gopath").toString() +} + +def findSdkDir() { + def androidHome = System.getenv('ANDROID_HOME') + if (androidHome != null) { + return androidHome + } + + def localPropertiesFile = project.rootProject.file('local.properties') + if (localPropertiesFile.exists()) { + Properties properties = new Properties() + properties.load(localPropertiesFile.newDataInputStream()) + def sdkDir = properties.get('sdk.dir') + if (sdkDir != null) { + return sdkDir + } + } + + throw GradleException( + "Couldn't locate your android SDK location. Make sure to set sdk.dir property " + + "in your local.properties at the root of the project or set ANDROID_HOME " + + "environment variable" + ) +} + +def findNdkDir() { + def sdkDir = findSdkDir() + def ndkPath = Paths.get(sdkDir, 'ndk', NDK_VERSION).resolve().toAbsolutePath() + if (!ndkPath.toFile().exists()) { + throw new GradleException(String.format( + "Couldn't find a ndk bundle in %s. Make sure that you have the proper " + + "version installed in Android Studio's SDK Manager or that you have " + + "run \"%s 'ndk;%s'\".", + ndkPath.toString(), + Paths.get(sdkDir, 'tools', 'bin', 'sdkmanager').toString(), + NDK_VERSION + )) + } + return ndkPath.toString() +} + +def getCrossCompiler(bin) { + def osName = System.properties['os.name'].toLowerCase() + def osArch = System.properties['os.arch'] + def os + if (osName.contains('windows')) { + if(osArch.equals('amd64')) { + os = "windows-x86_64" + } else if (osArch.equals('x86')) { + os = "windows" + } + } else if (osName.contains("linux")) { + os = "linux-x86_64" + } else if (osName.contains('mac')) { + os = "darwin-x86_64" + } else { + throw new GradleException("Unsupported OS.") + } + + return Paths.get( + findNdkDir(), + 'toolchains', + 'llvm', + 'prebuilt', + os, + 'bin', + bin + ) +} + +def getLibrclone(arch) { + return Paths.get('..', 'app', 'lib', arch, 'librclone.so').toString() +} + +def buildRclone(compiler, arch, abi, env = [:]) { + return { + doLast { + exec { + environment 'GOPATH', GOPATH + def crossCompiler = getCrossCompiler(compiler) + environment 'CC', crossCompiler + environment 'CC_FOR_TARGET', crossCompiler + environment 'GOOS', 'android' + environment 'GOARCH', arch + environment 'CGO_ENABLED', '1' + env.each {entry -> environment "$entry.key", "$entry.value"} + workingDir CACHE_PATH + commandLine 'go', 'build', '-tags', 'linux', '-o', getLibrclone(abi), RCLONE_MODULE + } + } } -} else if (osName.contains("linux")) { - os = "linux-x86_64" -} else if (osName.contains('mac')) { - os = "darwin-x86_64" } -task fetchRclone(type: Exec) { - mkdir "gopath" - environment 'GOPATH', Paths.get(projectDir.absolutePath, 'gopath') - commandLine 'go', 'get', '-d', repository +task createRcloneModule(type: Exec) { + // We create a fake go module as it's the only way to checkout + // a specific tag. + onlyIf { !Paths.get(CACHE_PATH, 'go.mod').toFile().exists() } + mkdir CACHE_PATH + workingDir CACHE_PATH + environment 'GOPATH', GOPATH + commandLine 'go', 'mod', 'init', 'rclone' } -task checkoutRclone(type: Exec) { - dependsOn fetchRclone - workingDir Paths.get(goPath, "src/${repository}".split('/')) - commandLine 'git', 'checkout', buildTag +task checkoutRclone(type: Exec, dependsOn: createRcloneModule) { + workingDir CACHE_PATH + environment 'GOPATH', GOPATH + commandLine 'go', 'get', '-v', '-d', "${RCLONE_MODULE}@v${RCLONE_VERSION}" } task cleanNative { enabled = false doLast { - delete '../app/lib/armeabi-v7a/librclone.so' - delete '../app/lib/arm64-v8a/librclone.so' - delete '../app/lib/x86/librclone.so' - delete '../app/lib/x86_64/librclone.so' + delete getLibrclone('armeabi-v7a') + delete getLibrclone('arm64-v8a') + delete getLibrclone('x86') + delete getLibrclone('x86_64') } } -task buildArm(type: Exec) { - dependsOn checkoutRclone - environment 'GOPATH', Paths.get(projectDir.absolutePath, 'gopath') - def String crossCompiler = Paths.get(System.getenv('ANDROID_HOME'), 'ndk-bundle', 'toolchains', 'llvm', 'prebuilt', os, 'bin', 'armv7a-linux-androideabi21-clang') - environment 'CC', crossCompiler - environment 'CC_FOR_TARGET', crossCompiler - environment 'GOOS', 'android' - environment 'GOARCH', 'arm' - environment 'GOARM', '7' - environment 'CGO_ENABLED', '1' - commandLine 'go', 'build', '-tags', 'linux', '-o', '../app/lib/armeabi-v7a/librclone.so', repository +task buildArm(dependsOn: checkoutRclone) { + configure buildRclone('armv7a-linux-androideabi21-clang', 'arm', 'armeabi-v7a', ['GOARM': '7']) } -task buildArm64(type: Exec) { - dependsOn checkoutRclone - environment 'GOPATH', Paths.get(projectDir.absolutePath, 'gopath') - def String crossCompiler = Paths.get(System.getenv('ANDROID_HOME'), 'ndk-bundle', 'toolchains', 'llvm', 'prebuilt', os, 'bin', 'aarch64-linux-android21-clang') - environment 'CC', crossCompiler - environment 'CC_FOR_TARGET', crossCompiler - environment 'GOOS', 'android' - environment 'GOARCH', 'arm64' - environment 'CGO_ENABLED', '1' - commandLine 'go', 'build', '-tags', 'linux', '-o', '../app/lib/arm64-v8a/librclone.so', repository +task buildArm64(dependsOn: checkoutRclone) { + configure buildRclone('aarch64-linux-android21-clang', 'arm64', 'arm64-v8a') } -task buildx86(type: Exec) { - dependsOn checkoutRclone - environment 'GOPATH', Paths.get(projectDir.absolutePath, 'gopath') - def String crossCompiler = Paths.get(System.getenv('ANDROID_HOME'), 'ndk-bundle', 'toolchains', 'llvm', 'prebuilt', os, 'bin', 'i686-linux-android21-clang') - environment 'CC', crossCompiler - environment 'CC_FOR_TARGET', crossCompiler - environment 'GOOS', 'android' - environment 'GOARCH', '386' - environment 'CGO_ENABLED', '1' - commandLine 'go', 'build', '-tags', 'linux', '-o', '../app/lib/x86/librclone.so', repository +task buildx86(dependsOn: checkoutRclone) { + configure buildRclone('i686-linux-android21-clang', '386', 'x86') } -task buildx64(type: Exec) { - dependsOn checkoutRclone - environment 'GOPATH', Paths.get(projectDir.absolutePath, 'gopath') - def String crossCompiler = Paths.get(System.getenv('ANDROID_HOME'), 'ndk-bundle', 'toolchains', 'llvm', 'prebuilt', os, 'bin', 'x86_64-linux-android21-clang') - environment 'CC', crossCompiler - environment 'CC_FOR_TARGET', crossCompiler - environment 'GOOS', 'android' - environment 'GOARCH', 'amd64' - environment 'CGO_ENABLED', '1' - commandLine 'go', 'build', '-tags', 'linux', '-o', '../app/lib/x86_64/librclone.so', repository +task buildx64(dependsOn: checkoutRclone) { + configure buildRclone('x86_64-linux-android21-clang', 'amd64', 'x86_64') } task buildNative { - dependsOn fetchRclone - dependsOn checkoutRclone dependsOn buildArm dependsOn buildArm64 dependsOn buildx86 dependsOn buildx64 } -buildNative.mustRunAfter(buildArm, buildArm64, buildx86, buildx64) defaultTasks 'buildNative' diff --git a/safdav/build.gradle b/safdav/build.gradle index 5ddc3ba2..17fac3db 100644 --- a/safdav/build.gradle +++ b/safdav/build.gradle @@ -2,7 +2,7 @@ apply plugin: 'com.android.library' android { compileSdkVersion 29 - + buildToolsVersion project.properties['io.github.x0b.rcx.buildToolsVersion'] defaultConfig { minSdkVersion 21