From 5e4781bf918abf00f240d5b90dd22c12c0b08b3a Mon Sep 17 00:00:00 2001 From: Gilbert Gilb's Date: Sun, 12 Jul 2020 04:30:24 +0200 Subject: [PATCH 01/15] Improve build environment. - Update android SDK and gradlewrapper. - Make rclone build automatically when building the app. Previously, rcx would happily build without the SO which would make it crash at runtime and be surprising to the developer. - Fix build when google-services.json is missing. We now just check if the "google-services.json" exists to know if we want to include firebase and gms. --- app/build.gradle | 19 ++++-- build.gradle | 13 ++-- gradle/wrapper/gradle-wrapper.properties | 4 +- rclone/build.gradle | 75 ++++++++++++++---------- rclone/gradle.properties | 1 + safdav/build.gradle | 5 +- 6 files changed, 71 insertions(+), 46 deletions(-) create mode 100644 rclone/gradle.properties diff --git a/app/build.gradle b/app/build.gradle index d412e7e3..d0d57a18 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,20 +1,28 @@ 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 + } } android { + gradle.projectsEvaluated { + preBuild.dependsOn(':rclone:buildNative') + } + signingConfigs { github_x0b { keyAlias 'github_x0b' } } - compileSdkVersion 29 + compileSdkVersion 30 defaultConfig { applicationId 'io.github.x0b.rcx' minSdkVersion 21 - targetSdkVersion 29 + targetSdkVersion 30 versionCode 170 // last digit is reserved for ABI, only ever end on 0! versionName '1.11.4' testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -78,6 +86,7 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + buildToolsVersion '30.0.1' } repositories { diff --git a/build.gradle b/build.gradle index 77912987..a818c90d 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.0' - 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' // google-services plugin + 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/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/build.gradle b/rclone/build.gradle index 5015e0cb..fca32ad7 100644 --- a/rclone/build.gradle +++ b/rclone/build.gradle @@ -1,37 +1,51 @@ -// 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" + +def getCrossCompiler(bin) { + Properties properties = new Properties() + properties.load(project.rootProject.file('local.properties').newDataInputStream()) + def ndkDir = properties.getProperty('ndk.dir') + + 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.") } -} else if (osName.contains("linux")) { - os = "linux-x86_64" -} else if (osName.contains('mac')) { - os = "darwin-x86_64" + + return Paths.get( + ndkDir, + 'toolchains', + 'llvm', + 'prebuilt', + os, + 'bin', + bin + ) } task fetchRclone(type: Exec) { - mkdir "gopath" - environment 'GOPATH', Paths.get(projectDir.absolutePath, 'gopath') + mkdir goPath + environment 'GOPATH', goPath commandLine 'go', 'get', '-d', repository } task checkoutRclone(type: Exec) { dependsOn fetchRclone workingDir Paths.get(goPath, "src/${repository}".split('/')) - commandLine 'git', 'checkout', buildTag + commandLine 'git', 'checkout', RCLONE_VERSION } task cleanNative { @@ -46,8 +60,8 @@ task cleanNative { 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 'GOPATH', goPath + def crossCompiler = getCrossCompiler('armv7a-linux-androideabi21-clang') environment 'CC', crossCompiler environment 'CC_FOR_TARGET', crossCompiler environment 'GOOS', 'android' @@ -59,8 +73,8 @@ task buildArm(type: Exec) { 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 'GOPATH', goPath + def crossCompiler = getCrossCompiler('aarch64-linux-android21-clang') environment 'CC', crossCompiler environment 'CC_FOR_TARGET', crossCompiler environment 'GOOS', 'android' @@ -71,8 +85,8 @@ task buildArm64(type: Exec) { 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 'GOPATH', goPath + def crossCompiler = getCrossCompiler('i686-linux-android21-clang') environment 'CC', crossCompiler environment 'CC_FOR_TARGET', crossCompiler environment 'GOOS', 'android' @@ -83,8 +97,8 @@ task buildx86(type: Exec) { 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 'GOPATH', goPath + def crossCompiler = getCrossCompiler('x86_64-linux-android21-clang') environment 'CC', crossCompiler environment 'CC_FOR_TARGET', crossCompiler environment 'GOOS', 'android' @@ -94,13 +108,10 @@ task buildx64(type: Exec) { } 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/rclone/gradle.properties b/rclone/gradle.properties new file mode 100644 index 00000000..08998049 --- /dev/null +++ b/rclone/gradle.properties @@ -0,0 +1 @@ +RCLONE_VERSION=v1.51.0 diff --git a/safdav/build.gradle b/safdav/build.gradle index 5ddc3ba2..21cb55bd 100644 --- a/safdav/build.gradle +++ b/safdav/build.gradle @@ -1,12 +1,12 @@ apply plugin: 'com.android.library' android { - compileSdkVersion 29 + compileSdkVersion 30 defaultConfig { minSdkVersion 21 - targetSdkVersion 29 + targetSdkVersion 30 versionCode 1 versionName "1.0" @@ -29,6 +29,7 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + buildToolsVersion '30.0.1' } repositories { From 42f9f0c6d4cfde5681d4ce19ea7968106a474f45 Mon Sep 17 00:00:00 2001 From: Gilbert Gilb's Date: Mon, 13 Jul 2020 05:33:46 +0200 Subject: [PATCH 02/15] Implement SAF documents provider. --- app/src/main/AndroidManifest.xml | 11 + .../Dialogs/RemoteDestinationDialog.java | 2 +- .../Fragments/FileExplorerFragment.java | 4 +- .../Fragments/RemotesFragment.java | 1 - .../Fragments/ShareFragment.java | 2 +- .../java/ca/pkay/rcloneexplorer/Rclone.java | 147 +++++--- .../SAFProvider/BufferedTransferThread.java | 66 ++++ .../rcloneexplorer/SAFProvider/RcxUri.java | 111 ++++++ .../SAFProvider/SAFProvider.java | 317 ++++++++++++++++++ 9 files changed, 604 insertions(+), 57 deletions(-) create mode 100644 app/src/main/java/ca/pkay/rcloneexplorer/SAFProvider/BufferedTransferThread.java create mode 100644 app/src/main/java/ca/pkay/rcloneexplorer/SAFProvider/RcxUri.java create mode 100644 app/src/main/java/ca/pkay/rcloneexplorer/SAFProvider/SAFProvider.java diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1fe34ab5..6c16452a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -63,6 +63,17 @@ android:resource="@xml/file_provider_paths" /> + + + + + + 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..dceb496a 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/Fragments/RemotesFragment.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/Fragments/RemotesFragment.java @@ -33,7 +33,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; 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..a21fc8ce 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/Fragments/ShareFragment.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/Fragments/ShareFragment.java @@ -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/Rclone.java b/app/src/main/java/ca/pkay/rcloneexplorer/Rclone.java index fdd1f507..027276ad 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; @@ -46,6 +47,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 +182,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 +258,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; } } @@ -288,7 +292,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<>(); } @@ -329,7 +333,7 @@ public List getRemotes() { remoteItemList.add(newRemote); } catch (JSONException e) { - e.printStackTrace(); + Log.e(TAG, "Runtime error.", e); } } @@ -387,7 +391,7 @@ private RemoteItem getRemoteType(JSONObject remotesJSON, RemoteItem remoteItem, remoteItem.setType(type); return remoteItem; } catch (JSONException e) { - e.printStackTrace(); + Log.e(TAG, "Runtime error.", e); } } @@ -407,7 +411,7 @@ public Process configCreate(List options) { try { return Runtime.getRuntime().exec(commandWithOptions); } catch (IOException e) { - e.printStackTrace(); + Log.e(TAG, "Runtime error.", e); return null; } } @@ -426,7 +430,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 +448,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 +505,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 +520,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; } + } - File file = new File(uploadFile); + 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(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 +661,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 +686,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 +713,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 +741,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 +755,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 +783,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 +820,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 +854,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 +876,7 @@ public String getRcloneVersion() { result.add(line); } } catch (IOException | InterruptedException e) { - e.printStackTrace(); + Log.e(TAG, "Runtime error.", e); return "-1"; } @@ -946,7 +991,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 +1005,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 +1017,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 +1048,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; 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); + } +} From a9aee9dea9c39fc08ee5eff26119fdc96b95855e Mon Sep 17 00:00:00 2001 From: Gilbert Gilb's Date: Mon, 13 Jul 2020 06:12:50 +0200 Subject: [PATCH 03/15] Fix root display name not refreshed in SAF provider. This is a major rework of how remote names are handled. --- app/build.gradle | 10 +-- .../rcloneexplorer/AppShortcutsHelper.java | 7 +- .../Dialogs/RemotePropertiesDialog.java | 2 +- .../Fragments/FileExplorerFragment.java | 4 +- .../Fragments/RemotesFragment.java | 23 ++----- .../Fragments/ShareRemotesFragment.java | 1 - .../pkay/rcloneexplorer/Items/RemoteItem.java | 26 ++------ .../ca/pkay/rcloneexplorer/MainActivity.java | 37 +++-------- .../java/ca/pkay/rcloneexplorer/Rclone.java | 64 ++++++++++++++++++- .../RemotesRecyclerViewAdapter.java | 2 +- .../ShareRemotesRecyclerViewAdapter.java | 2 +- .../RemoteConfig/AliasConfig.java | 5 +- .../RemoteConfig/CacheConfig.java | 5 +- .../RemoteConfig/CryptConfig.java | 5 +- .../RemoteConfig/UnionConfig.java | 5 +- .../SAFProvider/SAFProvider.java | 2 +- .../Settings/GeneralSettingsFragment.java | 9 ++- app/src/main/res/values/strings.xml | 2 - 18 files changed, 106 insertions(+), 105 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index d0d57a18..5c049d69 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -71,10 +71,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 -> @@ -111,6 +112,7 @@ dependencies { implementation 'org.markdownj:markdownj-core:0.4' implementation 'jp.wasabeef:recyclerview-animators:2.3.0' implementation 'com.github.GrenderG:Toasty:1.3.0' + implementation 'commons-io:commons-io:2.7' // Firebase & Crashlytics rcxImplementation 'com.google.firebase:firebase-analytics:17.3.0' rcxImplementation 'com.google.firebase:firebase-crashlytics:17.0.0-beta03' diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/AppShortcutsHelper.java b/app/src/main/java/ca/pkay/rcloneexplorer/AppShortcutsHelper.java index 174eac48..c0fa1285 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/AppShortcutsHelper.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/AppShortcutsHelper.java @@ -42,7 +42,6 @@ public static void populateAppShortcuts(Context context, List remote Set shortcutSet = new HashSet<>(); List shortcutInfoList = new ArrayList<>(); - RemoteItem.prepareDisplay(context, remotes); for (RemoteItem remoteItem : remotes) { String id = getUniqueIdFromString(remoteItem.getName()); @@ -51,7 +50,7 @@ public static void populateAppShortcuts(Context context, List remote intent.putExtra(APP_SHORTCUT_REMOTE_NAME, remoteItem.getName()); ShortcutInfo shortcut = new ShortcutInfo.Builder(context, id) - .setShortLabel(remoteItem.getDisplayName()) + .setShortLabel(remoteItem.getName()) .setIcon(Icon.createWithResource(context, AppShortcutsHelper.getRemoteIcon(remoteItem.getType(), remoteItem.isCrypt()))) .setIntent(intent) .build(); @@ -164,7 +163,7 @@ public static void addRemoteToAppShortcuts(Context context, RemoteItem remoteIte intent.putExtra(APP_SHORTCUT_REMOTE_NAME, remoteItem.getName()); ShortcutInfo shortcut = new ShortcutInfo.Builder(context, id) - .setShortLabel(remoteItem.getDisplayName()) + .setShortLabel(remoteItem.getName()) .setIcon(Icon.createWithResource(context, AppShortcutsHelper.getRemoteIcon(remoteItem.getType(), remoteItem.isCrypt()))) .setIntent(intent) .build(); @@ -184,7 +183,7 @@ public static void addRemoteToHomeScreen(Context context, RemoteItem remoteItem) intent.putExtra(APP_SHORTCUT_REMOTE_NAME, remoteItem.getName()); ShortcutInfoCompat shortcutInfo = new ShortcutInfoCompat.Builder(context, id) - .setShortLabel(remoteItem.getDisplayName()) + .setShortLabel(remoteItem.getName()) .setIcon(IconCompat.createWithResource(context, AppShortcutsHelper.getRemoteIcon(remoteItem.getType(), remoteItem.isCrypt()))) .setIntent(intent) .build(); diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/Dialogs/RemotePropertiesDialog.java b/app/src/main/java/ca/pkay/rcloneexplorer/Dialogs/RemotePropertiesDialog.java index 15db0999..ff4e2f73 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/Dialogs/RemotePropertiesDialog.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/Dialogs/RemotePropertiesDialog.java @@ -90,7 +90,7 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { LayoutInflater inflater = ((FragmentActivity) context).getLayoutInflater(); view = inflater.inflate(R.layout.dialog_remote_properties, null); - ((TextView) view.findViewById(R.id.remote_name)).setText(remote.getDisplayName()); + ((TextView) view.findViewById(R.id.remote_name)).setText(remote.getName()); View storageContainer = view.findViewById(R.id.remote_storage_container); remoteStorageStats = view.findViewById(R.id.remote_storage_stats); 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 a4c948ff..9c15da7e 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/Fragments/FileExplorerFragment.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/Fragments/FileExplorerFragment.java @@ -303,7 +303,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.getDisplayName(), "//" + remoteName); + breadcrumbView.addCrumb(remote.getName(), "//" + remoteName); if (savedInstanceState != null) { if (!directoryObject.getCurrentPath().equals("//" + remoteName)) { breadcrumbView.buildBreadCrumbsFromPath(directoryObject.getCurrentPath()); @@ -935,7 +935,7 @@ private void cancelMoveClicked() { if (!moveStartPath.equals("//" + remoteName)) { breadcrumbView.buildBreadCrumbsFromPath(directoryObject.getCurrentPath()); } - breadcrumbView.addCrumb(remote.getDisplayName(), "//" + remoteName); + breadcrumbView.addCrumb(remote.getName(), "//" + remoteName); moveStartPath = null; } } 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 dceb496a..ce450407 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/Fragments/RemotesFragment.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/Fragments/RemotesFragment.java @@ -302,7 +302,6 @@ private void refreshRemotes() { 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<>(); @@ -314,13 +313,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; } @@ -469,21 +461,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(); + String initialText = remoteItem.getName(); 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); @@ -498,7 +483,7 @@ private void deleteRemote(final RemoteItem remoteItem) { builder = new AlertDialog.Builder(context); } builder.setTitle(R.string.delete_remote_title); - builder.setMessage(remoteItem.getDisplayName()); + builder.setMessage(remoteItem.getName()); builder.setNegativeButton(R.string.cancel, null); builder.setPositiveButton(R.string.delete, (dialog, which) -> new DeleteRemote(context, remoteItem).execute()); builder.show(); 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..b0e2e65d 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/Items/RemoteItem.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/Items/RemoteItem.java @@ -249,27 +249,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 +386,10 @@ public int compareTo(@NonNull RemoteItem remoteItem) { } else if (!this.isPinned && remoteItem.isPinned) { return 1; } - return getDisplayName().toLowerCase().compareTo(remoteItem.getDisplayName().toLowerCase()); + + String self = getName().toLowerCase(); + String other = remoteItem.getName().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..996d0028 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/MainActivity.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/MainActivity.java @@ -366,18 +366,9 @@ 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()); + MenuItem menuItem = subMenu.add(R.id.nav_pinned, availableDrawerPinnedRemoteId, Menu.NONE, remoteItem.getName()); drawerPinnedRemoteIds.put(availableDrawerPinnedRemoteId, remoteItem); availableDrawerPinnedRemoteId++; menuItem.setIcon(remoteItem.getRemoteIcon()); @@ -735,9 +726,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() { @@ -790,14 +778,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,23 +817,22 @@ 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(); 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 = rclone.getUniqueRemoteName(description); } } String path = root.getAbsolutePath(); ArrayList options = new ArrayList<>(); - options.add(id); + options.add(name); 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,11 @@ 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<>()); 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); + pinnedRemotes.add(name); + generatedRemotes.add(name); 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 027276ad..66d5ccd4 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/Rclone.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/Rclone.java @@ -18,6 +18,8 @@ import io.github.x0b.safdav.SafAccessProvider; import io.github.x0b.safdav.SafDAVServer; import io.github.x0b.safdav.file.SafConstants; + +import org.apache.commons.io.FileUtils; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -407,7 +409,6 @@ public Process configCreate(List options) { System.arraycopy(opt, 0, commandWithOptions, command.length, opt.length); - try { return Runtime.getRuntime().exec(commandWithOptions); } catch (IOException e) { @@ -1148,6 +1149,67 @@ public void exportConfigFile(Uri uri) throws IOException { outputStream.close(); } + public boolean renameRemote(String remoteName, String newName) { + // RClone uses this library to parse its config file: https://github.com/unknwon/goconfig + // It's very similar to the INI syntax, where section corresponds to a + // remote. Sections can contain any unicode character except for line + // breaks and are completely unescaped. + + boolean found = false; + String searchedLine = "[" + remoteName + "]"; + File file = new File(rcloneConf); + List configContent; + List newConfigContent; + + try { + configContent = FileUtils.readLines(file, "UTF-8"); + } catch (IOException e) { + Log.e(TAG, "Couldn't parse RClone config.", e); + return false; + } + + newConfigContent = new ArrayList<>(configContent.size()); + for (String line : configContent) { + if (line.trim().equals(searchedLine)) { + newConfigContent.add("[" + newName + "]"); + found = true; + } + else { + newConfigContent.add(line); + } + } + + if (!found) { + return false; + } + + try { + FileUtils.writeLines(file, newConfigContent); + } catch (IOException e) { + Log.e(TAG, "Couldn't write RClone config.", e); + return false; + } + + return true; + } + + public String getUniqueRemoteName(String desiredName) { + List remotes = getRemotes(); + Set remoteNames = new HashSet<>(remotes.size()); + for (RemoteItem remoteItem : remotes) { + remoteNames.add(remoteItem.getName()); + } + if (!remoteNames.contains(desiredName)) { + return desiredName; + } + for (int i = 1;;++i) { + String remoteName = desiredName + " " + i; + if (!remoteNames.contains(remoteName)) { + return remoteName; + } + } + } + /** * Prefixes local remotes with a base path on the primary external storage. * @param item diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/RecyclerViewAdapters/RemotesRecyclerViewAdapter.java b/app/src/main/java/ca/pkay/rcloneexplorer/RecyclerViewAdapters/RemotesRecyclerViewAdapter.java index ce1b5b65..f22a42a5 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/RecyclerViewAdapters/RemotesRecyclerViewAdapter.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/RecyclerViewAdapters/RemotesRecyclerViewAdapter.java @@ -48,7 +48,7 @@ public void onBindViewHolder(@NonNull final ViewHolder holder, final int positio RemoteItem item = remotes.get(position); boolean isPinned = item.isPinned(); holder.remoteName = item.getName(); - holder.tvName.setText(item.getDisplayName()); + holder.tvName.setText(item.getName()); int icon = item.getRemoteIcon(); holder.ivIcon.setImageDrawable(view.getResources().getDrawable(icon)); diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/RecyclerViewAdapters/ShareRemotesRecyclerViewAdapter.java b/app/src/main/java/ca/pkay/rcloneexplorer/RecyclerViewAdapters/ShareRemotesRecyclerViewAdapter.java index e7811bf3..02789cef 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/RecyclerViewAdapters/ShareRemotesRecyclerViewAdapter.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/RecyclerViewAdapters/ShareRemotesRecyclerViewAdapter.java @@ -40,7 +40,7 @@ public void onBindViewHolder(@NonNull final ViewHolder holder, final int positio String remoteName = remotes.get(position).getName(); boolean isPinned = remotes.get(position).isPinned(); holder.remoteName = remoteName; - holder.tvName.setText(remotes.get(position).getDisplayName()); + holder.tvName.setText(remotes.get(position).getName()); int icon = remotes.get(position).getRemoteIcon(); holder.ivIcon.setImageDrawable(view.getResources().getDrawable(icon)); 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..bf5b16ed 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/RemoteConfig/AliasConfig.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/RemoteConfig/AliasConfig.java @@ -131,12 +131,11 @@ private void setRemote() { return; } - RemoteItem.prepareDisplay(context, remotes); - Collections.sort(remotes, (a, b) -> a.getDisplayName().compareTo(b.getDisplayName())); + Collections.sort(remotes, (a, b) -> a.getName().compareTo(b.getName())); String[] options = new String[remotes.size()]; int i = 0; for (RemoteItem remote : remotes) { - options[i++] = remote.getDisplayName(); + options[i++] = remote.getName(); } AlertDialog.Builder builder; 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..130be09e 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/RemoteConfig/CacheConfig.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/RemoteConfig/CacheConfig.java @@ -286,12 +286,11 @@ 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())); + Collections.sort(remotes, (a, b) -> a.getName().compareTo(b.getName())); String[] options = new String[remotes.size()]; int i = 0; for (RemoteItem remote : remotes) { - options[i++] = remote.getDisplayName(); + options[i++] = remote.getName(); } AlertDialog.Builder builder; 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..3477a390 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/RemoteConfig/CryptConfig.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/RemoteConfig/CryptConfig.java @@ -289,12 +289,11 @@ private void setRemote() { return; } - RemoteItem.prepareDisplay(context, remotes); - Collections.sort(remotes, (a, b) -> a.getDisplayName().compareTo(b.getDisplayName())); + Collections.sort(remotes, (a, b) -> a.getName().compareTo(b.getName())); String[] options = new String[remotes.size()]; int i = 0; for (RemoteItem remote : remotes) { - options[i++] = remote.getDisplayName(); + options[i++] = remote.getName(); } AlertDialog.Builder builder; 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..10a89beb 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/RemoteConfig/UnionConfig.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/RemoteConfig/UnionConfig.java @@ -122,12 +122,11 @@ private void addRemote() { return; } - RemoteItem.prepareDisplay(context, configuredRemotes); - Collections.sort(configuredRemotes, (a, b) -> a.getDisplayName().compareTo(b.getDisplayName())); + Collections.sort(configuredRemotes, (a, b) -> a.getName().compareTo(b.getName())); String[] options = new String[configuredRemotes.size()]; int i = 0; for (RemoteItem remote : configuredRemotes) { - options[i++] = remote.getDisplayName(); + options[i++] = remote.getName(); } AlertDialog.Builder builder; diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/SAFProvider/SAFProvider.java b/app/src/main/java/ca/pkay/rcloneexplorer/SAFProvider/SAFProvider.java index 4a9517e0..c62982fd 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/SAFProvider/SAFProvider.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/SAFProvider/SAFProvider.java @@ -88,7 +88,7 @@ public Cursor queryRoots(String[] projection) { 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_SUMMARY, remote.getName()); row.add( DocumentsContract.Root.COLUMN_FLAGS, DocumentsContract.Root.FLAG_SUPPORTS_CREATE 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..d5178c29 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/Settings/GeneralSettingsFragment.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/Settings/GeneralSettingsFragment.java @@ -215,22 +215,21 @@ 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())); + Collections.sort(remotes, (a, b) -> a.getName().compareTo(b.getName())); final CharSequence[] options = new CharSequence[remotes.size()]; int i = 0; for (RemoteItem remoteItem : remotes) { - options[i++] = remoteItem.getDisplayName(); + options[i++] = remoteItem.getName(); } final ArrayList userSelected = new ArrayList<>(); 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/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e8ee464a..f1ce8b38 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -420,8 +420,6 @@ Hidden remotes shared_preferences_hidden_remotes Select remotes to hide - renamed_remotes - remote_name_%1$s Allow access from other devices local_alias_remotes refresh_local_aliases From e54ed51d6cf8b46266d6b1668a987dbc922f7260 Mon Sep 17 00:00:00 2001 From: Gilbert Gilb's Date: Mon, 13 Jul 2020 23:45:56 +0200 Subject: [PATCH 04/15] Fix CI. Also update NDK in CI. --- .github/workflows/android.yml | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index feade6e2..407df769 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -1,5 +1,8 @@ name: Android CI +env: + NDK_VERSION: "21.3.6528147" + on: push: branches: @@ -10,25 +13,25 @@ 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: Install NDK + run: echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;${NDK_VERSION}" + - name: Configure local.properties + run: | + echo "sdk.dir=${ANDROID_HOME}" >> local.properties + echo "ndk.dir=${ANDROID_HOME}/ndk/${NDK_VERSION}" >> local.properties - name: Build app run: ./gradlew assembleOssDebug - name: Upload APK From 7904a6e2be5d4372d6be84a9b1bd9cbfa4396cd7 Mon Sep 17 00:00:00 2001 From: Gilbert Gilb's Date: Tue, 14 Jul 2020 01:43:00 +0200 Subject: [PATCH 05/15] Improve Android SDK detection and fix NDK and build tools version. --- .github/workflows/android.yml | 12 ++--- app/build.gradle | 4 +- gradle.properties | 4 ++ rclone/build.gradle | 82 +++++++++++++++++++++++++---------- rclone/gradle.properties | 1 - 5 files changed, 71 insertions(+), 32 deletions(-) delete mode 100644 rclone/gradle.properties diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 407df769..a326d2dd 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -1,8 +1,5 @@ name: Android CI -env: - NDK_VERSION: "21.3.6528147" - on: push: branches: @@ -26,12 +23,11 @@ jobs: with: go-version: 1.14 id: go - - name: Install NDK - run: echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;${NDK_VERSION}" - - name: Configure local.properties + - name: Configure Android SDK run: | - echo "sdk.dir=${ANDROID_HOME}" >> local.properties - echo "ndk.dir=${ANDROID_HOME}/ndk/${NDK_VERSION}" >> local.properties + 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" "build-tools;${BUILD_TOOLS_VERSION}" "ndk;${NDK_VERSION}" - name: Build app run: ./gradlew assembleOssDebug - name: Upload APK diff --git a/app/build.gradle b/app/build.gradle index 5c049d69..cdf0f370 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,6 +13,9 @@ android { preBuild.dependsOn(':rclone:buildNative') } + buildToolsVersion project.properties['io.github.x0b.rcx.buildToolsVersion'] + ndkVersion project.properties['io.github.x0b.rcx.ndkVersion'] + signingConfigs { github_x0b { keyAlias 'github_x0b' @@ -87,7 +90,6 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } - buildToolsVersion '30.0.1' } repositories { diff --git a/gradle.properties b/gradle.properties index 347c1725..389f6b21 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=v1.51.0 +io.github.x0b.rcx.buildToolsVersion=30.0.1 +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/rclone/build.gradle b/rclone/build.gradle index fca32ad7..09489a6d 100644 --- a/rclone/build.gradle +++ b/rclone/build.gradle @@ -1,13 +1,47 @@ import java.nio.file.Paths -def repository = 'github.com/rclone/rclone' -def goPath = Paths.get(projectDir.absolutePath, 'gopath').toAbsolutePath().toString() +def RCLONE_VERSION = project.properties['io.github.x0b.rcx.rCloneVersion'] +def RCLONE_REPOSITORY = 'github.com/rclone/rclone' +def GO_PATH = Paths.get(projectDir.absolutePath, 'gopath').toAbsolutePath().toString() -def getCrossCompiler(bin) { - Properties properties = new Properties() - properties.load(project.rootProject.file('local.properties').newDataInputStream()) - def ndkDir = properties.getProperty('ndk.dir') +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 ndkVersion = project.properties['io.github.x0b.rcx.ndkVersion'] + def sdkDir = findSdkDir() + def ndkPath = Paths.get(sdkDir, 'ndk', ndkVersion).resolve().toAbsolutePath() + if (!ndkPath.toFile().exists()) { + throw new GradleException( + "Couldn't find a ndk-bundle in " + ndkPath.toString() + ". Make sure it is installed " + + "by running the command 'sdkmanager \"ndk;" + ndkVersion + "\"." + ) + } + return ndkPath.toString() +} +def getCrossCompiler(bin) { + def ndkDir = findNdkDir() def osName = System.properties['os.name'].toLowerCase() def osArch = System.properties['os.arch'] def os @@ -36,31 +70,35 @@ def getCrossCompiler(bin) { ) } +def getLibrclone(arch) { + return Paths.get('..', 'app', 'lib', arch, 'librclone.so').toString() +} + task fetchRclone(type: Exec) { - mkdir goPath - environment 'GOPATH', goPath - commandLine 'go', 'get', '-d', repository + mkdir GO_PATH + environment 'GOPATH', GO_PATH + commandLine 'go', 'get', '-d', RCLONE_REPOSITORY } task checkoutRclone(type: Exec) { dependsOn fetchRclone - workingDir Paths.get(goPath, "src/${repository}".split('/')) + workingDir Paths.get(GO_PATH, 'src', RCLONE_REPOSITORY) commandLine 'git', 'checkout', 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', goPath + environment 'GOPATH', GO_PATH def crossCompiler = getCrossCompiler('armv7a-linux-androideabi21-clang') environment 'CC', crossCompiler environment 'CC_FOR_TARGET', crossCompiler @@ -68,43 +106,43 @@ task buildArm(type: Exec) { environment 'GOARCH', 'arm' environment 'GOARM', '7' environment 'CGO_ENABLED', '1' - commandLine 'go', 'build', '-tags', 'linux', '-o', '../app/lib/armeabi-v7a/librclone.so', repository + commandLine 'go', 'build', '-tags', 'linux', '-o', getLibrclone('armeabi-v7a'), RCLONE_REPOSITORY } task buildArm64(type: Exec) { dependsOn checkoutRclone - environment 'GOPATH', goPath + environment 'GOPATH', GO_PATH def crossCompiler = getCrossCompiler('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 + commandLine 'go', 'build', '-tags', 'linux', '-o', getLibrclone('arm64-v8a'), RCLONE_REPOSITORY } task buildx86(type: Exec) { dependsOn checkoutRclone - environment 'GOPATH', goPath + environment 'GOPATH', GO_PATH def crossCompiler = getCrossCompiler('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 + commandLine 'go', 'build', '-tags', 'linux', '-o', getLibrclone('x86'), RCLONE_REPOSITORY } task buildx64(type: Exec) { dependsOn checkoutRclone - environment 'GOPATH', goPath + environment 'GOPATH', GO_PATH def crossCompiler = getCrossCompiler('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 + commandLine 'go', 'build', '-tags', 'linux', '-o', getLibrclone('x86_64'), RCLONE_REPOSITORY } task buildNative { diff --git a/rclone/gradle.properties b/rclone/gradle.properties deleted file mode 100644 index 08998049..00000000 --- a/rclone/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -RCLONE_VERSION=v1.51.0 From 94cd5b3bb6c8f0456046ff902b7d168c4b298f6e Mon Sep 17 00:00:00 2001 From: Gilbert Gilb's Date: Wed, 15 Jul 2020 10:33:24 +0200 Subject: [PATCH 06/15] Don't upgrade to API level 30 for now as it's still in beta. --- app/build.gradle | 4 ++-- gradle.properties | 2 +- rclone/build.gradle | 2 +- safdav/build.gradle | 7 +++---- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index cdf0f370..bbf51d03 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -21,11 +21,11 @@ android { keyAlias 'github_x0b' } } - compileSdkVersion 30 + compileSdkVersion 29 defaultConfig { applicationId 'io.github.x0b.rcx' minSdkVersion 21 - targetSdkVersion 30 + targetSdkVersion 29 versionCode 170 // last digit is reserved for ABI, only ever end on 0! versionName '1.11.4' testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/gradle.properties b/gradle.properties index 389f6b21..f646f09b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,7 +14,7 @@ android.useAndroidX=true # android.enableR8=true org.gradle.jvmargs=-Xmx1536m -io.github.x0b.rcx.rCloneVersion=v1.51.0 +io.github.x0b.rcx.rCloneVersion=1.51.0 io.github.x0b.rcx.buildToolsVersion=30.0.1 io.github.x0b.rcx.ndkVersion=21.3.6528147 diff --git a/rclone/build.gradle b/rclone/build.gradle index 09489a6d..5024e257 100644 --- a/rclone/build.gradle +++ b/rclone/build.gradle @@ -83,7 +83,7 @@ task fetchRclone(type: Exec) { task checkoutRclone(type: Exec) { dependsOn fetchRclone workingDir Paths.get(GO_PATH, 'src', RCLONE_REPOSITORY) - commandLine 'git', 'checkout', RCLONE_VERSION + commandLine 'git', 'checkout', 'v' + RCLONE_VERSION } task cleanNative { diff --git a/safdav/build.gradle b/safdav/build.gradle index 21cb55bd..17fac3db 100644 --- a/safdav/build.gradle +++ b/safdav/build.gradle @@ -1,12 +1,12 @@ apply plugin: 'com.android.library' android { - compileSdkVersion 30 - + compileSdkVersion 29 + buildToolsVersion project.properties['io.github.x0b.rcx.buildToolsVersion'] defaultConfig { minSdkVersion 21 - targetSdkVersion 30 + targetSdkVersion 29 versionCode 1 versionName "1.0" @@ -29,7 +29,6 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } - buildToolsVersion '30.0.1' } repositories { From fe4d56ad1274bf73a8c14365ace4fedcd7e4a457 Mon Sep 17 00:00:00 2001 From: Gilbert Gilb's Date: Wed, 15 Jul 2020 10:34:25 +0200 Subject: [PATCH 07/15] Add migration code for renamed remotes. --- .../ca/pkay/rcloneexplorer/MainActivity.java | 24 +++++++++++++++++++ app/src/main/res/values/strings.xml | 2 ++ 2 files changed, 26 insertions(+) diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/MainActivity.java b/app/src/main/java/ca/pkay/rcloneexplorer/MainActivity.java index 996d0028..061f9fce 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/MainActivity.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/MainActivity.java @@ -176,6 +176,10 @@ protected void onCreate(Bundle savedInstanceState) { AppShortcutsHelper.populateAppShortcuts(this, rclone.getRemotes()); } + // FIXME (2020-07-15) migration code that will be safe to remove once all clients have + // updated to a newer version. + migrateRenamedRemotes(); + startRemotesFragment(); SharedPreferences.Editor editor = sharedPreferences.edit(); @@ -208,6 +212,26 @@ protected void onCreate(Bundle savedInstanceState) { } } + private void migrateRenamedRemotes() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + SharedPreferences.Editor editor = sharedPreferences.edit(); + Set renamedRemotes = sharedPreferences.getStringSet( + getString(R.string.pref_key_renamed_remotes), + new HashSet<>() + ); + for(String oldName : renamedRemotes) { + String key = getString(R.string.pref_key_renamed_remote_prefix, oldName); + String newName = sharedPreferences.getString(key, null); + if (newName == null) { + continue; + } + rclone.renameRemote(oldName, newName); + editor.remove(key); + } + editor.remove(getString(R.string.pref_key_renamed_remotes)); + editor.apply(); + } + @Override protected void onStart() { super.onStart(); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f1ce8b38..e8ee464a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -420,6 +420,8 @@ Hidden remotes shared_preferences_hidden_remotes Select remotes to hide + renamed_remotes + remote_name_%1$s Allow access from other devices local_alias_remotes refresh_local_aliases From 1f6e4f2999f8c19e51ec090194825b2c174d2d16 Mon Sep 17 00:00:00 2001 From: Gilbert Gilb's Date: Wed, 15 Jul 2020 13:38:12 +0200 Subject: [PATCH 08/15] Revert back display name and let RClone class handle them. --- .../rcloneexplorer/AppShortcutsHelper.java | 6 +- .../Dialogs/RemotePropertiesDialog.java | 2 +- .../Fragments/FileExplorerFragment.java | 4 +- .../Fragments/RemotesFragment.java | 4 +- .../Fragments/ShareFragment.java | 2 +- .../pkay/rcloneexplorer/Items/RemoteItem.java | 9 +- .../ca/pkay/rcloneexplorer/MainActivity.java | 27 +----- .../java/ca/pkay/rcloneexplorer/Rclone.java | 83 +++++++++---------- .../RemotesRecyclerViewAdapter.java | 2 +- .../ShareRemotesRecyclerViewAdapter.java | 2 +- .../RemoteConfig/AliasConfig.java | 2 +- .../RemoteConfig/CacheConfig.java | 2 +- .../RemoteConfig/CryptConfig.java | 2 +- .../RemoteConfig/UnionConfig.java | 2 +- .../SAFProvider/SAFProvider.java | 2 +- .../Settings/GeneralSettingsFragment.java | 2 +- 16 files changed, 62 insertions(+), 91 deletions(-) diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/AppShortcutsHelper.java b/app/src/main/java/ca/pkay/rcloneexplorer/AppShortcutsHelper.java index c0fa1285..cd795c0e 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/AppShortcutsHelper.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/AppShortcutsHelper.java @@ -50,7 +50,7 @@ public static void populateAppShortcuts(Context context, List remote intent.putExtra(APP_SHORTCUT_REMOTE_NAME, remoteItem.getName()); ShortcutInfo shortcut = new ShortcutInfo.Builder(context, id) - .setShortLabel(remoteItem.getName()) + .setShortLabel(remoteItem.getDisplayName()) .setIcon(Icon.createWithResource(context, AppShortcutsHelper.getRemoteIcon(remoteItem.getType(), remoteItem.isCrypt()))) .setIntent(intent) .build(); @@ -163,7 +163,7 @@ public static void addRemoteToAppShortcuts(Context context, RemoteItem remoteIte intent.putExtra(APP_SHORTCUT_REMOTE_NAME, remoteItem.getName()); ShortcutInfo shortcut = new ShortcutInfo.Builder(context, id) - .setShortLabel(remoteItem.getName()) + .setShortLabel(remoteItem.getDisplayName()) .setIcon(Icon.createWithResource(context, AppShortcutsHelper.getRemoteIcon(remoteItem.getType(), remoteItem.isCrypt()))) .setIntent(intent) .build(); @@ -183,7 +183,7 @@ public static void addRemoteToHomeScreen(Context context, RemoteItem remoteItem) intent.putExtra(APP_SHORTCUT_REMOTE_NAME, remoteItem.getName()); ShortcutInfoCompat shortcutInfo = new ShortcutInfoCompat.Builder(context, id) - .setShortLabel(remoteItem.getName()) + .setShortLabel(remoteItem.getDisplayName()) .setIcon(IconCompat.createWithResource(context, AppShortcutsHelper.getRemoteIcon(remoteItem.getType(), remoteItem.isCrypt()))) .setIntent(intent) .build(); diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/Dialogs/RemotePropertiesDialog.java b/app/src/main/java/ca/pkay/rcloneexplorer/Dialogs/RemotePropertiesDialog.java index ff4e2f73..15db0999 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/Dialogs/RemotePropertiesDialog.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/Dialogs/RemotePropertiesDialog.java @@ -90,7 +90,7 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { LayoutInflater inflater = ((FragmentActivity) context).getLayoutInflater(); view = inflater.inflate(R.layout.dialog_remote_properties, null); - ((TextView) view.findViewById(R.id.remote_name)).setText(remote.getName()); + ((TextView) view.findViewById(R.id.remote_name)).setText(remote.getDisplayName()); View storageContainer = view.findViewById(R.id.remote_storage_container); remoteStorageStats = view.findViewById(R.id.remote_storage_stats); 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 9c15da7e..a4c948ff 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/Fragments/FileExplorerFragment.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/Fragments/FileExplorerFragment.java @@ -303,7 +303,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(), "//" + remoteName); + breadcrumbView.addCrumb(remote.getDisplayName(), "//" + remoteName); if (savedInstanceState != null) { if (!directoryObject.getCurrentPath().equals("//" + remoteName)) { breadcrumbView.buildBreadCrumbsFromPath(directoryObject.getCurrentPath()); @@ -935,7 +935,7 @@ private void cancelMoveClicked() { if (!moveStartPath.equals("//" + remoteName)) { breadcrumbView.buildBreadCrumbsFromPath(directoryObject.getCurrentPath()); } - breadcrumbView.addCrumb(remote.getName(), "//" + remoteName); + breadcrumbView.addCrumb(remote.getDisplayName(), "//" + remoteName); moveStartPath = null; } } 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 ce450407..dfab6490 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/Fragments/RemotesFragment.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/Fragments/RemotesFragment.java @@ -462,7 +462,7 @@ private void renameRemote(final RemoteItem remoteItem) { } final EditText remoteNameEdit = new EditText(context); - String initialText = remoteItem.getName(); + String initialText = remoteItem.getDisplayName(); remoteNameEdit.setText(initialText); builder.setView(remoteNameEdit); builder.setNegativeButton(R.string.cancel, null); @@ -483,7 +483,7 @@ private void deleteRemote(final RemoteItem remoteItem) { builder = new AlertDialog.Builder(context); } builder.setTitle(R.string.delete_remote_title); - builder.setMessage(remoteItem.getName()); + builder.setMessage(remoteItem.getDisplayName()); builder.setNegativeButton(R.string.cancel, null); builder.setPositiveButton(R.string.delete, (dialog, which) -> new DeleteRemote(context, remoteItem).execute()); builder.show(); 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 a21fc8ce..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); 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 b0e2e65d..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; } @@ -387,8 +390,8 @@ public int compareTo(@NonNull RemoteItem remoteItem) { return 1; } - String self = getName().toLowerCase(); - String other = remoteItem.getName().toLowerCase(); + String self = getDisplayName().toLowerCase(); + String other = remoteItem.getDisplayName().toLowerCase(); return self.compareTo(other); } diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/MainActivity.java b/app/src/main/java/ca/pkay/rcloneexplorer/MainActivity.java index 061f9fce..98792244 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,10 +175,6 @@ protected void onCreate(Bundle savedInstanceState) { AppShortcutsHelper.populateAppShortcuts(this, rclone.getRemotes()); } - // FIXME (2020-07-15) migration code that will be safe to remove once all clients have - // updated to a newer version. - migrateRenamedRemotes(); - startRemotesFragment(); SharedPreferences.Editor editor = sharedPreferences.edit(); @@ -212,26 +207,6 @@ protected void onCreate(Bundle savedInstanceState) { } } - private void migrateRenamedRemotes() { - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - SharedPreferences.Editor editor = sharedPreferences.edit(); - Set renamedRemotes = sharedPreferences.getStringSet( - getString(R.string.pref_key_renamed_remotes), - new HashSet<>() - ); - for(String oldName : renamedRemotes) { - String key = getString(R.string.pref_key_renamed_remote_prefix, oldName); - String newName = sharedPreferences.getString(key, null); - if (newName == null) { - continue; - } - rclone.renameRemote(oldName, newName); - editor.remove(key); - } - editor.remove(getString(R.string.pref_key_renamed_remotes)); - editor.apply(); - } - @Override protected void onStart() { super.onStart(); @@ -392,7 +367,7 @@ private void pinRemotesToDrawer() { Collections.sort(remoteItems); for (RemoteItem remoteItem : remoteItems) { if (remoteItem.isDrawerPinned()) { - MenuItem menuItem = subMenu.add(R.id.nav_pinned, availableDrawerPinnedRemoteId, Menu.NONE, remoteItem.getName()); + MenuItem menuItem = subMenu.add(R.id.nav_pinned, availableDrawerPinnedRemoteId, Menu.NONE, remoteItem.getDisplayName()); drawerPinnedRemoteIds.put(availableDrawerPinnedRemoteId, remoteItem); availableDrawerPinnedRemoteId++; menuItem.setIcon(remoteItem.getRemoteIcon()); diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/Rclone.java b/app/src/main/java/ca/pkay/rcloneexplorer/Rclone.java index 66d5ccd4..f07b2600 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/Rclone.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/Rclone.java @@ -273,8 +273,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); @@ -316,7 +326,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) { @@ -1149,48 +1167,23 @@ public void exportConfigFile(Uri uri) throws IOException { outputStream.close(); } - public boolean renameRemote(String remoteName, String newName) { - // RClone uses this library to parse its config file: https://github.com/unknwon/goconfig - // It's very similar to the INI syntax, where section corresponds to a - // remote. Sections can contain any unicode character except for line - // breaks and are completely unescaped. - - boolean found = false; - String searchedLine = "[" + remoteName + "]"; - File file = new File(rcloneConf); - List configContent; - List newConfigContent; - - try { - configContent = FileUtils.readLines(file, "UTF-8"); - } catch (IOException e) { - Log.e(TAG, "Couldn't parse RClone config.", e); - return false; - } - - newConfigContent = new ArrayList<>(configContent.size()); - for (String line : configContent) { - if (line.trim().equals(searchedLine)) { - newConfigContent.add("[" + newName + "]"); - found = true; - } - else { - newConfigContent.add(line); - } - } - - if (!found) { - return false; - } - - try { - FileUtils.writeLines(file, newConfigContent); - } catch (IOException e) { - Log.e(TAG, "Couldn't write RClone config.", e); - return false; - } - - return true; + 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(); } public String getUniqueRemoteName(String desiredName) { diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/RecyclerViewAdapters/RemotesRecyclerViewAdapter.java b/app/src/main/java/ca/pkay/rcloneexplorer/RecyclerViewAdapters/RemotesRecyclerViewAdapter.java index f22a42a5..ce1b5b65 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/RecyclerViewAdapters/RemotesRecyclerViewAdapter.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/RecyclerViewAdapters/RemotesRecyclerViewAdapter.java @@ -48,7 +48,7 @@ public void onBindViewHolder(@NonNull final ViewHolder holder, final int positio RemoteItem item = remotes.get(position); boolean isPinned = item.isPinned(); holder.remoteName = item.getName(); - holder.tvName.setText(item.getName()); + holder.tvName.setText(item.getDisplayName()); int icon = item.getRemoteIcon(); holder.ivIcon.setImageDrawable(view.getResources().getDrawable(icon)); diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/RecyclerViewAdapters/ShareRemotesRecyclerViewAdapter.java b/app/src/main/java/ca/pkay/rcloneexplorer/RecyclerViewAdapters/ShareRemotesRecyclerViewAdapter.java index 02789cef..e7811bf3 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/RecyclerViewAdapters/ShareRemotesRecyclerViewAdapter.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/RecyclerViewAdapters/ShareRemotesRecyclerViewAdapter.java @@ -40,7 +40,7 @@ public void onBindViewHolder(@NonNull final ViewHolder holder, final int positio String remoteName = remotes.get(position).getName(); boolean isPinned = remotes.get(position).isPinned(); holder.remoteName = remoteName; - holder.tvName.setText(remotes.get(position).getName()); + holder.tvName.setText(remotes.get(position).getDisplayName()); int icon = remotes.get(position).getRemoteIcon(); holder.ivIcon.setImageDrawable(view.getResources().getDrawable(icon)); 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 bf5b16ed..375c187c 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,7 @@ private void setRemote() { return; } - Collections.sort(remotes, (a, b) -> a.getName().compareTo(b.getName())); + Collections.sort(remotes, (a, b) -> a.getDisplayName().compareTo(b.getDisplayName())); String[] options = new String[remotes.size()]; int i = 0; for (RemoteItem remote : remotes) { 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 130be09e..821b834a 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,7 @@ private void setRemote() { Toasty.info(context, getString(R.string.no_remotes), Toast.LENGTH_SHORT, true).show(); return; } - Collections.sort(remotes, (a, b) -> a.getName().compareTo(b.getName())); + Collections.sort(remotes, (a, b) -> a.getDisplayName().compareTo(b.getDisplayName())); String[] options = new String[remotes.size()]; int i = 0; for (RemoteItem remote : remotes) { 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 3477a390..784aa069 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,7 @@ private void setRemote() { return; } - Collections.sort(remotes, (a, b) -> a.getName().compareTo(b.getName())); + Collections.sort(remotes, (a, b) -> a.getDisplayName().compareTo(b.getDisplayName())); String[] options = new String[remotes.size()]; int i = 0; for (RemoteItem remote : remotes) { 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 10a89beb..2ac98bc9 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,7 @@ private void addRemote() { return; } - Collections.sort(configuredRemotes, (a, b) -> a.getName().compareTo(b.getName())); + Collections.sort(configuredRemotes, (a, b) -> a.getDisplayName().compareTo(b.getDisplayName())); String[] options = new String[configuredRemotes.size()]; int i = 0; for (RemoteItem remote : configuredRemotes) { diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/SAFProvider/SAFProvider.java b/app/src/main/java/ca/pkay/rcloneexplorer/SAFProvider/SAFProvider.java index c62982fd..4a9517e0 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/SAFProvider/SAFProvider.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/SAFProvider/SAFProvider.java @@ -88,7 +88,7 @@ public Cursor queryRoots(String[] projection) { final MatrixCursor.RowBuilder row = result.newRow(); row.add(DocumentsContract.Root.COLUMN_ROOT_ID, rcxUri); - row.add(DocumentsContract.Root.COLUMN_SUMMARY, remote.getName()); + row.add(DocumentsContract.Root.COLUMN_SUMMARY, remote.getDisplayName()); row.add( DocumentsContract.Root.COLUMN_FLAGS, DocumentsContract.Root.FLAG_SUPPORTS_CREATE 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 d5178c29..787141f3 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,7 @@ private void showAppShortcutDialog() { Rclone rclone = new Rclone(context); final ArrayList remotes = new ArrayList<>(rclone.getRemotes()); - Collections.sort(remotes, (a, b) -> a.getName().compareTo(b.getName())); + Collections.sort(remotes, (a, b) -> a.getDisplayName().compareTo(b.getDisplayName())); final CharSequence[] options = new CharSequence[remotes.size()]; int i = 0; for (RemoteItem remoteItem : remotes) { From cbadca7993599ebd479cea486d48d5b20799bfb5 Mon Sep 17 00:00:00 2001 From: Gilbert Gilb's Date: Wed, 15 Jul 2020 13:39:17 +0200 Subject: [PATCH 09/15] Refresh SAF roots when remotes may have changed. --- app/build.gradle | 4 ++++ app/src/main/AndroidManifest.xml | 2 +- .../ca/pkay/rcloneexplorer/Fragments/RemotesFragment.java | 8 ++++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index bbf51d03..0b48e8e9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -29,6 +29,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 { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6c16452a..b7cc7720 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -65,7 +65,7 @@ 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 dfab6490..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; @@ -297,6 +299,12 @@ 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() { From e34132f91bac3801f719ce55344307caf30f00c6 Mon Sep 17 00:00:00 2001 From: Gilbert Gilb's Date: Wed, 15 Jul 2020 13:40:59 +0200 Subject: [PATCH 10/15] Fix encryption password not asked after updates. --- app/src/main/java/ca/pkay/rcloneexplorer/MainActivity.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/MainActivity.java b/app/src/main/java/ca/pkay/rcloneexplorer/MainActivity.java index 98792244..dc14fde3 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/MainActivity.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/MainActivity.java @@ -73,7 +73,6 @@ import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.UUID; import static ca.pkay.rcloneexplorer.ActivityHelper.tryStartActivity; import static ca.pkay.rcloneexplorer.ActivityHelper.tryStartActivityForResult; @@ -175,13 +174,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); From 96248d9947cd4f3480774bcda92631968cec68fb Mon Sep 17 00:00:00 2001 From: Gilbert Gilb's Date: Wed, 15 Jul 2020 13:47:59 +0200 Subject: [PATCH 11/15] Minor improvements to build scripts. Most importantly, 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. This commit also de-duplicates rclone build jobs for earch arch. --- app/build.gradle | 12 ++++-- rclone/build.gradle | 99 ++++++++++++++++++++------------------------- 2 files changed, 53 insertions(+), 58 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 0b48e8e9..a5387630 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,11 +8,17 @@ for (String taskName : getGradle().getStartParameter().getTaskNames()) { } } -android { - gradle.projectsEvaluated { - preBuild.dependsOn(':rclone:buildNative') +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'] diff --git a/rclone/build.gradle b/rclone/build.gradle index 5024e257..454da17f 100644 --- a/rclone/build.gradle +++ b/rclone/build.gradle @@ -1,8 +1,11 @@ import java.nio.file.Paths -def RCLONE_VERSION = project.properties['io.github.x0b.rcx.rCloneVersion'] -def RCLONE_REPOSITORY = 'github.com/rclone/rclone' -def GO_PATH = Paths.get(projectDir.absolutePath, 'gopath').toAbsolutePath().toString() +ext { + NDK_VERSION = project.properties['io.github.x0b.rcx.ndkVersion'] + RCLONE_VERSION = project.properties['io.github.x0b.rcx.rCloneVersion'] + RCLONE_REPOSITORY = 'github.com/rclone/rclone' + GO_PATH = Paths.get(projectDir.absolutePath, 'gopath').toAbsolutePath().toString() +} def findSdkDir() { def androidHome = System.getenv('ANDROID_HOME') @@ -28,20 +31,22 @@ def findSdkDir() { } def findNdkDir() { - def ndkVersion = project.properties['io.github.x0b.rcx.ndkVersion'] def sdkDir = findSdkDir() - def ndkPath = Paths.get(sdkDir, 'ndk', ndkVersion).resolve().toAbsolutePath() + def ndkPath = Paths.get(sdkDir, 'ndk', NDK_VERSION).resolve().toAbsolutePath() if (!ndkPath.toFile().exists()) { - throw new GradleException( - "Couldn't find a ndk-bundle in " + ndkPath.toString() + ". Make sure it is installed " + - "by running the command 'sdkmanager \"ndk;" + ndkVersion + "\"." - ) + 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 ndkDir = findNdkDir() def osName = System.properties['os.name'].toLowerCase() def osArch = System.properties['os.arch'] def os @@ -60,7 +65,7 @@ def getCrossCompiler(bin) { } return Paths.get( - ndkDir, + findNdkDir(), 'toolchains', 'llvm', 'prebuilt', @@ -74,15 +79,32 @@ def getLibrclone(arch) { return Paths.get('..', 'app', 'lib', arch, 'librclone.so').toString() } +def buildRclone(compiler, arch, abi, env = [:]) { + return { + doLast { + exec { + environment 'GOPATH', GO_PATH + 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"} + commandLine 'go', 'build', '-tags', 'linux', '-o', getLibrclone(abi), RCLONE_REPOSITORY + } + } + } +} + task fetchRclone(type: Exec) { mkdir GO_PATH environment 'GOPATH', GO_PATH commandLine 'go', 'get', '-d', RCLONE_REPOSITORY } -task checkoutRclone(type: Exec) { - dependsOn fetchRclone - workingDir Paths.get(GO_PATH, 'src', RCLONE_REPOSITORY) +task checkoutRclone(type: Exec, dependsOn: fetchRclone) { + workingDir Paths.get(GO_PATH, 'src', *RCLONE_REPOSITORY.split('/')) commandLine 'git', 'checkout', 'v' + RCLONE_VERSION } @@ -96,53 +118,20 @@ task cleanNative { } } -task buildArm(type: Exec) { - dependsOn checkoutRclone - environment 'GOPATH', GO_PATH - def crossCompiler = getCrossCompiler('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', getLibrclone('armeabi-v7a'), RCLONE_REPOSITORY +task buildArm(dependsOn: checkoutRclone) { + configure buildRclone('armv7a-linux-androideabi21-clang', 'arm', 'armeabi-v7a', ['GOARM': '7']) } -task buildArm64(type: Exec) { - dependsOn checkoutRclone - environment 'GOPATH', GO_PATH - def crossCompiler = getCrossCompiler('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', getLibrclone('arm64-v8a'), RCLONE_REPOSITORY +task buildArm64(dependsOn: checkoutRclone) { + configure buildRclone('aarch64-linux-android21-clang', 'arm64', 'arm64-v8a') } -task buildx86(type: Exec) { - dependsOn checkoutRclone - environment 'GOPATH', GO_PATH - def crossCompiler = getCrossCompiler('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', getLibrclone('x86'), RCLONE_REPOSITORY +task buildx86(dependsOn: checkoutRclone) { + configure buildRclone('i686-linux-android21-clang', '386', 'x86') } -task buildx64(type: Exec) { - dependsOn checkoutRclone - environment 'GOPATH', GO_PATH - def crossCompiler = getCrossCompiler('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', getLibrclone('x86_64'), RCLONE_REPOSITORY +task buildx64(dependsOn: checkoutRclone) { + configure buildRclone('x86_64-linux-android21-clang', 'amd64', 'x86_64') } task buildNative { From 6bcca4c7fc41e58225bbc87b4c2be178ef60be2e Mon Sep 17 00:00:00 2001 From: Gilbert Gilb's Date: Wed, 15 Jul 2020 21:44:15 +0200 Subject: [PATCH 12/15] commons-io is no longer necessary. --- app/build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index a5387630..84e111c8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -124,7 +124,6 @@ dependencies { implementation 'org.markdownj:markdownj-core:0.4' implementation 'jp.wasabeef:recyclerview-animators:2.3.0' implementation 'com.github.GrenderG:Toasty:1.3.0' - implementation 'commons-io:commons-io:2.7' // Firebase & Crashlytics rcxImplementation 'com.google.firebase:firebase-analytics:17.3.0' rcxImplementation 'com.google.firebase:firebase-crashlytics:17.0.0-beta03' From 9beaaaf536d9a7b2bf3985fa7fc93e9af716c342 Mon Sep 17 00:00:00 2001 From: Gilbert Gilb's Date: Wed, 15 Jul 2020 22:09:38 +0200 Subject: [PATCH 13/15] Fixes for renamed remotes. --- .../ca/pkay/rcloneexplorer/MainActivity.java | 13 ++++++++----- .../java/ca/pkay/rcloneexplorer/Rclone.java | 18 ------------------ .../RemoteConfig/AliasConfig.java | 2 +- .../RemoteConfig/CacheConfig.java | 2 +- .../RemoteConfig/CryptConfig.java | 2 +- .../RemoteConfig/UnionConfig.java | 2 +- .../Settings/GeneralSettingsFragment.java | 2 +- 7 files changed, 13 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/MainActivity.java b/app/src/main/java/ca/pkay/rcloneexplorer/MainActivity.java index dc14fde3..895b1305 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/MainActivity.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/MainActivity.java @@ -73,6 +73,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.UUID; import static ca.pkay.rcloneexplorer.ActivityHelper.tryStartActivity; import static ca.pkay.rcloneexplorer.ActivityHelper.tryStartActivityForResult; @@ -742,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() @@ -815,18 +816,19 @@ private File getVolumeRoot(File file) { private void addLocalRemote(File root) throws IOException { SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context); String name = root.getCanonicalPath(); + 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); String description = storageVolume != null ? storageVolume.getDescription(context) : null; if (description != null) { - name = rclone.getUniqueRemoteName(description); + name = description; } } String path = root.getAbsolutePath(); ArrayList options = new ArrayList<>(); - options.add(name); + options.add(id); options.add("alias"); options.add("remote"); options.add(path); @@ -842,10 +844,11 @@ private void addLocalRemote(File root) throws IOException { FLog.e(TAG, "addLocalRemote: process error", e); return; } + 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<>()); - pinnedRemotes.add(name); - generatedRemotes.add(name); + pinnedRemotes.add(id); + generatedRemotes.add(id); pref.edit() .putStringSet(getString(R.string.shared_preferences_drawer_pinned_remotes), pinnedRemotes) .putStringSet(getString(R.string.pref_key_local_alias_remotes), generatedRemotes) diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/Rclone.java b/app/src/main/java/ca/pkay/rcloneexplorer/Rclone.java index f07b2600..8dad14f1 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/Rclone.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/Rclone.java @@ -19,7 +19,6 @@ import io.github.x0b.safdav.SafDAVServer; import io.github.x0b.safdav.file.SafConstants; -import org.apache.commons.io.FileUtils; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -1186,23 +1185,6 @@ public void renameRemote(String remoteName, String displayName) { .apply(); } - public String getUniqueRemoteName(String desiredName) { - List remotes = getRemotes(); - Set remoteNames = new HashSet<>(remotes.size()); - for (RemoteItem remoteItem : remotes) { - remoteNames.add(remoteItem.getName()); - } - if (!remoteNames.contains(desiredName)) { - return desiredName; - } - for (int i = 1;;++i) { - String remoteName = desiredName + " " + i; - if (!remoteNames.contains(remoteName)) { - return remoteName; - } - } - } - /** * 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 375c187c..d471cad8 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/RemoteConfig/AliasConfig.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/RemoteConfig/AliasConfig.java @@ -135,7 +135,7 @@ private void setRemote() { String[] options = new String[remotes.size()]; int i = 0; for (RemoteItem remote : remotes) { - options[i++] = remote.getName(); + options[i++] = remote.getDisplayName(); } AlertDialog.Builder builder; 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 821b834a..8e95b04d 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/RemoteConfig/CacheConfig.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/RemoteConfig/CacheConfig.java @@ -290,7 +290,7 @@ private void setRemote() { String[] options = new String[remotes.size()]; int i = 0; for (RemoteItem remote : remotes) { - options[i++] = remote.getName(); + options[i++] = remote.getDisplayName(); } AlertDialog.Builder builder; 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 784aa069..bbc6378f 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/RemoteConfig/CryptConfig.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/RemoteConfig/CryptConfig.java @@ -293,7 +293,7 @@ private void setRemote() { String[] options = new String[remotes.size()]; int i = 0; for (RemoteItem remote : remotes) { - options[i++] = remote.getName(); + options[i++] = remote.getDisplayName(); } AlertDialog.Builder builder; 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 2ac98bc9..f04e25d2 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/RemoteConfig/UnionConfig.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/RemoteConfig/UnionConfig.java @@ -126,7 +126,7 @@ private void addRemote() { String[] options = new String[configuredRemotes.size()]; int i = 0; for (RemoteItem remote : configuredRemotes) { - options[i++] = remote.getName(); + options[i++] = remote.getDisplayName(); } AlertDialog.Builder builder; 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 787141f3..40b1fa06 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/Settings/GeneralSettingsFragment.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/Settings/GeneralSettingsFragment.java @@ -219,7 +219,7 @@ private void showAppShortcutDialog() { final CharSequence[] options = new CharSequence[remotes.size()]; int i = 0; for (RemoteItem remoteItem : remotes) { - options[i++] = remoteItem.getName(); + options[i++] = remoteItem.getDisplayName(); } final ArrayList userSelected = new ArrayList<>(); From ef1a36898678d4f8de50ec75ebc09f735bf7ce60 Mon Sep 17 00:00:00 2001 From: Gilbert Gilb's Date: Wed, 15 Jul 2020 22:15:38 +0200 Subject: [PATCH 14/15] Remove useless comment. --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index a818c90d..2ce2db62 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,7 @@ buildscript { for (String taskName : getGradle().getStartParameter().getTaskNames()) { if (taskName.endsWith('RcxDebug') || taskName.endsWith('RcxRelease')) { - classpath 'com.google.gms:google-services:4.3.3' // google-services plugin + classpath 'com.google.gms:google-services:4.3.3' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.0.0-beta03' break } From ff4c5ce693120fbf019c08471edae816f429b7ac Mon Sep 17 00:00:00 2001 From: Gilbert Gilb's Date: Wed, 12 Aug 2020 21:51:20 +0200 Subject: [PATCH 15/15] Update dependencies and build RClone in a go module. This commit updates build tools, android gradle plugin and RClone to their latest version. Also, the RClone build recipe now creates a go module instead of git. his will make sure dependencies are pinned to the version expected by RClone. --- .github/workflows/android.yml | 4 ++- .gitignore | 3 ++ .../RemoteConfig/DriveConfig.java | 3 -- build.gradle | 2 +- gradle.properties | 4 +-- rclone/.gitignore | 2 +- rclone/build.gradle | 29 ++++++++++++------- 7 files changed, 28 insertions(+), 19 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index a326d2dd..59e00ed7 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -27,7 +27,9 @@ jobs: 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" "build-tools;${BUILD_TOOLS_VERSION}" "ndk;${NDK_VERSION}" + + 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/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/build.gradle b/build.gradle index 2ce2db62..8af4fed9 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:4.0.0' + classpath 'com.android.tools.build:gradle:4.0.1' for (String taskName : getGradle().getStartParameter().getTaskNames()) { if (taskName.endsWith('RcxDebug') || taskName.endsWith('RcxRelease')) { diff --git a/gradle.properties b/gradle.properties index f646f09b..ccb5d7ec 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,8 +14,8 @@ android.useAndroidX=true # android.enableR8=true org.gradle.jvmargs=-Xmx1536m -io.github.x0b.rcx.rCloneVersion=1.51.0 -io.github.x0b.rcx.buildToolsVersion=30.0.1 +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. 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 454da17f..1349b105 100644 --- a/rclone/build.gradle +++ b/rclone/build.gradle @@ -3,8 +3,9 @@ import java.nio.file.Paths ext { NDK_VERSION = project.properties['io.github.x0b.rcx.ndkVersion'] RCLONE_VERSION = project.properties['io.github.x0b.rcx.rCloneVersion'] - RCLONE_REPOSITORY = 'github.com/rclone/rclone' - GO_PATH = Paths.get(projectDir.absolutePath, 'gopath').toAbsolutePath().toString() + RCLONE_MODULE = 'github.com/rclone/rclone' + CACHE_PATH = Paths.get(projectDir.absolutePath, 'cache').toAbsolutePath().toString() + GOPATH = Paths.get(CACHE_PATH, "gopath").toString() } def findSdkDir() { @@ -83,7 +84,7 @@ def buildRclone(compiler, arch, abi, env = [:]) { return { doLast { exec { - environment 'GOPATH', GO_PATH + environment 'GOPATH', GOPATH def crossCompiler = getCrossCompiler(compiler) environment 'CC', crossCompiler environment 'CC_FOR_TARGET', crossCompiler @@ -91,21 +92,27 @@ def buildRclone(compiler, arch, abi, env = [:]) { environment 'GOARCH', arch environment 'CGO_ENABLED', '1' env.each {entry -> environment "$entry.key", "$entry.value"} - commandLine 'go', 'build', '-tags', 'linux', '-o', getLibrclone(abi), RCLONE_REPOSITORY + workingDir CACHE_PATH + commandLine 'go', 'build', '-tags', 'linux', '-o', getLibrclone(abi), RCLONE_MODULE } } } } -task fetchRclone(type: Exec) { - mkdir GO_PATH - environment 'GOPATH', GO_PATH - commandLine 'go', 'get', '-d', RCLONE_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(GO_PATH, 'src', *RCLONE_REPOSITORY.split('/')) - commandLine 'git', 'checkout', 'v' + RCLONE_VERSION +task checkoutRclone(type: Exec, dependsOn: createRcloneModule) { + workingDir CACHE_PATH + environment 'GOPATH', GOPATH + commandLine 'go', 'get', '-v', '-d', "${RCLONE_MODULE}@v${RCLONE_VERSION}" } task cleanNative {