From a59c48553af80a4517827e9a730d8e946ad5b3c0 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 15 Jul 2019 10:44:26 +0300 Subject: [PATCH 001/184] Update README.md --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 511c940276..f1b7d7bd3b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ # Kuroba - imageboard browser for Android -## This project is finished as of July 14th, 2019. [APK releases](https://github.com/Adamantcheese/Kuroba/releases) Kuroba is a fast Android app for browsing imageboards, such as 4chan and 8chan. It adds inline replying, thread watching, notifications, themes, pass support, filters and a whole lot more. It is based on Clover by Floens, but has additional features added in because Floens doesn't want to merge PRs. Credits to K1rakishou for a number of features. From 3fb60638f572a5062ca1c092e652287ee0abda72 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 15 Jul 2019 10:46:45 +0300 Subject: [PATCH 002/184] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f1b7d7bd3b..843045848e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ # Kuroba - imageboard browser for Android [APK releases](https://github.com/Adamantcheese/Kuroba/releases) -Kuroba is a fast Android app for browsing imageboards, such as 4chan and 8chan. It adds inline replying, thread watching, notifications, themes, pass support, filters and a whole lot more. It is based on Clover by Floens, but has additional features added in because Floens doesn't want to merge PRs. Credits to K1rakishou for a number of features. +Kuroba is a fast Android app for browsing imageboards, such as 4chan and 8chan. It adds inline replying, thread watching, notifications, themes, pass support, filters and a whole lot more. It is based on Clover by Floens, but has additional features added in because Floens doesn't want to merge PRs. +Big thanks to Adamantcheese for maintaining the project up until now. ## License Kuroba is [GPLv3](https://github.com/Adamantcheese/Kuroba/blob/multi-feature/COPYING.txt), [licenses of the used libraries](https://github.com/Adamantcheese/Kuroba/blob/multi-feature/Kuroba/app/src/main/assets/html/licenses.html). From 72270df91cd47932a0003c477060036c09e9da36 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 15 Jul 2019 10:47:07 +0300 Subject: [PATCH 003/184] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 843045848e..42befb9a15 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [APK releases](https://github.com/Adamantcheese/Kuroba/releases) Kuroba is a fast Android app for browsing imageboards, such as 4chan and 8chan. It adds inline replying, thread watching, notifications, themes, pass support, filters and a whole lot more. It is based on Clover by Floens, but has additional features added in because Floens doesn't want to merge PRs. + Big thanks to Adamantcheese for maintaining the project up until now. ## License From 48ca131cfe189c3701b2581d8d4a9ba6ef9cf2a4 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 15 Jul 2019 10:48:46 +0300 Subject: [PATCH 004/184] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 42befb9a15..a2c4d178bf 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Kuroba is a fast Android app for browsing imageboards, such as 4chan and 8chan. It adds inline replying, thread watching, notifications, themes, pass support, filters and a whole lot more. It is based on Clover by Floens, but has additional features added in because Floens doesn't want to merge PRs. -Big thanks to Adamantcheese for maintaining the project up until now. +Big thanks to Adamantcheese for maintaining the project up until now. I guess it's my turn now. ## License Kuroba is [GPLv3](https://github.com/Adamantcheese/Kuroba/blob/multi-feature/COPYING.txt), [licenses of the used libraries](https://github.com/Adamantcheese/Kuroba/blob/multi-feature/Kuroba/app/src/main/assets/html/licenses.html). From 1604d021b04f1e428fd2ced9d1b4d61bf27086db Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 15 Jul 2019 10:50:14 +0300 Subject: [PATCH 005/184] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a2c4d178bf..082562970e 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Kuroba is a fast Android app for browsing imageboards, such as 4chan and 8chan. It adds inline replying, thread watching, notifications, themes, pass support, filters and a whole lot more. It is based on Clover by Floens, but has additional features added in because Floens doesn't want to merge PRs. -Big thanks to Adamantcheese for maintaining the project up until now. I guess it's my turn now. +Big thanks to Adamantcheese for maintaining the project up until 15.07.19. I guess it's my turn now. ## License Kuroba is [GPLv3](https://github.com/Adamantcheese/Kuroba/blob/multi-feature/COPYING.txt), [licenses of the used libraries](https://github.com/Adamantcheese/Kuroba/blob/multi-feature/Kuroba/app/src/main/assets/html/licenses.html). From ce880e86b32244c2396b30cf96218c012b82a625 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Mon, 15 Jul 2019 21:00:00 +0300 Subject: [PATCH 006/184] Introduce travis CI --- .travis.yml | 15 +++++++++++++ Kuroba/.travis.yml | 30 +++++++++++++++++++++++++ Kuroba/app/build.gradle | 28 +++++++++++++++++++++-- Kuroba/app/src/main/AndroidManifest.xml | 2 +- 4 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 .travis.yml create mode 100644 Kuroba/.travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000000..b5304e904d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,15 @@ +sudo: false +language: android +jdk: + - oraclejdk8 +before_install: + - cd Kuroba && chmod +x gradlew +android: + components: + - platform-tools + - tools + - extra-android-m2repository + - build-tools-28.0.3 + - android-28 + +script: ./gradlew build --console plain -x lint diff --git a/Kuroba/.travis.yml b/Kuroba/.travis.yml new file mode 100644 index 0000000000..39f174856b --- /dev/null +++ b/Kuroba/.travis.yml @@ -0,0 +1,30 @@ +language: android +sudo: false +jdk: oraclejdk8 + +before_cache: + -rm -f $HOME/.gradle/caches/modules-2/modules-2.lock + -rm -fr $HOME/.gradle/caches/*/plugin-resolution/ +cache: + directories: + -$HOME/.gradle/caches/ + -$HOME/.gradle/wrapper/ + +env: + global: + - ANDROID_API=28 + - EMULATOR_API=21 + - ANDROID_BUILD_TOOLS=28.0.3 + +android: + components: + - tools + - platform-tools + - extra-android-m2repository + - build-tools-$ANDROID_BUILD_TOOLS + - android-$ANDROID_API + +script: ./gradlew build + +notifications: + email: false \ No newline at end of file diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 4a4cec53e7..b6997d9d3c 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -34,8 +34,6 @@ android { */ //if you change this, also change the AndroidManifest package applicationId "com.github.adamantcheese.chan" - //these are manifest placeholders for the application name and icon location - manifestPlaceholders = [appName: "Kuroba", iconLoc: "@mipmap/ic_launcher"] //these are your update and github repo endpoints, change it to your repository //the repo endpoint is also used to calculate the issues endpoint buildConfigField "String", "UPDATE_API_ENDPOINT", "\"https://api.github.com/repos/Adamantcheese/Kuroba/releases/latest\"" @@ -128,6 +126,32 @@ android { debuggable = true } } + + flavorDimensions "default" + + productFlavors { + stable { + dimension "default" + //these are manifest placeholders for the application name and icon location + manifestPlaceholders = [ + appName: "Kuroba", + iconLoc: "@mipmap/ic_launcher", + fileProviderAuthority:"${applicationIdSuffix}.fileprovider" + ] + } + dev { + dimension "default" + applicationIdSuffix ".dev" + versionNameSuffix "-dev" + + //these are manifest placeholders for the application name and icon location + manifestPlaceholders = [ + appName: "Kuroba${versionNameSuffix}", + iconLoc: "@mipmap/ic_launcher", + fileProviderAuthority:"${applicationIdSuffix}.fileprovider" + ] + } + } } dependencies { diff --git a/Kuroba/app/src/main/AndroidManifest.xml b/Kuroba/app/src/main/AndroidManifest.xml index 8555bb4808..25c43ca977 100644 --- a/Kuroba/app/src/main/AndroidManifest.xml +++ b/Kuroba/app/src/main/AndroidManifest.xml @@ -104,7 +104,7 @@ along with this program. If not, see . From 3ab6b25e24b7e647fc3e9fe738213d6221a27787 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 15 Jul 2019 21:45:44 +0300 Subject: [PATCH 007/184] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 082562970e..410a0b5973 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[![Build Status](https://travis-ci.org/K1rakishou/Kuroba.svg?branch=multi-feature)](https://travis-ci.org/K1rakishou/Kuroba) + # Kuroba - imageboard browser for Android [APK releases](https://github.com/Adamantcheese/Kuroba/releases) From c12c6863d3f0ff01b5b7ea971157a54ce1c9e24e Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Tue, 16 Jul 2019 21:04:38 +0300 Subject: [PATCH 008/184] CI apk uploading --- Kuroba/.travis.yml | 30 ------------------------------ Kuroba/scripts/update-apk.sh | 19 +++++++++++++++++++ 2 files changed, 19 insertions(+), 30 deletions(-) delete mode 100644 Kuroba/.travis.yml create mode 100644 Kuroba/scripts/update-apk.sh diff --git a/Kuroba/.travis.yml b/Kuroba/.travis.yml deleted file mode 100644 index 39f174856b..0000000000 --- a/Kuroba/.travis.yml +++ /dev/null @@ -1,30 +0,0 @@ -language: android -sudo: false -jdk: oraclejdk8 - -before_cache: - -rm -f $HOME/.gradle/caches/modules-2/modules-2.lock - -rm -fr $HOME/.gradle/caches/*/plugin-resolution/ -cache: - directories: - -$HOME/.gradle/caches/ - -$HOME/.gradle/wrapper/ - -env: - global: - - ANDROID_API=28 - - EMULATOR_API=21 - - ANDROID_BUILD_TOOLS=28.0.3 - -android: - components: - - tools - - platform-tools - - extra-android-m2repository - - build-tools-$ANDROID_BUILD_TOOLS - - android-$ANDROID_API - -script: ./gradlew build - -notifications: - email: false \ No newline at end of file diff --git a/Kuroba/scripts/update-apk.sh b/Kuroba/scripts/update-apk.sh new file mode 100644 index 0000000000..5cb5168d83 --- /dev/null +++ b/Kuroba/scripts/update-apk.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +git config --global user.name "Travis CI" +git config --global user.email "noreply+travis@fossasia.org" + +git clone --quiet --branch=apk https://fossasia:$GITHUB_API_KEY@github.com/fossasia/open-event-android apk > /dev/null +cd apk +\cp -r ../app/build/outputs/apk/*/**.apk . +\cp -r ../app/build/outputs/apk/debug/output.json debug-output.json +\cp -r ../app/build/outputs/apk/release/output.json release-output.json + +git checkout --orphan temporary + +git add --all . +git commit -am "[Auto] Update Test Apk ($(date +%Y-%m-%d.%H:%M:%S))" + +git branch -D apk +git branch -m apk + +git push origin apk --force --quiet > /dev/null \ No newline at end of file From 725f752a1f902926c29b504ad0306411ef6bf20c Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Tue, 16 Jul 2019 21:15:00 +0300 Subject: [PATCH 009/184] Trigger CI build --- .travis.yml | 3 +++ Kuroba/scripts/update-apk.sh | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b5304e904d..03d5ca78c3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,3 +13,6 @@ android: - android-28 script: ./gradlew build --console plain -x lint + +after_success: + - bash scripts/update-apk.sh \ No newline at end of file diff --git a/Kuroba/scripts/update-apk.sh b/Kuroba/scripts/update-apk.sh index 5cb5168d83..95cbb6d641 100644 --- a/Kuroba/scripts/update-apk.sh +++ b/Kuroba/scripts/update-apk.sh @@ -16,4 +16,4 @@ git commit -am "[Auto] Update Test Apk ($(date +%Y-%m-%d.%H:%M:%S))" git branch -D apk git branch -m apk -git push origin apk --force --quiet > /dev/null \ No newline at end of file +git push origin apk --force --quiet > /dev/null \ No newline at end of file From abbf9ade1fad68373ab91638ac1e8c598327bda5 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Tue, 16 Jul 2019 21:39:54 +0300 Subject: [PATCH 010/184] Trigger CI build --- Kuroba/scripts/update-apk.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Kuroba/scripts/update-apk.sh b/Kuroba/scripts/update-apk.sh index 95cbb6d641..72979d56d4 100644 --- a/Kuroba/scripts/update-apk.sh +++ b/Kuroba/scripts/update-apk.sh @@ -2,11 +2,11 @@ git config --global user.name "Travis CI" git config --global user.email "noreply+travis@fossasia.org" -git clone --quiet --branch=apk https://fossasia:$GITHUB_API_KEY@github.com/fossasia/open-event-android apk > /dev/null +git clone --quiet --branch=apk https://github.com/K1rakishou/Kuroba.git apk > /dev/null cd apk -\cp -r ../app/build/outputs/apk/*/**.apk . -\cp -r ../app/build/outputs/apk/debug/output.json debug-output.json -\cp -r ../app/build/outputs/apk/release/output.json release-output.json +\cp -r ../*/**.apk . +\cp -r ../debug/output.json debug-output.json +\cp -r ../release/output.json release-output.json git checkout --orphan temporary @@ -16,4 +16,4 @@ git commit -am "[Auto] Update Test Apk ($(date +%Y-%m-%d.%H:%M:%S))" git branch -D apk git branch -m apk -git push origin apk --force --quiet > /dev/null \ No newline at end of file +git push origin apk --force --quiet > /dev/null \ No newline at end of file From 6019ab13ed8dc814ffabbdc76902eb07e2332f30 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Sat, 20 Jul 2019 08:25:30 +0300 Subject: [PATCH 011/184] Update README.md --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index b0707dc502..f81431d93c 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,5 @@ Kuroba is a fast Android app for browsing imageboards, such as 4chan and 8chan. It adds inline replying, thread watching, notifications, themes, pass support, filters and a whole lot more. It is based on Clover by Floens, but has additional features added in because Floens doesn't want to merge PRs. -Big thanks to Adamantcheese for maintaining the project up until 15.07.19. I guess it's my turn now. - ## License [Kuroba is GPLv3](https://github.com/Adamantcheese/Kuroba/blob/multi-feature/COPYING.txt), [licenses of the used libraries.](https://github.com/Adamantcheese/Kuroba/blob/multi-feature/Kuroba/app/src/main/assets/html/licenses.html). From e42adbd1f128a562908cec86eb891294ac26215e Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 10 Aug 2019 21:26:22 +0300 Subject: [PATCH 012/184] (#172) SAF base API implementation --- Kuroba/app/build.gradle | 4 + .../adamantcheese/chan/StartActivity.java | 34 +++- .../adamantcheese/chan/core/Extensions.kt | 21 +++ .../adamantcheese/chan/core/di/AppModule.java | 7 + .../chan/core/saf/FileChooser.kt | 167 ++++++++++++++++++ .../chan/core/saf/FileManager.kt | 72 ++++++++ .../chan/core/saf/callback/ChooserCallback.kt | 8 + .../saf/callback/DirectoryChooserCallback.kt | 3 + .../core/saf/callback/FileChooserCallback.kt | 3 + .../saf/callback/StartActivityCallbacks.kt | 7 + .../chan/core/saf/file/AbstractFile.kt | 77 ++++++++ .../chan/core/saf/file/ExternalFile.kt | 160 +++++++++++++++++ .../chan/core/saf/file/RawFile.kt | 106 +++++++++++ .../chan/core/settings/ChanSettings.java | 4 + .../controller/MediaSettingsController.java | 88 +++++++-- .../ui/controller/SaveLocationController.java | 149 ---------------- .../chan/ui/helper/ImagePickDelegate.java | 59 ++++--- Kuroba/build.gradle | 3 + 18 files changed, 784 insertions(+), 188 deletions(-) create mode 100644 Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/Extensions.kt create mode 100644 Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileChooser.kt create mode 100644 Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt create mode 100644 Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/callback/ChooserCallback.kt create mode 100644 Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/callback/DirectoryChooserCallback.kt create mode 100644 Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/callback/FileChooserCallback.kt create mode 100644 Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/callback/StartActivityCallbacks.kt create mode 100644 Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt create mode 100644 Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt create mode 100644 Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt delete mode 100644 Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/SaveLocationController.java diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 5704198b7d..4117407367 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -1,4 +1,7 @@ apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-kapt' android { compileSdkVersion 28 @@ -154,4 +157,5 @@ dependencies { implementation 'com.vdurmont:emoji-java:4.0.0' implementation 'io.reactivex.rxjava2:rxjava:2.2.9' implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/StartActivity.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/StartActivity.java index ff351401ef..d67f746f13 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/StartActivity.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/StartActivity.java @@ -45,6 +45,8 @@ import com.github.adamantcheese.chan.core.model.orm.Loadable; import com.github.adamantcheese.chan.core.model.orm.Pin; import com.github.adamantcheese.chan.core.repository.SiteRepository; +import com.github.adamantcheese.chan.core.saf.FileManager; +import com.github.adamantcheese.chan.core.saf.callback.StartActivityCallbacks; import com.github.adamantcheese.chan.core.settings.ChanSettings; import com.github.adamantcheese.chan.core.site.Site; import com.github.adamantcheese.chan.core.site.SiteResolver; @@ -62,6 +64,8 @@ import com.github.adamantcheese.chan.utils.AndroidUtils; import com.github.adamantcheese.chan.utils.Logger; +import org.jetbrains.annotations.NotNull; + import java.io.PrintWriter; import java.io.StringWriter; import java.util.ArrayList; @@ -73,7 +77,10 @@ import static com.github.adamantcheese.chan.Chan.inject; import static com.github.adamantcheese.chan.utils.AndroidUtils.getApplicationLabel; -public class StartActivity extends AppCompatActivity implements NfcAdapter.CreateNdefMessageCallback { +public class StartActivity + extends AppCompatActivity + implements NfcAdapter.CreateNdefMessageCallback, + StartActivityCallbacks { private static final String TAG = "StartActivity"; private static final String STATE_KEY = "chan_state"; @@ -104,6 +111,9 @@ public class StartActivity extends AppCompatActivity implements NfcAdapter.Creat @Inject SiteService siteService; + @Inject + FileManager fileManager; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -115,6 +125,8 @@ protected void onCreate(Bundle savedInstanceState) { Chan.injector().instance(ThemeHelper.class).setupContext(this); + fileManager.setCallbacks(this); + imagePickDelegate = new ImagePickDelegate(this); runtimePermissionsHelper = new RuntimePermissionsHelper(this); updateManager = new UpdateManager(this); @@ -534,6 +546,8 @@ protected void onDestroy() { return; } + fileManager.removeCallbacks(); + // TODO: clear whole stack? stackTop().onHide(); stackTop().onDestroy(); @@ -544,7 +558,13 @@ protected void onDestroy() { protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); - imagePickDelegate.onActivityResult(requestCode, resultCode, data); + if (fileManager.onActivityResult(requestCode, resultCode, data)) { + return; + } + + if (imagePickDelegate.onActivityResult(requestCode, resultCode, data)) { + return; + } } private Controller stackTop() { @@ -581,4 +601,14 @@ public void restartApp() { Runtime.getRuntime().exit(0); } + + @Override + public boolean myStartActivityForResult(@NotNull Intent intent, int requestCode) { + if (intent.resolveActivity(getPackageManager()) == null) { + return false; + } + + startActivityForResult(intent, requestCode); + return true; + } } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/Extensions.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/Extensions.kt new file mode 100644 index 0000000000..ae2b8ac862 --- /dev/null +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/Extensions.kt @@ -0,0 +1,21 @@ +package com.github.adamantcheese.chan.core + +import android.net.Uri + + +fun String.extension(): String? { + val index = this.indexOfLast { ch -> ch == '.' } + if (index == -1) { + return null + } + + return this.substring(index + 1) +} + +fun Uri.Builder.appendManyEncoded(segments: List): Uri.Builder { + for (segment in segments) { + this.appendEncodedPath(segment) + } + + return this +} \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/AppModule.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/AppModule.java index 981fc0f73c..e5e56d8f08 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/AppModule.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/AppModule.java @@ -23,6 +23,7 @@ import com.android.volley.toolbox.ImageLoader; import com.github.adamantcheese.chan.core.image.ImageLoaderV2; import com.github.adamantcheese.chan.core.net.BitmapLruImageCache; +import com.github.adamantcheese.chan.core.saf.FileManager; import com.github.adamantcheese.chan.core.saver.ImageSaver; import com.github.adamantcheese.chan.ui.captcha.CaptchaHolder; import com.github.adamantcheese.chan.ui.theme.ThemeHelper; @@ -85,4 +86,10 @@ public ImageSaver provideImageSaver() { public CaptchaHolder provideCaptchaHolder() { return new CaptchaHolder(); } + + @Provides + @Singleton + public FileManager provideFileManager() { + return new FileManager(applicationContext); + } } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileChooser.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileChooser.kt new file mode 100644 index 0000000000..eb3cc5a0d4 --- /dev/null +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileChooser.kt @@ -0,0 +1,167 @@ +package com.github.adamantcheese.chan.core.saf + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.provider.DocumentsContract +import com.github.adamantcheese.chan.core.saf.callback.ChooserCallback +import com.github.adamantcheese.chan.core.saf.callback.DirectoryChooserCallback +import com.github.adamantcheese.chan.core.saf.callback.FileChooserCallback +import com.github.adamantcheese.chan.core.saf.callback.StartActivityCallbacks +import com.github.adamantcheese.chan.utils.Logger + +internal class FileChooser( + private val appContext: Context +) { + private val callbacksMap = hashMapOf() + + private var requestCode = 10000 + private var startActivityCallbacks: StartActivityCallbacks? = null + + internal fun setCallbacks(startActivityCallbacks: StartActivityCallbacks) { + this.startActivityCallbacks = startActivityCallbacks + } + + internal fun removeCallbacks() { + this.startActivityCallbacks = null + } + + internal fun openChooseDirectoryDialog(directoryChooserCallback: DirectoryChooserCallback): Boolean { + return startActivityCallbacks?.let { callbacks -> + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + intent.addFlags( + Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or + Intent.FLAG_GRANT_READ_URI_PERMISSION or + Intent.FLAG_GRANT_WRITE_URI_PERMISSION or + Intent.FLAG_GRANT_PREFIX_URI_PERMISSION + ) + + val nextRequestCode = ++requestCode + callbacksMap[nextRequestCode] = directoryChooserCallback as ChooserCallback + + return@let callbacks.myStartActivityForResult(intent, nextRequestCode) + } ?: false + } + + internal fun openChooseFileDialog(fileChooserCallback: FileChooserCallback): Boolean { + return startActivityCallbacks?.let { callbacks -> + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) + intent.addFlags( + Intent.FLAG_GRANT_READ_URI_PERMISSION or + Intent.FLAG_GRANT_WRITE_URI_PERMISSION + ) + + intent.addCategory(Intent.CATEGORY_OPENABLE) + + val nextRequestCode = ++requestCode + callbacksMap[nextRequestCode] = fileChooserCallback as ChooserCallback + + return@let callbacks.myStartActivityForResult(intent, nextRequestCode) + } ?: false + } + + internal fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { + if (!callbacksMap.containsKey(requestCode)) { + return false + } + + try { + val callback = callbacksMap[requestCode] + if (callback == null) { + Logger.d(TAG, "Callback is already removed from the map") + return false + } + + if (startActivityCallbacks == null) { + // Skip all requests when the callback is not set + return false + } + + when (callback) { + is DirectoryChooserCallback -> { + handleDirectoryChooserCallback(callback, resultCode, data) + } + is FileChooserCallback -> { + handleFileChooserCallback(callback, resultCode, data) + } + } + + return true + } finally { + callbacksMap.remove(requestCode) + } + } + + private fun handleFileChooserCallback( + callback: FileChooserCallback, + resultCode: Int, + intent: Intent? + ) { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + private fun handleDirectoryChooserCallback( + callback: DirectoryChooserCallback, + resultCode: Int, + intent: Intent? + ) { + if (resultCode != Activity.RESULT_OK) { + val msg = "Non OK result ($resultCode)" + + Logger.e(TAG, msg) + callback.onCancel(msg) + return + } + + if (intent == null) { + val msg = "Intent is null" + + Logger.e(TAG, msg) + callback.onCancel(msg) + return + } + + val read = (intent.flags and Intent.FLAG_GRANT_READ_URI_PERMISSION) != 0 + val write = (intent.flags and Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0 + + if (!read) { + val msg = "No grant read uri permission given" + + Logger.e(TAG, msg) + callback.onCancel(msg) + return + } + + if (!write) { + val msg = "No grant write uri permission given" + + Logger.e(TAG, msg) + callback.onCancel(msg) + return + } + + val uri = intent.data + if (uri == null) { + val msg = "intent.getData() == null" + + Logger.e(TAG, msg) + callback.onCancel(msg) + return + } + + val documentId = DocumentsContract.getTreeDocumentId(uri) + val treeDocumentUri = DocumentsContract.buildDocumentUriUsingTree(uri, documentId) + + val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or + Intent.FLAG_GRANT_WRITE_URI_PERMISSION + + val contentResolver = appContext.contentResolver + contentResolver.takePersistableUriPermission(uri, flags) + + callback.onResult(treeDocumentUri) + } + + companion object { + private const val TAG = "FileChooser" + } +} \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt new file mode 100644 index 0000000000..5e7d30e602 --- /dev/null +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt @@ -0,0 +1,72 @@ +package com.github.adamantcheese.chan.core.saf + +import android.content.Context +import android.content.Intent +import android.net.Uri +import com.github.adamantcheese.chan.core.saf.callback.DirectoryChooserCallback +import com.github.adamantcheese.chan.core.saf.callback.FileChooserCallback +import com.github.adamantcheese.chan.core.saf.callback.StartActivityCallbacks +import com.github.adamantcheese.chan.core.saf.file.AbstractFile +import com.github.adamantcheese.chan.core.saf.file.ExternalFile +import com.github.adamantcheese.chan.core.saf.file.RawFile +import java.io.File + +class FileManager( + private val appContext: Context +) { + private val fileChooser = FileChooser(appContext) + + fun setCallbacks(startActivityCallbacks: StartActivityCallbacks) { + fileChooser.setCallbacks(startActivityCallbacks) + } + + fun removeCallbacks() { + fileChooser.removeCallbacks() + } + + //======================================================= + // Api to open file/directory chooser and handling the result + //======================================================= + + fun openChooseDirectoryDialog(callback: DirectoryChooserCallback): Boolean { + return fileChooser.openChooseDirectoryDialog(callback) + } + + fun openChooseFileDialog(callback: FileChooserCallback): Boolean { + return fileChooser.openChooseFileDialog(callback) + } + + fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { + return fileChooser.onActivityResult(requestCode, resultCode, data) + } + + //======================================================= + // Api to convert native file/documentFile classes into our own abstractions + //======================================================= + + /** + * Create a raw file from a path + * */ + fun fromPath(path: String): RawFile { + return fromRawFile(File(path)) + } + + /** + * Create RawFile from Java File + * */ + fun fromRawFile(file: File): RawFile { + if (file.isFile) { + return RawFile(AbstractFile.Root.FileRoot(file, file.name)) + } + + return RawFile(AbstractFile.Root.DirRoot(file)) + } + + /** + * Create an external file from Uri + * */ + fun fromUri(uri: Uri): ExternalFile? { + // FIXME: Uri must be a directory!!! An additional check is needed here! + return ExternalFile(appContext, AbstractFile.Root.DirRoot(uri)) + } +} \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/callback/ChooserCallback.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/callback/ChooserCallback.kt new file mode 100644 index 0000000000..472b72b98c --- /dev/null +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/callback/ChooserCallback.kt @@ -0,0 +1,8 @@ +package com.github.adamantcheese.chan.core.saf.callback + +import android.net.Uri + +interface ChooserCallback { + fun onResult(uri: Uri) + fun onCancel(reason: String) +} \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/callback/DirectoryChooserCallback.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/callback/DirectoryChooserCallback.kt new file mode 100644 index 0000000000..32338a5360 --- /dev/null +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/callback/DirectoryChooserCallback.kt @@ -0,0 +1,3 @@ +package com.github.adamantcheese.chan.core.saf.callback + +abstract class DirectoryChooserCallback : ChooserCallback \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/callback/FileChooserCallback.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/callback/FileChooserCallback.kt new file mode 100644 index 0000000000..c889c53a52 --- /dev/null +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/callback/FileChooserCallback.kt @@ -0,0 +1,3 @@ +package com.github.adamantcheese.chan.core.saf.callback + +abstract class FileChooserCallback : ChooserCallback \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/callback/StartActivityCallbacks.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/callback/StartActivityCallbacks.kt new file mode 100644 index 0000000000..3c473f4f49 --- /dev/null +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/callback/StartActivityCallbacks.kt @@ -0,0 +1,7 @@ +package com.github.adamantcheese.chan.core.saf.callback + +import android.content.Intent + +interface StartActivityCallbacks { + fun myStartActivityForResult(intent: Intent, requestCode: Int): Boolean +} \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt new file mode 100644 index 0000000000..b248cc3bbe --- /dev/null +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt @@ -0,0 +1,77 @@ +package com.github.adamantcheese.chan.core.saf.file + +abstract class AbstractFile( + protected val segments: MutableList = mutableListOf() +) { + /** + * We can't append anything if the last segment's isFileName is true. + * This is a terminal operation. + * */ + protected var isFilenameAppended = segments.lastOrNull()?.isFileName ?: false + + /** + * Appends a new subdirectory to the root directory + * */ + abstract fun appendSubDirSegment(name: String): T + + /** + * Appends a file name to the root directory + * */ + abstract fun appendFileNameSegment(name: String): T + + /** + * Creates a new file that consists of the root directory and segments (sub dirs or the file name) + * */ + abstract fun create(): T? + + abstract fun exists(): Boolean + abstract fun isFile(): Boolean + abstract fun isDirectory(): Boolean + abstract fun canRead(): Boolean + abstract fun canWrite(): Boolean + abstract fun name(): String? + + fun segmentsCount(): Int = segments.size + + /** + * Removes the last appended segment if there are any + * */ + fun removeLastSegment(): Boolean { + if (segments.isEmpty()) { + return false + } + + segments.removeAt(segments.lastIndex) + return true + } + + /** + * We can have the root to be a directory or a file. + * If it's a directory, that means that we can append sub directories to it. + * If it's a file we can't do that so usually when attempting to append something to the FileRoot + * an exception will be thrown + * + * @param holder either Uri or File. Represents either just a path or a path with file name + * */ + sealed class Root(val holder: T) { + + fun name(): String? { + if (this is FileRoot) { + return this.fileName + } + + return null + } + + class DirRoot(holder: T) : Root(holder) + class FileRoot(holder: T, val fileName: String) : Root(holder) + } + + /** + * Segment represents a sub directory or a file name + * */ + class Segment( + val name: String, + val isFileName: Boolean = false + ) +} \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt new file mode 100644 index 0000000000..c32ad3c4c0 --- /dev/null +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt @@ -0,0 +1,160 @@ +package com.github.adamantcheese.chan.core.saf.file + +import android.content.Context +import android.net.Uri +import android.webkit.MimeTypeMap +import androidx.documentfile.provider.DocumentFile +import com.github.adamantcheese.chan.core.appendManyEncoded +import com.github.adamantcheese.chan.core.extension +import com.github.adamantcheese.chan.utils.Logger + +class ExternalFile( + private val appContext: Context, + private val root: Root +) : AbstractFile() { + private val mimeTypeMap = MimeTypeMap.getSingleton() + + override fun appendSubDirSegment(name: String): ExternalFile { + if (root is Root.FileRoot) { + throw IllegalStateException("root is already FileRoot, cannot append anything anymore") + } + + if (isFilenameAppended) { + throw IllegalStateException("Cannot append anything after file name has been appended") + } + + if (name.isNullOrBlank()) { + throw IllegalArgumentException("Bad name: $name") + } + + if (name.extension() != null) { + throw IllegalArgumentException("Directory name must not contain extension, " + + "extension = ${name.extension()}") + } + + segments += Segment(name) + return this + } + + override fun appendFileNameSegment(name: String): ExternalFile { + if (root is Root.FileRoot) { + throw IllegalStateException("root is already FileRoot, cannot append anything anymore") + } + + if (isFilenameAppended) { + throw IllegalStateException("Cannot append anything after file name has been appended") + } + + if (name.isNullOrBlank()) { + throw IllegalArgumentException("Bad name: $name") + } + + segments += Segment(name, true) + return this + } + + override fun create(): ExternalFile? { + if (root is Root.FileRoot) { + throw IllegalStateException("root is already FileRoot, cannot append anything anymore") + } + + val rootDir = DocumentFile.fromTreeUri(appContext, root.holder) + if (rootDir == null) { + // Couldn't create a DocumentFile from the root + Logger.e(TAG, "DocumentFile.fromTreeUri returned null, root.uri = ${root.holder}") + return null + } + + if (segments.isEmpty()) { + // Root is probably already exists and there is no point in creating it again so just + // return null here + Logger.e(TAG, "No segments") + return null + } + + var newFile: DocumentFile? = null + + for (segment in segments) { + val file = newFile ?: rootDir + + val prevFile = file.findFile(segment.name) + if (prevFile != null) { + // File already exists, no need to create it again (and we won't be able) + newFile = prevFile + continue + } + + if (!segment.isFileName) { + newFile = file.createDirectory(segment.name) + if (newFile == null) { + Logger.e(TAG, "file.createDirectory returned null, file.uri = ${file.uri}, " + + "segment.name = ${segment.name}") + return null + } + } else { + newFile = file.createFile(getMimeType(segment.name), segment.name) + if (newFile == null) { + Logger.e(TAG, "file.createFile returned null, file.uri = ${file.uri}, " + + "segment.name = ${segment.name}") + return null + } + + // Ignore any left segments (which we shouldn't have) after encountering fileName + // segment + return ExternalFile(appContext, Root.FileRoot(newFile.uri, segment.name)) + } + } + + if (newFile == null) { + Logger.e(TAG, "result file is null") + return null + } + + return ExternalFile(appContext, Root.DirRoot(newFile.uri)) + } + + override fun exists(): Boolean = toDocumentFile()?.exists() ?: false + override fun isFile(): Boolean = toDocumentFile()?.isFile ?: false + override fun isDirectory(): Boolean = toDocumentFile()?.isDirectory ?: false + override fun canRead(): Boolean = toDocumentFile()?.canRead() ?: false + override fun canWrite(): Boolean = toDocumentFile()?.canWrite() ?: false + override fun name(): String? = root.name() + + private fun toDocumentFile(): DocumentFile? { + return if (segments.isEmpty()) { + when (root) { + is Root.DirRoot -> DocumentFile.fromTreeUri(appContext, root.holder) + is Root.FileRoot -> DocumentFile.fromSingleUri(appContext, root.holder) + } + } else { + val fullUri = root.holder + .buildUpon() + .appendManyEncoded(segments.map { segment -> segment.name }) + .build() + + when (root) { + is Root.DirRoot -> DocumentFile.fromTreeUri(appContext, fullUri) + is Root.FileRoot -> DocumentFile.fromSingleUri(appContext, fullUri) + } + } + } + + private fun getMimeType(filename: String): String { + val extension = filename.extension() + if (extension == null) { + return BINARY_FILE_MIME_TYPE + } + + val mimeType = mimeTypeMap.getMimeTypeFromExtension(extension) + if (mimeType == null || mimeType.isEmpty()) { + return BINARY_FILE_MIME_TYPE + } + + return mimeType + } + + companion object { + private const val TAG = "FileManager" + private const val BINARY_FILE_MIME_TYPE = "application/octet-stream" + } +} \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt new file mode 100644 index 0000000000..5eb7400f3a --- /dev/null +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt @@ -0,0 +1,106 @@ +package com.github.adamantcheese.chan.core.saf.file + +import com.github.adamantcheese.chan.core.extension +import com.github.adamantcheese.chan.utils.Logger +import java.io.File +import java.lang.IllegalStateException + +class RawFile( + private val root: Root +) : AbstractFile() { + + override fun appendSubDirSegment(name: String): RawFile { + if (root is Root.FileRoot) { + throw IllegalStateException("root is already FileRoot, cannot append anything anymore") + } + + if (isFilenameAppended) { + throw IllegalStateException("Cannot append anything after file name has been appended") + } + + if (name.isNullOrBlank()) { + throw IllegalArgumentException("Bad name: $name") + } + + if (name.extension() != null) { + throw IllegalArgumentException("Directory name must not contain extension, " + + "extension = ${name.extension()}") + } + + segments += Segment(name) + return this + } + + override fun appendFileNameSegment(name: String): RawFile { + if (root is Root.FileRoot) { + throw IllegalStateException("root is already FileRoot, cannot append anything anymore") + } + + if (isFilenameAppended) { + throw IllegalStateException("Cannot append anything after file name has been appended") + } + + if (name.isNullOrBlank()) { + throw IllegalArgumentException("Bad name: $name") + } + + segments += Segment(name, true) + return this + } + + override fun create(): RawFile? { + if (root is Root.FileRoot) { + throw IllegalStateException("root is already FileRoot, cannot append anything anymore") + } + + if (segments.isEmpty()) { + // Root is probably already existing and there is no point in creating it again so just + // return null here + Logger.e(TAG, "No segments") + return null + } + + var newFile = root.holder + + for (segment in segments) { + if (!segment.isFileName) { + newFile = File(newFile, segment.name) + } else { + return RawFile(Root.FileRoot(File(newFile, segment.name), segment.name)) + } + } + + return RawFile(Root.DirRoot(newFile)) + } + + override fun exists(): Boolean = toFile().exists() + override fun isFile(): Boolean = toFile().isFile + override fun isDirectory(): Boolean = toFile().isDirectory + override fun canRead(): Boolean = toFile().canRead() + override fun canWrite(): Boolean = toFile().canWrite() + override fun name(): String? = root.name() + + private fun toFile(): File { + return if (segments.isEmpty()) { + when (root) { + is Root.DirRoot -> root.holder + is Root.FileRoot -> root.holder + } + } else { + var newFile = root.holder + + for (segment in segments) { + newFile = File(newFile, segment.name) + } + + when (root) { + is Root.DirRoot -> newFile + is Root.FileRoot -> newFile + } + } + } + + companion object { + private const val TAG = "RawFile" + } +} \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/ChanSettings.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/ChanSettings.java index 6a65fe2221..d56027fcec 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/ChanSettings.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/ChanSettings.java @@ -121,7 +121,9 @@ public String getKey() { public static final BooleanSetting postPinThread; public static final BooleanSetting shortPinInfo; + @Deprecated public static final StringSetting saveLocation; + public static final StringSetting saveLocationUri; public static final BooleanSetting saveServerFilename; public static final BooleanSetting shareUrl; public static final BooleanSetting enableReplyFab; @@ -211,6 +213,8 @@ public String getKey() { shortPinInfo = new BooleanSetting(p, "preference_short_pin_info", true); saveLocation = new StringSetting(p, "preference_image_save_location", Environment.getExternalStorageDirectory() + File.separator + getApplicationLabel()); + saveLocationUri = new StringSetting(p, "preference_image_save_location_uri", ""); + saveLocation.addCallback((setting, value) -> EventBus.getDefault().post(new SettingChanged<>(saveLocation))); saveServerFilename = new BooleanSetting(p, "preference_image_save_original", false); diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java index 2cf4361e55..1e4037c98f 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java @@ -17,8 +17,13 @@ package com.github.adamantcheese.chan.ui.controller; import android.content.Context; +import android.net.Uri; +import android.widget.Toast; import com.github.adamantcheese.chan.R; +import com.github.adamantcheese.chan.core.saf.FileManager; +import com.github.adamantcheese.chan.core.saf.callback.DirectoryChooserCallback; +import com.github.adamantcheese.chan.core.saf.file.ExternalFile; import com.github.adamantcheese.chan.core.settings.ChanSettings; import com.github.adamantcheese.chan.ui.settings.BooleanSettingView; import com.github.adamantcheese.chan.ui.settings.LinkSettingView; @@ -29,10 +34,14 @@ import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; +import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.List; +import javax.inject.Inject; + +import static com.github.adamantcheese.chan.Chan.inject; import static com.github.adamantcheese.chan.utils.AndroidUtils.getString; public class MediaSettingsController extends SettingsController { @@ -43,6 +52,9 @@ public class MediaSettingsController extends SettingsController { private ListSettingView imageAutoLoadView; private ListSettingView videoAutoLoadView; + @Inject + FileManager fileManager; + public MediaSettingsController(Context context) { super(context); } @@ -50,15 +62,14 @@ public MediaSettingsController(Context context) { @Override public void onCreate() { super.onCreate(); + inject(this); EventBus.getDefault().register(this); navigation.setTitle(R.string.settings_screen_media); setupLayout(); - populatePreferences(); - buildPreferences(); onPreferenceChange(imageAutoLoadView); @@ -86,8 +97,8 @@ public void onPreferenceChange(SettingView item) { @Subscribe public void onEvent(ChanSettings.SettingChanged setting) { - if (setting.setting == ChanSettings.saveLocation) { - updateSaveLocationSetting(); + if (setting.setting == ChanSettings.saveLocationUri) { + saveLocation.setDescription(ChanSettings.saveLocationUri.get()); } } @@ -215,14 +226,71 @@ private void updateVideoLoadModes() { } private void setupSaveLocationSetting(SettingsGroup media) { - saveLocation = (LinkSettingView) media.add(new LinkSettingView(this, - R.string.save_location_screen, 0, - v -> navigationController.pushController(new SaveLocationController(context)))); - updateSaveLocationSetting(); + LinkSettingView chooseSaveLocationSetting = new LinkSettingView(this, + R.string.save_location_screen, + 0, + v -> handleDirChoose()); + + saveLocation = (LinkSettingView) media.add(chooseSaveLocationSetting); + saveLocation.setDescription(ChanSettings.saveLocationUri.get()); + } + + private void handleDirChoose() { + boolean result = fileManager.openChooseDirectoryDialog(new DirectoryChooserCallback() { + @Override + public void onResult(@NotNull Uri uri) { + ChanSettings.saveLocationUri.set(uri.toString()); + + // We won't use it anymore + ChanSettings.saveLocation.set(""); + + saveLocation.setDescription(uri.toString()); + + testMethod(uri); + } + + @Override + public void onCancel(@NotNull String reason) { + Toast.makeText(context, reason, Toast.LENGTH_LONG).show(); + } + }); + + if (!result) { + Toast.makeText(context, "Could not start activity for result", Toast.LENGTH_SHORT).show(); + } } - private void updateSaveLocationSetting() { - saveLocation.setDescription(ChanSettings.saveLocation.get()); + private void testMethod(@NotNull Uri uri) { + ExternalFile externalFile1 = fileManager.fromUri(uri) + .appendSubDirSegment("123") + .appendSubDirSegment("456") + .appendSubDirSegment("789") + .appendFileNameSegment("test123.txt") + .create(); + + boolean exists = fileManager.fromUri(uri) + .appendSubDirSegment("123") + .appendSubDirSegment("456") + .appendSubDirSegment("789") + .exists(); + + System.out.println(); + System.out.println(); + System.out.println(); + +// AbstractFile newDir = fileManager.newDir(externalFile, "test2"); +// AbstractFile createdDir = fileManager.createDir(newDir); +// +// AbstractFile newDir2 = fileManager.newDir(createdDir, "test2"); +// AbstractFile createdDir2 = fileManager.createDir(newDir2); +// +// AbstractFile newFile2 = fileManager.newFile(createdDir2, "test123.wav"); +// AbstractFile createdFile2 = fileManager.createFile(newFile2); +// +// System.out.println(createdFile2.isDirectory()); +// System.out.println(createdFile2.isFile()); +// System.out.println(createdFile2.isRawFile()); + } private void updateThreadFolderSetting() { diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/SaveLocationController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/SaveLocationController.java deleted file mode 100644 index dd6253c97c..0000000000 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/SaveLocationController.java +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Kuroba - *chan browser https://github.com/Adamantcheese/Kuroba/ - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package com.github.adamantcheese.chan.ui.controller; - -import android.Manifest; -import android.annotation.SuppressLint; -import android.content.Context; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.Toast; - -import androidx.appcompat.app.AlertDialog; - -import com.github.adamantcheese.chan.R; -import com.github.adamantcheese.chan.StartActivity; -import com.github.adamantcheese.chan.controller.Controller; -import com.github.adamantcheese.chan.core.saver.FileWatcher; -import com.github.adamantcheese.chan.core.settings.ChanSettings; -import com.github.adamantcheese.chan.ui.adapter.FilesAdapter; -import com.github.adamantcheese.chan.ui.helper.RuntimePermissionsHelper; -import com.github.adamantcheese.chan.ui.layout.FilesLayout; -import com.github.adamantcheese.chan.ui.layout.NewFolderLayout; -import com.google.android.material.floatingactionbutton.FloatingActionButton; - -import java.io.File; - -public class SaveLocationController extends Controller implements FileWatcher.FileWatcherCallback, FilesAdapter.Callback, FilesLayout.Callback, View.OnClickListener { - private FilesLayout filesLayout; - private FloatingActionButton setButton; - private FloatingActionButton addButton; - - private RuntimePermissionsHelper runtimePermissionsHelper; - - private FileWatcher fileWatcher; - - public SaveLocationController(Context context) { - super(context); - } - - @Override - public void onCreate() { - super.onCreate(); - - navigation.setTitle(R.string.save_location_screen); - - view = inflateRes(R.layout.controller_save_location); - filesLayout = view.findViewById(R.id.files_layout); - filesLayout.setCallback(this); - setButton = view.findViewById(R.id.set_button); - setButton.setOnClickListener(this); - addButton = view.findViewById(R.id.add_button); - addButton.setOnClickListener(this); - - File saveLocation = new File(ChanSettings.saveLocation.get()); - fileWatcher = new FileWatcher(this, saveLocation); - - runtimePermissionsHelper = ((StartActivity) context).getRuntimePermissionsHelper(); - if (runtimePermissionsHelper.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { - initialize(); - } else { - requestPermission(); - } - } - - @Override - public void onClick(View v) { - if (v == setButton) { - File currentPath = fileWatcher.getCurrentPath(); - ChanSettings.saveLocation.set(currentPath.getAbsolutePath()); - navigationController.popController(); - } else if (v == addButton) { - @SuppressLint("InflateParams") final NewFolderLayout dialogView = - (NewFolderLayout) LayoutInflater.from(context) - .inflate(R.layout.layout_folder_add, null); - - new AlertDialog.Builder(context) - .setView(dialogView) - .setTitle(R.string.save_new_folder) - .setPositiveButton(R.string.add, (dialog, which) -> { - if (!dialogView.getFolderName().matches("\\A\\w+\\z")) { - Toast.makeText(context, "Folder must be a word, no spaces", Toast.LENGTH_SHORT).show(); - } else { - File newDir = new File(fileWatcher.getCurrentPath().getAbsolutePath() + File.separator + dialogView.getFolderName()); - //noinspection ResultOfMethodCallIgnored - newDir.mkdir(); - fileWatcher.navigateTo(newDir); - ChanSettings.saveLocation.set(fileWatcher.getCurrentPath().getAbsolutePath()); - navigationController.popController(); - } - dialog.dismiss(); - }) - .setNegativeButton(R.string.cancel, null) - .create() - .show(); - } - } - - @Override - public void onFiles(FileWatcher.FileItems fileItems) { - filesLayout.setFiles(fileItems); - } - - @Override - public void onBackClicked() { - fileWatcher.navigateUp(); - } - - @Override - public void onFileItemClicked(FileWatcher.FileItem fileItem) { - if (fileItem.canNavigate()) { - fileWatcher.navigateTo(fileItem.file); - } - // Else ignore, we only do folder selection here - } - - private void requestPermission() { - runtimePermissionsHelper.requestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, granted -> { - if (runtimePermissionsHelper.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { - initialize(); - } else { - runtimePermissionsHelper.showPermissionRequiredDialog( - context, - context.getString(R.string.save_location_storage_permission_required_title), - context.getString(R.string.save_location_storage_permission_required), - this::requestPermission - ); - } - }); - } - - private void initialize() { - filesLayout.initialize(); - fileWatcher.initialize(); - } -} diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/helper/ImagePickDelegate.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/helper/ImagePickDelegate.java index b8183601fc..048dc067fc 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/helper/ImagePickDelegate.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/helper/ImagePickDelegate.java @@ -122,49 +122,54 @@ public void onFail(boolean notFound) { } } - public void onActivityResult(int requestCode, int resultCode, Intent data) { + public boolean onActivityResult(int requestCode, int resultCode, Intent data) { if (callback == null) { - return; + return false; + } + + if (requestCode != IMAGE_PICK_RESULT) { + return false; } boolean ok = false; boolean cancelled = false; - if (requestCode == IMAGE_PICK_RESULT) { - if (resultCode == Activity.RESULT_OK && data != null) { - uri = data.getData(); - - Cursor returnCursor = activity.getContentResolver().query(uri, null, null, null, null); - if (returnCursor != null) { - int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); - returnCursor.moveToFirst(); - if (nameIndex > -1) { - fileName = returnCursor.getString(nameIndex); - } - - returnCursor.close(); - } - if (fileName == null) { - // As per the comment on OpenableColumns.DISPLAY_NAME: - // If this is not provided then the name should default to the last segment of the file's URI. - fileName = uri.getLastPathSegment(); - } + if (resultCode == Activity.RESULT_OK && data != null) { + uri = data.getData(); - if (fileName == null) { - fileName = DEFAULT_FILE_NAME; + Cursor returnCursor = activity.getContentResolver().query(uri, null, null, null, null); + if (returnCursor != null) { + int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); + returnCursor.moveToFirst(); + if (nameIndex > -1) { + fileName = returnCursor.getString(nameIndex); } - new Thread(this).start(); - ok = true; - } else if (resultCode == Activity.RESULT_CANCELED) { - cancelled = true; + returnCursor.close(); + } + + if (fileName == null) { + // As per the comment on OpenableColumns.DISPLAY_NAME: + // If this is not provided then the name should default to the last segment of the file's URI. + fileName = uri.getLastPathSegment(); + } + + if (fileName == null) { + fileName = DEFAULT_FILE_NAME; } + + new Thread(this).start(); + ok = true; + } else if (resultCode == Activity.RESULT_CANCELED) { + cancelled = true; } if (!ok) { callback.onFilePickError(cancelled); reset(); } + + return true; } @Override diff --git a/Kuroba/build.gradle b/Kuroba/build.gradle index dcf3372b38..a053d130d8 100644 --- a/Kuroba/build.gradle +++ b/Kuroba/build.gradle @@ -1,11 +1,14 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { + ext.kotlin_version = '1.3.41' + repositories { google() jcenter() } dependencies { classpath 'com.android.tools.build:gradle:3.4.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } From f537fdfdf15d36223f4fb7fd80b638fe9b2ade51 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 10 Aug 2019 21:53:18 +0300 Subject: [PATCH 013/184] (#172) Couple of little code improvements --- .../chan/core/saf/file/ExternalFile.kt | 17 +++++++---------- .../adamantcheese/chan/core/saf/file/RawFile.kt | 17 ++++++++--------- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt index c32ad3c4c0..2adf6beb26 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt @@ -121,21 +121,18 @@ class ExternalFile( override fun name(): String? = root.name() private fun toDocumentFile(): DocumentFile? { - return if (segments.isEmpty()) { - when (root) { - is Root.DirRoot -> DocumentFile.fromTreeUri(appContext, root.holder) - is Root.FileRoot -> DocumentFile.fromSingleUri(appContext, root.holder) - } + val uri = if (segments.isEmpty()) { + root.holder } else { - val fullUri = root.holder + root.holder .buildUpon() .appendManyEncoded(segments.map { segment -> segment.name }) .build() + } - when (root) { - is Root.DirRoot -> DocumentFile.fromTreeUri(appContext, fullUri) - is Root.FileRoot -> DocumentFile.fromSingleUri(appContext, fullUri) - } + return when (root) { + is Root.DirRoot -> DocumentFile.fromTreeUri(appContext, uri) + is Root.FileRoot -> DocumentFile.fromSingleUri(appContext, uri) } } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt index 5eb7400f3a..dfc8a2f9c9 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt @@ -81,11 +81,8 @@ class RawFile( override fun name(): String? = root.name() private fun toFile(): File { - return if (segments.isEmpty()) { - when (root) { - is Root.DirRoot -> root.holder - is Root.FileRoot -> root.holder - } + val uri = if (segments.isEmpty()) { + root.holder } else { var newFile = root.holder @@ -93,10 +90,12 @@ class RawFile( newFile = File(newFile, segment.name) } - when (root) { - is Root.DirRoot -> newFile - is Root.FileRoot -> newFile - } + newFile + } + + return when (root) { + is Root.DirRoot -> uri + is Root.FileRoot -> uri } } From b08d175459054ca462b2adade9215c796d444a75 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 11 Aug 2019 12:55:37 +0300 Subject: [PATCH 014/184] (#172) Comments and more File API methods --- .../adamantcheese/chan/core/Extensions.kt | 19 +++++++++++++++ .../chan/core/saf/file/AbstractFile.kt | 24 ++++++++++++++++++- .../chan/core/saf/file/ExternalFile.kt | 16 +++++++++++++ .../chan/core/saf/file/RawFile.kt | 9 +++++++ 4 files changed, 67 insertions(+), 1 deletion(-) diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/Extensions.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/Extensions.kt index ae2b8ac862..ad98ab5550 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/Extensions.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/Extensions.kt @@ -9,6 +9,11 @@ fun String.extension(): String? { return null } + if (index == this.lastIndex) { + // The dot is at the very end of the string, so there is no extension + return null + } + return this.substring(index + 1) } @@ -18,4 +23,18 @@ fun Uri.Builder.appendManyEncoded(segments: List): Uri.Builder { } return this +} + +fun Uri.removeLastSegment(): Uri? { + if (this.pathSegments.size <= 1) { + // I think we shouldn't return "/" directory here since android won't let us access it anyway + return null + } + + val newSegments = this.pathSegments + .subList(0, pathSegments.lastIndex - 1) + + return Uri.Builder() + .appendManyEncoded(newSegments) + .build() } \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt index b248cc3bbe..c3da7dcac0 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt @@ -1,6 +1,9 @@ package com.github.adamantcheese.chan.core.saf.file abstract class AbstractFile( + /** + * /test/123/test2/filename.txt -> 4 segments + * */ protected val segments: MutableList = mutableListOf() ) { /** @@ -30,11 +33,13 @@ abstract class AbstractFile( abstract fun canRead(): Boolean abstract fun canWrite(): Boolean abstract fun name(): String? + abstract fun getParent(): T? fun segmentsCount(): Int = segments.size /** * Removes the last appended segment if there are any + * e.g: /test/123/test2 -> /test/123 -> /test * */ fun removeLastSegment(): Boolean { if (segments.isEmpty()) { @@ -63,12 +68,29 @@ abstract class AbstractFile( return null } + /** + * /test/123/test2 + * or + * /test/123/test2/5/6/7/8/112233 + * + * Cannot have an extension! + * */ class DirRoot(holder: T) : Root(holder) + + /** + * /test/123/test2/filename.txt + * where holder = /test/123/test2/filename.txt (Uri), + * fileName = filename.txt (may have no extension) + * */ class FileRoot(holder: T, val fileName: String) : Root(holder) } /** - * Segment represents a sub directory or a file name + * Segment represents a sub directory or a file name, e.g: + * /test/123/test2/filename.txt + * ^ ^ ^ ^ + * | | | +--- File name segment (name = filename.txt, isFileName == true) + * +---+----+-- Directory segments (names = [test, 123, test2], isFileName == false) * */ class Segment( val name: String, diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt index 2adf6beb26..7dbf98b1c3 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt @@ -6,6 +6,7 @@ import android.webkit.MimeTypeMap import androidx.documentfile.provider.DocumentFile import com.github.adamantcheese.chan.core.appendManyEncoded import com.github.adamantcheese.chan.core.extension +import com.github.adamantcheese.chan.core.removeLastSegment import com.github.adamantcheese.chan.utils.Logger class ExternalFile( @@ -120,6 +121,21 @@ class ExternalFile( override fun canWrite(): Boolean = toDocumentFile()?.canWrite() ?: false override fun name(): String? = root.name() + override fun getParent(): ExternalFile? { + if (segments.isNotEmpty()) { + removeLastSegment() + return this + } + + val newUri = root.holder.removeLastSegment() + if (newUri == null) { + Logger.e(TAG, "getParent() removeLastSegment() returned null") + return null + } + + return ExternalFile(appContext, Root.DirRoot(newUri)) + } + private fun toDocumentFile(): DocumentFile? { val uri = if (segments.isEmpty()) { root.holder diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt index dfc8a2f9c9..a75d259af4 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt @@ -80,6 +80,15 @@ class RawFile( override fun canWrite(): Boolean = toFile().canWrite() override fun name(): String? = root.name() + override fun getParent(): RawFile? { + if (segments.isNotEmpty()) { + removeLastSegment() + return this + } + + return RawFile(Root.DirRoot(root.holder.parentFile)) + } + private fun toFile(): File { val uri = if (segments.isEmpty()) { root.holder From d26cdcf38b77aa002f2a2353663fc6d6dd8ef618 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 11 Aug 2019 14:40:14 +0300 Subject: [PATCH 015/184] (#172) Settings exporting/importing now works with SAF --- .../adamantcheese/chan/core/Extensions.kt | 27 +++ .../ImportExportSettingsPresenter.java | 7 +- .../repository/ImportExportRepository.java | 127 ++++++++------ .../chan/core/saf/FileChooser.kt | 157 ++++++++++++++++-- .../chan/core/saf/FileManager.kt | 80 ++++++++- .../core/saf/callback/FileCreateCallback.kt | 3 + .../chan/core/saf/file/AbstractFile.kt | 1 + .../chan/core/saf/file/ExternalFile.kt | 54 ++++-- .../chan/core/saf/file/RawFile.kt | 15 +- .../ImportExportSettingsController.java | 154 +++++------------ .../controller/MediaSettingsController.java | 35 ---- Kuroba/app/src/main/res/values/strings.xml | 16 +- 12 files changed, 423 insertions(+), 253 deletions(-) create mode 100644 Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/callback/FileCreateCallback.kt diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/Extensions.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/Extensions.kt index ad98ab5550..ba4ccd34d2 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/Extensions.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/Extensions.kt @@ -1,7 +1,10 @@ package com.github.adamantcheese.chan.core import android.net.Uri +import android.webkit.MimeTypeMap +import java.io.File +private const val BINARY_FILE_MIME_TYPE = "application/octet-stream" fun String.extension(): String? { val index = this.indexOfLast { ch -> ch == '.' } @@ -37,4 +40,28 @@ fun Uri.removeLastSegment(): Uri? { return Uri.Builder() .appendManyEncoded(newSegments) .build() +} + +fun MimeTypeMap.getMimeFromFilename(filename: String): String { + val extension = filename.extension() + if (extension == null) { + return BINARY_FILE_MIME_TYPE + } + + val mimeType = this.getMimeTypeFromExtension(extension) + if (mimeType == null || mimeType.isEmpty()) { + return BINARY_FILE_MIME_TYPE + } + + return mimeType +} + +fun File.appendMany(segments: List): File { + var newFile = File(this.absolutePath) + + for (segment in segments) { + newFile = File(newFile, segment) + } + + return newFile } \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/ImportExportSettingsPresenter.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/ImportExportSettingsPresenter.java index ccfc24d566..cf986b5a74 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/ImportExportSettingsPresenter.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/ImportExportSettingsPresenter.java @@ -20,8 +20,7 @@ import androidx.annotation.Nullable; import com.github.adamantcheese.chan.core.repository.ImportExportRepository; - -import java.io.File; +import com.github.adamantcheese.chan.core.saf.file.ExternalFile; import javax.inject.Inject; @@ -45,7 +44,7 @@ public void onDestroy() { this.callbacks = null; } - public void doExport(File settingsFile) { + public void doExport(ExternalFile settingsFile) { importExportRepository.exportTo(settingsFile, new ImportExportRepository.ImportExportCallbacks() { @Override public void onSuccess(ImportExportRepository.ImportExport importExport) { @@ -76,7 +75,7 @@ public void onError(Throwable error, ImportExportRepository.ImportExport importE }); } - public void doImport(File settingsFile) { + public void doImport(ExternalFile settingsFile) { importExportRepository.importFrom(settingsFile, new ImportExportRepository.ImportExportCallbacks() { @Override public void onSuccess(ImportExportRepository.ImportExport importExport) { diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/ImportExportRepository.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/ImportExportRepository.java index e73fc1d2d8..3e56be01ff 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/ImportExportRepository.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/ImportExportRepository.java @@ -17,6 +17,7 @@ package com.github.adamantcheese.chan.core.repository; import android.annotation.SuppressLint; +import android.os.ParcelFileDescriptor; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -38,12 +39,13 @@ import com.github.adamantcheese.chan.core.model.orm.PostHide; import com.github.adamantcheese.chan.core.model.orm.SavedThread; import com.github.adamantcheese.chan.core.model.orm.SiteModel; +import com.github.adamantcheese.chan.core.saf.file.ExternalFile; import com.github.adamantcheese.chan.core.settings.ChanSettings; import com.github.adamantcheese.chan.utils.Logger; import com.google.gson.Gson; import com.j256.ormlite.support.ConnectionSource; -import java.io.File; +import java.io.FileDescriptor; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; @@ -79,7 +81,7 @@ public ImportExportRepository( this.gson = gson; } - public void exportTo(File settingsFile, ImportExportCallbacks callbacks) { + public void exportTo(ExternalFile settingsFile, ImportExportCallbacks callbacks) { databaseManager.runTask(() -> { try { ExportedAppSettings appSettings = readSettingsFromDatabase(); @@ -90,41 +92,47 @@ public void exportTo(File settingsFile, ImportExportCallbacks callbacks) { String json = gson.toJson(appSettings); - if (settingsFile.exists()) { - if (!settingsFile.isFile()) { - throw new IOException( - "Settings file is not a file (???) " + settingsFile.getAbsolutePath() - ); - } - - if (!settingsFile.delete()) { - Logger.w(TAG, "Could not delete export file before exporting " + settingsFile.getAbsolutePath()); - } - } - - if (!settingsFile.createNewFile()) { - throw new IOException( - "Could not create a file for exporting " + settingsFile.getAbsolutePath() - ); - } - if (!settingsFile.exists() || !settingsFile.canWrite()) { throw new IOException( "Something wrong with export file (Can't write or it doesn't exist) " - + settingsFile.getAbsolutePath() + + settingsFile.getFullPath() ); } - try (FileWriter writer = new FileWriter(settingsFile)) { - writer.write(json); - } + try (ParcelFileDescriptor parcelFileDescriptor = settingsFile.getParcelFileDescriptor( + ExternalFile.FileDescriptorMode.Write)) { - Logger.d(TAG, "Exporting done!"); - callbacks.onSuccess(ImportExport.Export); + if (parcelFileDescriptor == null) { + IllegalStateException exception = new IllegalStateException( + "parcelFileDescriptor is null, path = " + settingsFile.getFullPath()); + + callbacks.onError( + exception, + ImportExport.Export); + return null; + } + + FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor(); + if (fileDescriptor == null) { + IllegalStateException exception = new IllegalStateException( + "fileDescriptor is null, path = " + settingsFile.getFullPath()); + + callbacks.onError( + exception, + ImportExport.Export); + return null; + } + + try (FileWriter writer = new FileWriter(fileDescriptor)) { + writer.write(json); + } + + Logger.d(TAG, "Exporting done!"); + callbacks.onSuccess(ImportExport.Export); + } } catch (Throwable error) { Logger.e(TAG, "Error while trying to export settings", error); - deleteExportFile(settingsFile); callbacks.onError(error, ImportExport.Export); } @@ -132,11 +140,12 @@ public void exportTo(File settingsFile, ImportExportCallbacks callbacks) { }); } - public void importFrom(File settingsFile, ImportExportCallbacks callbacks) { + public void importFrom(ExternalFile settingsFile, ImportExportCallbacks callbacks) { databaseManager.runTask(() -> { try { if (!settingsFile.exists()) { - Logger.i(TAG, "There is nothing to import, importFile does not exist " + settingsFile.getAbsolutePath()); + Logger.i(TAG, "There is nothing to import, importFile does not exist " + + settingsFile.getFullPath()); callbacks.onNothingToImportExport(ImportExport.Import); return null; } @@ -144,26 +153,52 @@ public void importFrom(File settingsFile, ImportExportCallbacks callbacks) { if (!settingsFile.canRead()) { throw new IOException( "Something wrong with import file (Can't read or it doesn't exist) " - + settingsFile.getAbsolutePath() + + settingsFile.getFullPath() ); } - ExportedAppSettings appSettings; + try (ParcelFileDescriptor parcelFileDescriptor = settingsFile.getParcelFileDescriptor( + ExternalFile.FileDescriptorMode.Read)) { - try (FileReader reader = new FileReader(settingsFile)) { - appSettings = gson.fromJson(reader, ExportedAppSettings.class); - } + if (parcelFileDescriptor == null) { + IllegalStateException exception = new IllegalStateException( + "parcelFileDescriptor is null, path = " + settingsFile.getFullPath()); - if (appSettings.isEmpty()) { - Logger.i(TAG, "There is nothing to import, appSettings is empty"); - callbacks.onNothingToImportExport(ImportExport.Import); - return null; - } + callbacks.onError( + exception, + ImportExport.Import); + return null; + } - writeSettingsToDatabase(appSettings); + FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor(); + if (fileDescriptor == null) { + IllegalStateException exception = new IllegalStateException( + "fileDescriptor is null, path = " + settingsFile.getFullPath()); + + callbacks.onError( + exception, + ImportExport.Import); + return null; + } + + ExportedAppSettings appSettings; + + try (FileReader reader = new FileReader(fileDescriptor)) { + appSettings = gson.fromJson(reader, ExportedAppSettings.class); + } - Logger.d(TAG, "Importing done!"); - callbacks.onSuccess(ImportExport.Import); + if (appSettings.isEmpty()) { + Logger.i(TAG, "There is nothing to import, appSettings is empty"); + callbacks.onNothingToImportExport(ImportExport.Import); + return null; + } + + writeSettingsToDatabase(appSettings); + + Logger.d(TAG, "Importing done!"); + callbacks.onSuccess(ImportExport.Import); + + } } catch (Throwable error) { Logger.e(TAG, "Error while trying to import settings", error); callbacks.onError(error, ImportExport.Import); @@ -173,14 +208,6 @@ public void importFrom(File settingsFile, ImportExportCallbacks callbacks) { }); } - private void deleteExportFile(File exportFile) { - if (exportFile != null) { - if (!exportFile.delete()) { - Logger.w(TAG, "Could not delete export file " + exportFile.getAbsolutePath()); - } - } - } - private void writeSettingsToDatabase(@NonNull ExportedAppSettings appSettingsParam) throws SQLException, IOException, DowngradeNotSupportedException { ExportedAppSettings appSettings = appSettingsParam; diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileChooser.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileChooser.kt index eb3cc5a0d4..a8d0ae4708 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileChooser.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileChooser.kt @@ -4,16 +4,17 @@ import android.app.Activity import android.content.Context import android.content.Intent import android.provider.DocumentsContract -import com.github.adamantcheese.chan.core.saf.callback.ChooserCallback -import com.github.adamantcheese.chan.core.saf.callback.DirectoryChooserCallback -import com.github.adamantcheese.chan.core.saf.callback.FileChooserCallback -import com.github.adamantcheese.chan.core.saf.callback.StartActivityCallbacks +import android.webkit.MimeTypeMap +import com.github.adamantcheese.chan.core.getMimeFromFilename +import com.github.adamantcheese.chan.core.saf.callback.* import com.github.adamantcheese.chan.utils.Logger +import java.lang.IllegalArgumentException internal class FileChooser( private val appContext: Context ) { private val callbacksMap = hashMapOf() + private val mimeTypeMap = MimeTypeMap.getSingleton() private var requestCode = 10000 private var startActivityCallbacks: StartActivityCallbacks? = null @@ -52,6 +53,7 @@ internal class FileChooser( ) intent.addCategory(Intent.CATEGORY_OPENABLE) + intent.type = "*/*" val nextRequestCode = ++requestCode callbacksMap[nextRequestCode] = fileChooserCallback as ChooserCallback @@ -60,20 +62,42 @@ internal class FileChooser( } ?: false } + internal fun openCreateFileDialog( + fileName: String? = null, + fileCreateCallback: FileCreateCallback + ): Boolean { + return startActivityCallbacks?.let { callbacks -> + val intent = Intent(Intent.ACTION_CREATE_DOCUMENT) + intent.addFlags( + Intent.FLAG_GRANT_READ_URI_PERMISSION or + Intent.FLAG_GRANT_WRITE_URI_PERMISSION + ) + + intent.addCategory(Intent.CATEGORY_OPENABLE) + + if (fileName != null) { + intent.type = mimeTypeMap.getMimeFromFilename(fileName) + intent.putExtra(Intent.EXTRA_TITLE, fileName) + } + + val nextRequestCode = ++requestCode + callbacksMap[nextRequestCode] = fileCreateCallback as ChooserCallback + + return@let callbacks.myStartActivityForResult(intent, nextRequestCode) + } ?: false + } + internal fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { - if (!callbacksMap.containsKey(requestCode)) { + val callback = callbacksMap[requestCode] + if (callback == null) { + Logger.d(TAG, "Callback is already removed from the map") return false } try { - val callback = callbacksMap[requestCode] - if (callback == null) { - Logger.d(TAG, "Callback is already removed from the map") - return false - } - if (startActivityCallbacks == null) { // Skip all requests when the callback is not set + Logger.d(TAG, "Callback is not attached") return false } @@ -84,6 +108,10 @@ internal class FileChooser( is FileChooserCallback -> { handleFileChooserCallback(callback, resultCode, data) } + is FileCreateCallback -> { + handleFileCreateCallback(callback, resultCode, data) + } + else -> throw IllegalArgumentException("Not implemented for ${callback.javaClass.name}") } return true @@ -92,12 +120,107 @@ internal class FileChooser( } } + private fun handleFileCreateCallback( + callback: FileCreateCallback, + resultCode: Int, + intent: Intent?) { + if (resultCode != Activity.RESULT_OK) { + val msg = "handleFileCreateCallback() Non OK result ($resultCode)" + + Logger.e(TAG, msg) + callback.onCancel(msg) + return + } + + if (intent == null) { + val msg = "handleFileCreateCallback() Intent is null" + + Logger.e(TAG, msg) + callback.onCancel(msg) + return + } + + val read = (intent.flags and Intent.FLAG_GRANT_READ_URI_PERMISSION) != 0 + val write = (intent.flags and Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0 + + if (!read) { + val msg = "handleFileCreateCallback() No grant read uri permission given" + + Logger.e(TAG, msg) + callback.onCancel(msg) + return + } + + if (!write) { + val msg = "handleFileCreateCallback() No grant write uri permission given" + + Logger.e(TAG, msg) + callback.onCancel(msg) + return + } + + val uri = intent.data + if (uri == null) { + val msg = "handleFileCreateCallback() intent.getData() == null" + + Logger.e(TAG, msg) + callback.onCancel(msg) + return + } + + callback.onResult(uri) + } + private fun handleFileChooserCallback( callback: FileChooserCallback, resultCode: Int, intent: Intent? ) { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + if (resultCode != Activity.RESULT_OK) { + val msg = "handleFileChooserCallback() Non OK result ($resultCode)" + + Logger.e(TAG, msg) + callback.onCancel(msg) + return + } + + if (intent == null) { + val msg = "handleFileChooserCallback() Intent is null" + + Logger.e(TAG, msg) + callback.onCancel(msg) + return + } + + val read = (intent.flags and Intent.FLAG_GRANT_READ_URI_PERMISSION) != 0 + val write = (intent.flags and Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0 + + if (!read) { + val msg = "handleFileChooserCallback() No grant read uri permission given" + + Logger.e(TAG, msg) + callback.onCancel(msg) + return + } + + if (!write) { + val msg = "handleFileChooserCallback() No grant write uri permission given" + + Logger.e(TAG, msg) + callback.onCancel(msg) + return + } + + val uri = intent.data + if (uri == null) { + val msg = "handleFileChooserCallback() intent.getData() == null" + + Logger.e(TAG, msg) + callback.onCancel(msg) + return + } + + callback.onResult(uri) } private fun handleDirectoryChooserCallback( @@ -106,7 +229,7 @@ internal class FileChooser( intent: Intent? ) { if (resultCode != Activity.RESULT_OK) { - val msg = "Non OK result ($resultCode)" + val msg = "handleDirectoryChooserCallback() Non OK result ($resultCode)" Logger.e(TAG, msg) callback.onCancel(msg) @@ -114,7 +237,7 @@ internal class FileChooser( } if (intent == null) { - val msg = "Intent is null" + val msg = "handleDirectoryChooserCallback() Intent is null" Logger.e(TAG, msg) callback.onCancel(msg) @@ -125,7 +248,7 @@ internal class FileChooser( val write = (intent.flags and Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0 if (!read) { - val msg = "No grant read uri permission given" + val msg = "handleDirectoryChooserCallback() No grant read uri permission given" Logger.e(TAG, msg) callback.onCancel(msg) @@ -133,7 +256,7 @@ internal class FileChooser( } if (!write) { - val msg = "No grant write uri permission given" + val msg = "handleDirectoryChooserCallback() No grant write uri permission given" Logger.e(TAG, msg) callback.onCancel(msg) @@ -142,7 +265,7 @@ internal class FileChooser( val uri = intent.data if (uri == null) { - val msg = "intent.getData() == null" + val msg = "handleDirectoryChooserCallback() intent.getData() == null" Logger.e(TAG, msg) callback.onCancel(msg) diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt index 5e7d30e602..acf39b7135 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt @@ -3,12 +3,16 @@ package com.github.adamantcheese.chan.core.saf import android.content.Context import android.content.Intent import android.net.Uri +import android.provider.DocumentsContract +import androidx.documentfile.provider.DocumentFile import com.github.adamantcheese.chan.core.saf.callback.DirectoryChooserCallback import com.github.adamantcheese.chan.core.saf.callback.FileChooserCallback +import com.github.adamantcheese.chan.core.saf.callback.FileCreateCallback import com.github.adamantcheese.chan.core.saf.callback.StartActivityCallbacks import com.github.adamantcheese.chan.core.saf.file.AbstractFile import com.github.adamantcheese.chan.core.saf.file.ExternalFile import com.github.adamantcheese.chan.core.saf.file.RawFile +import com.github.adamantcheese.chan.utils.Logger import java.io.File class FileManager( @@ -36,6 +40,14 @@ class FileManager( return fileChooser.openChooseFileDialog(callback) } + fun openCreateFileDialog(callback: FileCreateCallback): Boolean { + return openCreateFileDialog(null, callback) + } + + fun openCreateFileDialog(filename: String?, callback: FileCreateCallback): Boolean { + return fileChooser.openCreateFileDialog(filename, callback) + } + fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { return fileChooser.onActivityResult(requestCode, resultCode, data) } @@ -66,7 +78,71 @@ class FileManager( * Create an external file from Uri * */ fun fromUri(uri: Uri): ExternalFile? { - // FIXME: Uri must be a directory!!! An additional check is needed here! - return ExternalFile(appContext, AbstractFile.Root.DirRoot(uri)) + val documentFile = toDocumentFile(uri) + if (documentFile == null) { + Logger.e(TAG, "fromUri() toDocumentFile() returned null") + return null + } + + return if (documentFile.isFile) { + val filename = queryTreeName(uri) + if (filename == null) { + Logger.e(TAG, "fromUri() queryTreeName() returned null") + return null + } + + ExternalFile(appContext, AbstractFile.Root.FileRoot(uri, filename)) + } else { + ExternalFile(appContext, AbstractFile.Root.DirRoot(uri)) + } + } + + // TODO: may not work! + private fun toDocumentFile(uri: Uri): DocumentFile? { + if (!DocumentFile.isDocumentUri(appContext, uri)) { + Logger.e(TAG, "Not a DocumentFile, uri = $uri") + return null + } + + val treeUri = try { + DocumentFile.fromTreeUri(appContext, uri) + } catch (e: IllegalArgumentException) { + null + } + + if (treeUri != null) { + return treeUri + } + + return try { + DocumentFile.fromSingleUri(appContext, uri) + } catch (e: IllegalArgumentException) { + Logger.e(TAG, "provided uri is neither a treeUri nor singleUri, uri = ${uri}") + null + } + } + + // TODO: may not work! + private fun queryTreeName(uri: Uri): String? { + val contentResolver = appContext.contentResolver + + try { + return contentResolver.query(uri, FILENAME_PROJECTION, null, null, null)?.use { cursor -> + if (cursor.moveToNext()) { + return cursor.getString(0) + } + + return null + } + } catch (e: Throwable) { + Logger.e(TAG, "Error while trying to query for name, uri = $uri", e) + return null + } + } + + companion object { + private const val TAG = "FileManager" + + private val FILENAME_PROJECTION = arrayOf(DocumentsContract.Document.COLUMN_DISPLAY_NAME) } } \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/callback/FileCreateCallback.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/callback/FileCreateCallback.kt new file mode 100644 index 0000000000..a2d27021c4 --- /dev/null +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/callback/FileCreateCallback.kt @@ -0,0 +1,3 @@ +package com.github.adamantcheese.chan.core.saf.callback + +abstract class FileCreateCallback : ChooserCallback \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt index c3da7dcac0..35eccc001e 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt @@ -34,6 +34,7 @@ abstract class AbstractFile( abstract fun canWrite(): Boolean abstract fun name(): String? abstract fun getParent(): T? + abstract fun getFullPath(): String fun segmentsCount(): Int = segments.size diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt index 7dbf98b1c3..5134b24616 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt @@ -2,12 +2,15 @@ package com.github.adamantcheese.chan.core.saf.file import android.content.Context import android.net.Uri +import android.os.ParcelFileDescriptor import android.webkit.MimeTypeMap import androidx.documentfile.provider.DocumentFile import com.github.adamantcheese.chan.core.appendManyEncoded import com.github.adamantcheese.chan.core.extension +import com.github.adamantcheese.chan.core.getMimeFromFilename import com.github.adamantcheese.chan.core.removeLastSegment import com.github.adamantcheese.chan.utils.Logger +import java.io.FileDescriptor class ExternalFile( private val appContext: Context, @@ -93,7 +96,7 @@ class ExternalFile( return null } } else { - newFile = file.createFile(getMimeType(segment.name), segment.name) + newFile = file.createFile(mimeTypeMap.getMimeFromFilename(segment.name), segment.name) if (newFile == null) { Logger.e(TAG, "file.createFile returned null, file.uri = ${file.uri}, " + "segment.name = ${segment.name}") @@ -136,6 +139,36 @@ class ExternalFile( return ExternalFile(appContext, Root.DirRoot(newUri)) } + fun getParcelFileDescriptor(fileDescriptorMode: FileDescriptorMode): ParcelFileDescriptor? { + return appContext.contentResolver.openFileDescriptor( + root.holder, + fileDescriptorMode.mode) + } + + /** + * Example: + * withFileDescriptor(FileDescriptorMode.Read) { fd -> + * // Do anything here with FileDescriptor here, it will be closed automatically upon + * // exiting the lambda + * } + * */ + fun withFileDescriptor( + fileDescriptorMode: FileDescriptorMode, + func: (FileDescriptor) -> Unit + ): Boolean { + return getParcelFileDescriptor(fileDescriptorMode)?.use { pfd -> + func(pfd.fileDescriptor) + return@use true + } ?: false + } + + override fun getFullPath(): String { + return Uri.parse(root.holder.toString()).buildUpon() + .appendManyEncoded(segments.map { segment -> segment.name }) + .build() + .toString() + } + private fun toDocumentFile(): DocumentFile? { val uri = if (segments.isEmpty()) { root.holder @@ -152,22 +185,15 @@ class ExternalFile( } } - private fun getMimeType(filename: String): String { - val extension = filename.extension() - if (extension == null) { - return BINARY_FILE_MIME_TYPE - } - - val mimeType = mimeTypeMap.getMimeTypeFromExtension(extension) - if (mimeType == null || mimeType.isEmpty()) { - return BINARY_FILE_MIME_TYPE - } - - return mimeType + enum class FileDescriptorMode(val mode: String) { + Read("r"), + Write("w"), + // It is recommended to prefer either Read or Write modes in the documentation. + // Use ReadWrite only when it is really necessary. + ReadWrite("rw") } companion object { private const val TAG = "FileManager" - private const val BINARY_FILE_MIME_TYPE = "application/octet-stream" } } \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt index a75d259af4..ffbfb3efb3 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt @@ -1,5 +1,6 @@ package com.github.adamantcheese.chan.core.saf.file +import com.github.adamantcheese.chan.core.appendMany import com.github.adamantcheese.chan.core.extension import com.github.adamantcheese.chan.utils.Logger import java.io.File @@ -89,17 +90,17 @@ class RawFile( return RawFile(Root.DirRoot(root.holder.parentFile)) } + override fun getFullPath(): String { + return root.holder + .appendMany(segments.map { segment -> segment.name }) + .absolutePath + } + private fun toFile(): File { val uri = if (segments.isEmpty()) { root.holder } else { - var newFile = root.holder - - for (segment in segments) { - newFile = File(newFile, segment.name) - } - - newFile + root.holder.appendMany(segments.map { segment -> segment.name }) } return when (root) { diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ImportExportSettingsController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ImportExportSettingsController.java index a4201dace5..4544e5d54c 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ImportExportSettingsController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ImportExportSettingsController.java @@ -16,47 +16,52 @@ */ package com.github.adamantcheese.chan.ui.controller; -import android.Manifest; -import android.content.ClipData; -import android.content.ClipboardManager; import android.content.Context; -import android.os.Environment; +import android.net.Uri; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; import com.github.adamantcheese.chan.R; import com.github.adamantcheese.chan.StartActivity; import com.github.adamantcheese.chan.core.presenter.ImportExportSettingsPresenter; import com.github.adamantcheese.chan.core.repository.ImportExportRepository; -import com.github.adamantcheese.chan.core.settings.ChanSettings; +import com.github.adamantcheese.chan.core.saf.FileManager; +import com.github.adamantcheese.chan.core.saf.callback.FileChooserCallback; +import com.github.adamantcheese.chan.core.saf.callback.FileCreateCallback; +import com.github.adamantcheese.chan.core.saf.file.ExternalFile; import com.github.adamantcheese.chan.ui.settings.LinkSettingView; import com.github.adamantcheese.chan.ui.settings.SettingsController; import com.github.adamantcheese.chan.ui.settings.SettingsGroup; import com.github.adamantcheese.chan.utils.AndroidUtils; -import java.io.File; +import org.jetbrains.annotations.NotNull; +import javax.inject.Inject; + +import static com.github.adamantcheese.chan.Chan.inject; import static com.github.adamantcheese.chan.utils.AndroidUtils.getApplicationLabel; public class ImportExportSettingsController extends SettingsController implements ImportExportSettingsPresenter.ImportExportSettingsCallbacks { public static final String EXPORT_FILE_NAME = getApplicationLabel() + "_exported_settings.json"; - @Nullable + @Inject + FileManager fileManager; + private ImportExportSettingsPresenter presenter; @Nullable private OnExportSuccessCallbacks callbacks; private LoadingViewController loadingViewController; - private File settingsFile = new File(ChanSettings.saveLocation.get(), EXPORT_FILE_NAME); public ImportExportSettingsController(Context context, @NonNull OnExportSuccessCallbacks callbacks) { super(context); + inject(this); + this.callbacks = callbacks; this.loadingViewController = new LoadingViewController(context, true); } @@ -72,8 +77,6 @@ public void onCreate() { setupLayout(); populatePreferences(); buildPreferences(); - - showCreateDirectoryDialog(); } @Override @@ -112,103 +115,47 @@ private void populatePreferences() { } private void onExportClicked() { - String state = Environment.getExternalStorageState(); - if (!Environment.MEDIA_MOUNTED.equals(state)) { - showMessage(context.getString(R.string.error_external_storage_is_not_mounted)); - return; - } + fileManager.openCreateFileDialog(EXPORT_FILE_NAME, new FileCreateCallback() { + @Override + public void onResult(@NotNull Uri uri) { + ExternalFile externalFile = fileManager.fromUri(uri); + if (externalFile == null) { + showMessage("fileManager.fromUri() returned null externalFile, " + + "uri = " + uri.toString()); + return; + } - ((StartActivity) context).getRuntimePermissionsHelper().requestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, granted -> { - if (granted && presenter != null) { navigationController.presentController(loadingViewController); - presenter.doExport(settingsFile); - } else { - ((StartActivity) context).getRuntimePermissionsHelper().showPermissionRequiredDialog(context, - context.getString(R.string.update_storage_permission_required_title), - context.getString(R.string.storage_permission_required_to_export_settings), - this::onExportClicked); + presenter.doExport(externalFile); } - }); - } - private void showCreateDirectoryDialog() { - String state = Environment.getExternalStorageState(); - if (!Environment.MEDIA_MOUNTED.equals(state)) { - showMessage(context.getString(R.string.error_external_storage_is_not_mounted)); - return; - } - - // if we already have the permission and the default directory already exists - do not show - // the dialog again - if (((StartActivity) context).getRuntimePermissionsHelper().hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { - if (settingsFile.getParentFile().exists()) { - return; + @Override + public void onCancel(@NotNull String reason) { + showMessage(reason); } - } - - // Ask the user's permission to check whether the default directory exists and create it if it doesn't - new AlertDialog.Builder(context) - .setTitle(context.getString(R.string.default_directory_may_not_exist_title)) - .setMessage(context.getString(R.string.default_directory_may_not_exist_message)) - .setPositiveButton(context.getString(R.string.create), (dialog1, which) -> ((StartActivity) context).getRuntimePermissionsHelper().requestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, granted -> { - if (granted) { - onPermissionGrantedForDirectoryCreation(); - } - })) - .setNegativeButton(context.getString(R.string.do_not), null) - .create() - .show(); - } - - private void onPermissionGrantedForDirectoryCreation() { - if (settingsFile.getParentFile().exists()) { - return; - } - - if (!settingsFile.getParentFile().mkdirs()) { - showMessage(context.getString(R.string.could_not_create_dir_for_export_error_text, settingsFile.getParentFile().getAbsolutePath())); - } + }); } private void onImportClicked() { - String state = Environment.getExternalStorageState(); - if (!Environment.MEDIA_MOUNTED.equals(state)) { - showMessage(context.getString(R.string.error_external_storage_is_not_mounted)); - return; - } + fileManager.openChooseFileDialog(new FileChooserCallback() { + @Override + public void onResult(@NotNull Uri uri) { + ExternalFile externalFile = fileManager.fromUri(uri); + if (externalFile == null) { + showMessage("fileManager.fromUri() returned null externalFile, " + + "uri = " + uri.toString()); + return; + } - ((StartActivity) context).getRuntimePermissionsHelper().requestPermission(Manifest.permission.READ_EXTERNAL_STORAGE, granted -> { - if (granted) { - onPermissionGrantedForImport(); - } else { - ((StartActivity) context).getRuntimePermissionsHelper().showPermissionRequiredDialog(context, - context.getString(R.string.update_storage_permission_required_title), - context.getString(R.string.storage_permission_required_to_import_settings), - this::onImportClicked); + navigationController.presentController(loadingViewController); + presenter.doImport(externalFile); } - }); - } - - private void onPermissionGrantedForImport() { - String warningMessage = context.getString(R.string.import_warning_text, - settingsFile.getParentFile().getPath(), - settingsFile.getName()); - - AlertDialog dialog = new AlertDialog.Builder(context) - .setTitle(R.string.import_warning_title) - .setMessage(warningMessage) - .setPositiveButton(R.string.continue_text, (dialog1, which) -> onStartImportButtonClicked()) - .setNegativeButton(R.string.cancel, null) - .create(); - - dialog.show(); - } - private void onStartImportButtonClicked() { - if (presenter != null) { - navigationController.presentController(loadingViewController); - presenter.doImport(settingsFile); - } + @Override + public void onCancel(@NotNull String reason) { + showMessage(reason); + } + }); } @Override @@ -219,11 +166,8 @@ public void onSuccess(ImportExportRepository.ImportExport importExport) { if (importExport == ImportExportRepository.ImportExport.Import) { ((StartActivity) context).restartApp(); } else { - copyDirPathToClipboard(); clearAllChildControllers(); - - showMessage(context.getString(R.string.successfully_exported_text, - settingsFile.getAbsolutePath())); + showMessage(context.getString(R.string.successfully_exported_text)); if (callbacks != null) { callbacks.finish(); @@ -233,14 +177,6 @@ public void onSuccess(ImportExportRepository.ImportExport importExport) { } } - private void copyDirPathToClipboard() { - ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); - ClipData clip = ClipData.newPlainText("exported_file_path", settingsFile.getPath()); - - if (clipboard != null) { - clipboard.setPrimaryClip(clip); - } - } @Override public void onError(String message) { diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java index 1e4037c98f..76cd7679e0 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java @@ -245,8 +245,6 @@ public void onResult(@NotNull Uri uri) { ChanSettings.saveLocation.set(""); saveLocation.setDescription(uri.toString()); - - testMethod(uri); } @Override @@ -260,39 +258,6 @@ public void onCancel(@NotNull String reason) { } } - private void testMethod(@NotNull Uri uri) { - ExternalFile externalFile1 = fileManager.fromUri(uri) - .appendSubDirSegment("123") - .appendSubDirSegment("456") - .appendSubDirSegment("789") - .appendFileNameSegment("test123.txt") - .create(); - - boolean exists = fileManager.fromUri(uri) - .appendSubDirSegment("123") - .appendSubDirSegment("456") - .appendSubDirSegment("789") - .exists(); - - System.out.println(); - System.out.println(); - System.out.println(); - -// AbstractFile newDir = fileManager.newDir(externalFile, "test2"); -// AbstractFile createdDir = fileManager.createDir(newDir); -// -// AbstractFile newDir2 = fileManager.newDir(createdDir, "test2"); -// AbstractFile createdDir2 = fileManager.createDir(newDir2); -// -// AbstractFile newFile2 = fileManager.newFile(createdDir2, "test123.wav"); -// AbstractFile createdFile2 = fileManager.createFile(newFile2); -// -// System.out.println(createdFile2.isDirectory()); -// System.out.println(createdFile2.isFile()); -// System.out.println(createdFile2.isRawFile()); - - } - private void updateThreadFolderSetting() { if (ChanSettings.saveBoardFolder.get()) { threadFolderSetting.setEnabled(true); diff --git a/Kuroba/app/src/main/res/values/strings.xml b/Kuroba/app/src/main/res/values/strings.xml index 60fa1a0905..b5570cdf2f 100644 --- a/Kuroba/app/src/main/res/values/strings.xml +++ b/Kuroba/app/src/main/res/values/strings.xml @@ -49,15 +49,6 @@ along with this program. If not, see . Re-enable this permission in the app settings if you permanently disabled it." - -"Permission to access storage is required to export settings. - -Re-enable this permission in the app settings if you permanently disabled it." - - -"Permission to access storage is required to import settings. - -Re-enable this permission in the app settings if you permanently disabled it." %d minutes @@ -627,13 +618,8 @@ Don't have a 4chan Pass?
Export settings to a file Import settings Import settings from a file - Warning - You are about to import settings/saved pins/filters/hidden posts from a file. The file must be located at \"%1$s\" and have a name \"%2$s\". This will clear all your existing settings/pins/threads and replace them with the ones from the file. You MAY LOSE some of your settings/pins/hidden threads if you are trying to import a file with different app version (upgrade or downgrade). The app will be restarted. \nAre you sure you want to continue? - Exported successfully to \"%1$s\". File path has been copied to the clipboard" - External storage is not mounted + Exported successfully" Could not create directory for export file %1$s; you probably won\'t be able to export settings - The default directory may not exist yet. Do you want to check whether the directory exists and create it automatically? - Default directory may not exist Apply to replies Only on OP From 9d61e023e0f5900121c57d7c03dde0d5cb4c92e4 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 11 Aug 2019 18:55:56 +0300 Subject: [PATCH 016/184] (#172) Image saving now works (with bugs) with SAF --- .../adamantcheese/chan/core/Extensions.kt | 2 +- .../adamantcheese/chan/core/di/AppModule.java | 12 +- .../chan/core/saf/FileManager.kt | 58 ++++++- .../chan/core/saf/file/AbstractFile.kt | 22 ++- .../chan/core/saf/file/ExternalFile.kt | 161 +++++++++++++++--- .../chan/core/saf/file/RawFile.kt | 105 +++++++++--- .../chan/core/saver/ImageSaveTask.java | 38 +++-- .../chan/core/saver/ImageSaver.java | 66 +++++-- .../controller/MediaSettingsController.java | 47 +++++ 9 files changed, 427 insertions(+), 84 deletions(-) diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/Extensions.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/Extensions.kt index ba4ccd34d2..47234e5e6b 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/Extensions.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/Extensions.kt @@ -35,7 +35,7 @@ fun Uri.removeLastSegment(): Uri? { } val newSegments = this.pathSegments - .subList(0, pathSegments.lastIndex - 1) + .subList(0, pathSegments.lastIndex) return Uri.Builder() .appendManyEncoded(newSegments) diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/AppModule.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/AppModule.java index e5e56d8f08..ee7be46f20 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/AppModule.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/AppModule.java @@ -75,12 +75,6 @@ public ThemeHelper provideThemeHelper() { return new ThemeHelper(); } - @Provides - @Singleton - public ImageSaver provideImageSaver() { - return new ImageSaver(); - } - @Provides @Singleton public CaptchaHolder provideCaptchaHolder() { @@ -92,4 +86,10 @@ public CaptchaHolder provideCaptchaHolder() { public FileManager provideFileManager() { return new FileManager(applicationContext); } + + @Provides + @Singleton + public ImageSaver provideImageSaver(FileManager fileManager) { + return new ImageSaver(fileManager); + } } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt index acf39b7135..6b92c22fad 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt @@ -12,8 +12,11 @@ import com.github.adamantcheese.chan.core.saf.callback.StartActivityCallbacks import com.github.adamantcheese.chan.core.saf.file.AbstractFile import com.github.adamantcheese.chan.core.saf.file.ExternalFile import com.github.adamantcheese.chan.core.saf.file.RawFile +import com.github.adamantcheese.chan.core.settings.ChanSettings +import com.github.adamantcheese.chan.utils.IOUtils import com.github.adamantcheese.chan.utils.Logger import java.io.File +import java.io.IOException class FileManager( private val appContext: Context @@ -57,14 +60,16 @@ class FileManager( //======================================================= /** - * Create a raw file from a path + * Create a raw file from a path. + * Use this method to convert a file by this path into an AbstractFile. * */ fun fromPath(path: String): RawFile { return fromRawFile(File(path)) } /** - * Create RawFile from Java File + * Create RawFile from Java File. + * Use this method to convert this file into an AbstractFile. * */ fun fromRawFile(file: File): RawFile { if (file.isFile) { @@ -75,7 +80,9 @@ class FileManager( } /** - * Create an external file from Uri + * Create an external file from Uri. + * Use this method to convert external file uri (file that may be located at sd card) into an + * AbstractFile. * */ fun fromUri(uri: Uri): ExternalFile? { val documentFile = toDocumentFile(uri) @@ -97,6 +104,37 @@ class FileManager( } } + /** + * Use this method to create a new file that may be located at any user selected directory (it + * may be stored at sd card or even in google drive, anywhere) or if user has not selected an + * app directory via the SAF API it will be stored in the default external app directory + * (like /storage/Kuroba) + * */ + fun newFile(): AbstractFile { + val uri = ChanSettings.saveLocationUri.get() + if (uri.isNotEmpty()) { + return ExternalFile( + appContext, + AbstractFile.Root.DirRoot(Uri.parse(uri))) + } + + val path = ChanSettings.saveLocation.get() + return RawFile(AbstractFile.Root.DirRoot(File(path))) + } + + /** + * AbstractFiles are mutable, so if you want to append some directory to another AbstractFile to + * check whether it exists or not or something like this, you need to create a new AbstractFile + * from the other AbstractFile (Just like with regular files, e.g. File(oldFile, "subDir").exists() ) + * */ + fun fromAbstractFile(file: AbstractFile): AbstractFile { + return when (file) { + is RawFile -> RawFile(file.getFullRoot()) + is ExternalFile -> ExternalFile(appContext, file.getFullRoot()) + else -> throw IllegalArgumentException("Not implemented for ${file.javaClass.name}") + } + } + // TODO: may not work! private fun toDocumentFile(uri: Uri): DocumentFile? { if (!DocumentFile.isDocumentUri(appContext, uri)) { @@ -140,6 +178,20 @@ class FileManager( } } + fun copyFile(source: AbstractFile, destination: AbstractFile): Boolean { + try { + return source.getInputStream()?.use { inputStream -> + return@use destination.getOutputStream()?.use { outputStream -> + IOUtils.copy(inputStream, outputStream) + return@use true + } ?: false + } ?: false + } catch (e: IOException) { + Logger.e(TAG, "IOException while coping one file to another", e) + return false + } + } + companion object { private const val TAG = "FileManager" diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt index 35eccc001e..215d1d588c 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt @@ -1,6 +1,9 @@ package com.github.adamantcheese.chan.core.saf.file -abstract class AbstractFile( +import java.io.InputStream +import java.io.OutputStream + +abstract class AbstractFile( /** * /test/123/test2/filename.txt -> 4 segments * */ @@ -15,17 +18,21 @@ abstract class AbstractFile( /** * Appends a new subdirectory to the root directory * */ - abstract fun appendSubDirSegment(name: String): T + abstract fun appendSubDirSegment(name: String): T /** * Appends a file name to the root directory * */ - abstract fun appendFileNameSegment(name: String): T + abstract fun appendFileNameSegment(name: String): T /** * Creates a new file that consists of the root directory and segments (sub dirs or the file name) * */ - abstract fun create(): T? + abstract fun createNew(): T? + + fun create(): Boolean { + return createNew() != null + } abstract fun exists(): Boolean abstract fun isFile(): Boolean @@ -33,8 +40,13 @@ abstract class AbstractFile( abstract fun canRead(): Boolean abstract fun canWrite(): Boolean abstract fun name(): String? - abstract fun getParent(): T? + abstract fun getParent(): T? abstract fun getFullPath(): String + abstract fun delete(): Boolean + abstract fun getInputStream(): InputStream? + abstract fun getOutputStream(): OutputStream? + abstract fun getFullRoot(): Root + abstract fun getName(): String fun segmentsCount(): Int = segments.size diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt index 5134b24616..2d3ff8c7aa 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt @@ -8,17 +8,18 @@ import androidx.documentfile.provider.DocumentFile import com.github.adamantcheese.chan.core.appendManyEncoded import com.github.adamantcheese.chan.core.extension import com.github.adamantcheese.chan.core.getMimeFromFilename -import com.github.adamantcheese.chan.core.removeLastSegment import com.github.adamantcheese.chan.utils.Logger import java.io.FileDescriptor +import java.io.InputStream +import java.io.OutputStream class ExternalFile( private val appContext: Context, private val root: Root -) : AbstractFile() { +) : AbstractFile() { private val mimeTypeMap = MimeTypeMap.getSingleton() - override fun appendSubDirSegment(name: String): ExternalFile { + override fun appendSubDirSegment(name: String): T { if (root is Root.FileRoot) { throw IllegalStateException("root is already FileRoot, cannot append anything anymore") } @@ -37,10 +38,10 @@ class ExternalFile( } segments += Segment(name) - return this + return this as T } - override fun appendFileNameSegment(name: String): ExternalFile { + override fun appendFileNameSegment(name: String): T { if (root is Root.FileRoot) { throw IllegalStateException("root is already FileRoot, cannot append anything anymore") } @@ -54,10 +55,10 @@ class ExternalFile( } segments += Segment(name, true) - return this + return this as T } - override fun create(): ExternalFile? { + override fun createNew(): T? { if (root is Root.FileRoot) { throw IllegalStateException("root is already FileRoot, cannot append anything anymore") } @@ -105,7 +106,7 @@ class ExternalFile( // Ignore any left segments (which we shouldn't have) after encountering fileName // segment - return ExternalFile(appContext, Root.FileRoot(newFile.uri, segment.name)) + return ExternalFile(appContext, Root.FileRoot(newFile.uri, segment.name)) as T } } @@ -114,7 +115,7 @@ class ExternalFile( return null } - return ExternalFile(appContext, Root.DirRoot(newFile.uri)) + return ExternalFile(appContext, Root.DirRoot(newFile.uri)) as T } override fun exists(): Boolean = toDocumentFile()?.exists() ?: false @@ -124,19 +125,123 @@ class ExternalFile( override fun canWrite(): Boolean = toDocumentFile()?.canWrite() ?: false override fun name(): String? = root.name() - override fun getParent(): ExternalFile? { + override fun getParent(): T? { if (segments.isNotEmpty()) { removeLastSegment() - return this + return this as T } - val newUri = root.holder.removeLastSegment() - if (newUri == null) { - Logger.e(TAG, "getParent() removeLastSegment() returned null") + val parentUri = when (root) { + is Root.DirRoot -> DocumentFile.fromTreeUri(appContext, root.holder)?.parentFile?.uri + is Root.FileRoot -> DocumentFile.fromSingleUri(appContext, root.holder)?.parentFile?.uri + } + + if (parentUri == null) { + Logger.e(TAG, "getParent() parentUri == null") + return null + } + + return ExternalFile(appContext, Root.DirRoot(parentUri)) as T + } + + override fun getFullPath(): String { + return Uri.parse(root.holder.toString()).buildUpon() + .appendManyEncoded(segments.map { segment -> segment.name }) + .build() + .toString() + } + + override fun delete(): Boolean { + return toDocumentFile()?.delete() ?: false + } + + override fun getInputStream(): InputStream? { + val contentResolver = appContext.contentResolver + val documentFile = toDocumentFile() + + if (documentFile == null) { + Logger.e(TAG, "getInputStream() toDocumentFile() returned null") return null } - return ExternalFile(appContext, Root.DirRoot(newUri)) + if (!documentFile.exists()) { + Logger.e(TAG, "getInputStream() documentFile does not exist, uri = ${documentFile.uri}") + return null + } + + if (!documentFile.isFile) { + Logger.e(TAG, "getInputStream() documentFile is not a file, uri = ${documentFile.uri}") + return null + } + + if (!documentFile.canRead()) { + Logger.e(TAG, "getInputStream() cannot read from documentFile, uri = ${documentFile.uri}") + return null + } + + return contentResolver.openInputStream(documentFile.uri) + } + + override fun getOutputStream(): OutputStream? { + val contentResolver = appContext.contentResolver + val documentFile = toDocumentFile() + + if (documentFile == null) { + Logger.e(TAG, "getOutputStream() toDocumentFile() returned null") + return null + } + + if (!documentFile.exists()) { + Logger.e(TAG, "getOutputStream() documentFile does not exist, uri = ${documentFile.uri}") + return null + } + + if (!documentFile.isFile) { + Logger.e(TAG, "getOutputStream() documentFile is not a file, uri = ${documentFile.uri}") + return null + } + + if (!documentFile.canWrite()) { + Logger.e(TAG, "getOutputStream() cannot write to documentFile, uri = ${documentFile.uri}") + return null + } + + return contentResolver.openOutputStream(documentFile.uri) + } + + override fun getFullRoot(): Root { + return if (segments.isEmpty()) { + root as Root + } else { + val uriBuilder = root.holder.buildUpon() + + for (segment in segments) { + uriBuilder.appendEncodedPath(segment.name) + } + + val lastSegment = segments.last() + if (lastSegment.isFileName) { + return Root.FileRoot( + uriBuilder.build() as T, + lastSegment.name) + } + + return Root.DirRoot(uriBuilder.build() as T) + } + } + + override fun getName(): String { + if (segments.isNotEmpty() && segments.last().isFileName) { + return segments.last().name + } + + val documentFile = toDocumentFile() + if (documentFile == null) { + throw IllegalStateException("getName() toDocumentFile() returned null") + } + + return documentFile.name + ?:throw IllegalStateException("Could not extract file name from document file") } fun getParcelFileDescriptor(fileDescriptorMode: FileDescriptorMode): ParcelFileDescriptor? { @@ -146,6 +251,9 @@ class ExternalFile( } /** + * An extension function that allows to do something with a FileDescriptor without having + * to worry about not closing it in the end. + * * Example: * withFileDescriptor(FileDescriptorMode.Read) { fd -> * // Do anything here with FileDescriptor here, it will be closed automatically upon @@ -162,13 +270,6 @@ class ExternalFile( } ?: false } - override fun getFullPath(): String { - return Uri.parse(root.holder.toString()).buildUpon() - .appendManyEncoded(segments.map { segment -> segment.name }) - .build() - .toString() - } - private fun toDocumentFile(): DocumentFile? { val uri = if (segments.isEmpty()) { root.holder @@ -179,9 +280,19 @@ class ExternalFile( .build() } - return when (root) { - is Root.DirRoot -> DocumentFile.fromTreeUri(appContext, uri) - is Root.FileRoot -> DocumentFile.fromSingleUri(appContext, uri) + // If there are no segments check whether the root is a directory or a file + return if (segments.isEmpty()) { + when (root) { + is Root.DirRoot -> DocumentFile.fromTreeUri(appContext, uri) + is Root.FileRoot -> DocumentFile.fromSingleUri(appContext, uri) + } + } else { + // Otherwise if there are segments check whether the last segment is isFileName or not + if (!segments.last().isFileName) { + DocumentFile.fromTreeUri(appContext, uri) + } else { + DocumentFile.fromSingleUri(appContext, uri) + } } } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt index ffbfb3efb3..6ce96c1cc6 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt @@ -4,13 +4,14 @@ import com.github.adamantcheese.chan.core.appendMany import com.github.adamantcheese.chan.core.extension import com.github.adamantcheese.chan.utils.Logger import java.io.File -import java.lang.IllegalStateException +import java.io.InputStream +import java.io.OutputStream class RawFile( private val root: Root -) : AbstractFile() { +) : AbstractFile() { - override fun appendSubDirSegment(name: String): RawFile { + override fun appendSubDirSegment(name: String): T { if (root is Root.FileRoot) { throw IllegalStateException("root is already FileRoot, cannot append anything anymore") } @@ -19,7 +20,7 @@ class RawFile( throw IllegalStateException("Cannot append anything after file name has been appended") } - if (name.isNullOrBlank()) { + if (name.isBlank()) { throw IllegalArgumentException("Bad name: $name") } @@ -29,10 +30,10 @@ class RawFile( } segments += Segment(name) - return this + return this as T } - override fun appendFileNameSegment(name: String): RawFile { + override fun appendFileNameSegment(name: String): T { if (root is Root.FileRoot) { throw IllegalStateException("root is already FileRoot, cannot append anything anymore") } @@ -41,15 +42,15 @@ class RawFile( throw IllegalStateException("Cannot append anything after file name has been appended") } - if (name.isNullOrBlank()) { + if (name.isBlank()) { throw IllegalArgumentException("Bad name: $name") } segments += Segment(name, true) - return this + return this as T } - override fun create(): RawFile? { + override fun createNew(): T? { if (root is Root.FileRoot) { throw IllegalStateException("root is already FileRoot, cannot append anything anymore") } @@ -67,11 +68,11 @@ class RawFile( if (!segment.isFileName) { newFile = File(newFile, segment.name) } else { - return RawFile(Root.FileRoot(File(newFile, segment.name), segment.name)) + return RawFile(Root.FileRoot(File(newFile, segment.name), segment.name)) as T } } - return RawFile(Root.DirRoot(newFile)) + return RawFile(Root.DirRoot(newFile)) as T } override fun exists(): Boolean = toFile().exists() @@ -81,13 +82,13 @@ class RawFile( override fun canWrite(): Boolean = toFile().canWrite() override fun name(): String? = root.name() - override fun getParent(): RawFile? { + override fun getParent(): T? { if (segments.isNotEmpty()) { removeLastSegment() - return this + return this as T } - return RawFile(Root.DirRoot(root.holder.parentFile)) + return RawFile(Root.DirRoot(root.holder.parentFile)) as T } override fun getFullPath(): String { @@ -96,17 +97,81 @@ class RawFile( .absolutePath } + override fun delete(): Boolean { + return toFile().delete() + } + + override fun getInputStream(): InputStream? { + val file = toFile() + + if (!file.exists()) { + Logger.e(TAG, "getInputStream() file does not exist, path = ${file.absolutePath}") + return null + } + + if (!file.isFile) { + Logger.e(TAG, "getInputStream() file is not a file, path = ${file.absolutePath}") + return null + } + + if (!file.canRead()) { + Logger.e(TAG, "getInputStream() cannot read from file, path = ${file.absolutePath}") + return null + } + + return file.inputStream() + } + + override fun getOutputStream(): OutputStream? { + val file = toFile() + + if (!file.exists()) { + Logger.e(TAG, "getOutputStream() file does not exist, path = ${file.absolutePath}") + return null + } + + if (!file.isFile) { + Logger.e(TAG, "getOutputStream() file is not a file, path = ${file.absolutePath}") + return null + } + + if (!file.canWrite()) { + Logger.e(TAG, "getOutputStream() cannot write to file, path = ${file.absolutePath}") + return null + } + + return file.outputStream() + } + + override fun getFullRoot(): Root { + return if (segments.isEmpty()) { + root as Root + } else { + var newFile = File(root.holder.absolutePath) + + for (segment in segments) { + newFile = File(newFile, segment.name) + } + + val lastSegment = segments.last() + if (lastSegment.isFileName) { + return Root.FileRoot(newFile, lastSegment.name) as Root + } + + return Root.DirRoot(newFile) as Root + } + } + + override fun getName(): String { + return toFile().name + } + private fun toFile(): File { - val uri = if (segments.isEmpty()) { + return if (segments.isEmpty()) { root.holder } else { root.holder.appendMany(segments.map { segment -> segment.name }) } - - return when (root) { - is Root.DirRoot -> uri - is Root.FileRoot -> uri - } } companion object { diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaveTask.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaveTask.java index c20f560440..aa473e0065 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaveTask.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaveTask.java @@ -24,8 +24,10 @@ import com.github.adamantcheese.chan.core.cache.FileCacheListener; import com.github.adamantcheese.chan.core.model.PostImage; import com.github.adamantcheese.chan.core.model.orm.Loadable; +import com.github.adamantcheese.chan.core.saf.FileManager; +import com.github.adamantcheese.chan.core.saf.file.AbstractFile; +import com.github.adamantcheese.chan.core.saf.file.RawFile; import com.github.adamantcheese.chan.utils.AndroidUtils; -import com.github.adamantcheese.chan.utils.IOUtils; import com.github.adamantcheese.chan.utils.Logger; import java.io.File; @@ -41,11 +43,13 @@ public class ImageSaveTask extends FileCacheListener implements Runnable { @Inject FileCache fileCache; + @Inject + FileManager fileManager; private PostImage postImage; private Loadable loadable; private ImageSaveTaskCallback callback; - private File destination; + private AbstractFile destination; private boolean share; private String subFolder; @@ -73,11 +77,11 @@ public PostImage getPostImage() { return postImage; } - public void setDestination(File destination) { + public void setDestination(AbstractFile destination) { this.destination = destination; } - public File getDestination() { + public AbstractFile getDestination() { return destination; } @@ -128,7 +132,10 @@ private void deleteDestination() { private void onDestination() { success = true; - MediaScannerConnection.scanFile(getAppContext(), new String[]{destination.getAbsolutePath()}, null, (path, uri) -> { + String[] paths = {destination.getFullPath()}; + + // TODO: may not work + MediaScannerConnection.scanFile(getAppContext(), paths, null, (path, uri) -> { // Runs on a binder thread AndroidUtils.runOnUiThread(() -> afterScan(uri)); }); @@ -138,16 +145,27 @@ private boolean copyToDestination(File source) { boolean result = false; try { - File parent = destination.getParentFile(); - if (!parent.mkdirs() && !parent.isDirectory()) { - throw new IOException("Could not create parent directory"); + AbstractFile createdDestinationFile = destination.createNew(); + if (createdDestinationFile == null) { + throw new IOException("Could not create destination file, path = " + destination.getFullPath()); } - if (destination.isDirectory()) { + // If destination is a raw file then we need to check whether the parent directory exists. + // Otherwise we don't + if (createdDestinationFile instanceof RawFile) { + AbstractFile parent = fileManager.fromAbstractFile(createdDestinationFile).getParent(); + if (parent == null || (!parent.create() && !parent.isDirectory())) { + throw new IOException("Could not create parent directory"); + } + } + + if (createdDestinationFile.isDirectory()) { throw new IOException("Destination file is already a directory"); } - IOUtils.copyFile(source, destination); + if (!fileManager.copyFile(fileManager.fromRawFile(source), createdDestinationFile)) { + throw new IOException("Could not copy source file into destination"); + } result = true; } catch (IOException e) { diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaver.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaver.java index 6576e4733c..659b408ae8 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaver.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaver.java @@ -25,6 +25,8 @@ import com.github.adamantcheese.chan.R; import com.github.adamantcheese.chan.StartActivity; import com.github.adamantcheese.chan.core.model.PostImage; +import com.github.adamantcheese.chan.core.saf.FileManager; +import com.github.adamantcheese.chan.core.saf.file.AbstractFile; import com.github.adamantcheese.chan.core.settings.ChanSettings; import com.github.adamantcheese.chan.ui.helper.RuntimePermissionsHelper; import com.github.adamantcheese.chan.ui.service.SavingNotification; @@ -32,7 +34,6 @@ import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; -import java.io.File; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -45,12 +46,16 @@ public class ImageSaver implements ImageSaveTask.ImageSaveTaskCallback { private static final String TAG = "ImageSaver"; private static final int MAX_NAME_LENGTH = 50; private static final Pattern UNSAFE_CHARACTERS_PATTERN = Pattern.compile("[^a-zA-Z0-9._\\\\ -]"); + + private FileManager fileManager; private ExecutorService executor = Executors.newSingleThreadExecutor(); private int doneTasks = 0; private int totalTasks = 0; private Toast toast; - public ImageSaver() { + public ImageSaver(FileManager fileManager) { + this.fileManager = fileManager; + EventBus.getDefault().register(this); } @@ -58,11 +63,20 @@ public void startDownloadTask(Context context, final ImageSaveTask task) { PostImage postImage = task.getPostImage(); String name = ChanSettings.saveServerFilename.get() ? postImage.originalName : postImage.filename; String fileName = filterName(name + "." + postImage.extension); - File saveFile = new File(getSaveLocation(task), fileName); + + AbstractFile saveLocation = getSaveLocation(task); + AbstractFile saveFile = saveLocation.appendFileNameSegment(fileName); + while (saveFile.exists()) { - fileName = filterName(name + "_" + Long.toString(SystemClock.elapsedRealtimeNanos(), Character.MAX_RADIX) + "." + postImage.extension); - saveFile = new File(getSaveLocation(task), fileName); + String resultFileName = name + "_" + + Long.toString(SystemClock.elapsedRealtimeNanos(), Character.MAX_RADIX) + + "." + postImage.extension; + + fileName = filterName(resultFileName); + saveFile = fileManager.fromAbstractFile(saveLocation) + .appendFileNameSegment(fileName); } + task.setDestination(saveFile); if (hasPermission(context)) { @@ -106,14 +120,15 @@ public String getSubFolder(String name) { return filtered; } - public File getSaveLocation(ImageSaveTask task) { - String base = ChanSettings.saveLocation.get(); + public AbstractFile getSaveLocation(ImageSaveTask task) { String subFolder = task.getSubFolder(); + AbstractFile destination = fileManager.newFile(); + if (subFolder != null) { - return new File(base + File.separator + subFolder); - } else { - return new File(base); + destination.appendSubDirSegment(subFolder); } + + return destination; } @Override @@ -145,10 +160,15 @@ private void startBundledTaskInternal(String subFolder, List task for (ImageSaveTask task : tasks) { PostImage postImage = task.getPostImage(); String fileName = filterName(postImage.originalName + "." + postImage.extension); - task.setDestination(new File(getSaveLocation(task) + File.separator + subFolder + File.separator + fileName)); + AbstractFile saveLocation = getSaveLocation(task) + .appendSubDirSegment(subFolder) + .appendFileNameSegment(fileName); + + task.setDestination(saveLocation); startTask(task); } + updateNotification(); } @@ -180,15 +200,33 @@ private void showToast(ImageSaveTask task, boolean success, boolean wasAlbumSave toast.cancel(); } - String text = success ? - (wasAlbumSave ? getAppContext().getString(R.string.album_download_success, getSaveLocation(task).getPath()) : getAppContext().getString(R.string.image_save_as, task.getDestination().getName())) : - getString(R.string.image_save_failed); + String text = getText(task, success, wasAlbumSave); toast = Toast.makeText(getAppContext(), text, Toast.LENGTH_LONG); + if (task != null && !task.getShare()) { toast.show(); } } + private String getText(ImageSaveTask task, boolean success, boolean wasAlbumSave) { + String text; + if (success) { + if (wasAlbumSave) { + text = getAppContext().getString( + R.string.album_download_success, + getSaveLocation(task).getFullPath()); + } else { + text = getAppContext().getString( + R.string.image_save_as, + task.getDestination().getName()); + } + } else { + text = getString(R.string.image_save_failed); + } + + return text; + } + private String filterName(String name) { name = UNSAFE_CHARACTERS_PATTERN.matcher(name).replaceAll(""); if (name.length() == 0) { diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java index 76cd7679e0..092524a973 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java @@ -23,6 +23,7 @@ import com.github.adamantcheese.chan.R; import com.github.adamantcheese.chan.core.saf.FileManager; import com.github.adamantcheese.chan.core.saf.callback.DirectoryChooserCallback; +import com.github.adamantcheese.chan.core.saf.file.AbstractFile; import com.github.adamantcheese.chan.core.saf.file.ExternalFile; import com.github.adamantcheese.chan.core.settings.ChanSettings; import com.github.adamantcheese.chan.ui.settings.BooleanSettingView; @@ -245,6 +246,8 @@ public void onResult(@NotNull Uri uri) { ChanSettings.saveLocation.set(""); saveLocation.setDescription(uri.toString()); + +// testMethod(uri); } @Override @@ -258,6 +261,50 @@ public void onCancel(@NotNull String reason) { } } + private void testMethod(@NotNull Uri uri) { + ExternalFile externalFile1 = fileManager.fromUri(uri) + .appendSubDirSegment("123") + .appendSubDirSegment("456") + .appendSubDirSegment("789") + .appendFileNameSegment("test123.txt") + .createNew(); + + boolean exists = fileManager.fromUri(uri) + .appendSubDirSegment("123") + .appendSubDirSegment("456") + .appendSubDirSegment("789") + .exists(); + + System.out.println(); + System.out.println(); + System.out.println(); + System.out.println(); + + AbstractFile file = fileManager.newFile() + .appendSubDirSegment("1234") + .appendSubDirSegment("4566") + .appendFileNameSegment("filename.json") + .createNew(); + + System.out.println(); + System.out.println(); + System.out.println(); + +// AbstractFile newDir = fileManager.newDir(externalFile, "test2"); +// AbstractFile createdDir = fileManager.createDir(newDir); +// +// AbstractFile newDir2 = fileManager.newDir(createdDir, "test2"); +// AbstractFile createdDir2 = fileManager.createDir(newDir2); +// +// AbstractFile newFile2 = fileManager.newFile(createdDir2, "test123.wav"); +// AbstractFile createdFile2 = fileManager.createFile(newFile2); +// +// System.out.println(createdFile2.isDirectory()); +// System.out.println(createdFile2.isFile()); +// System.out.println(createdFile2.isRawFile()); + + } + private void updateThreadFolderSetting() { if (ChanSettings.saveBoardFolder.get()) { threadFolderSetting.setEnabled(true); From 05606fc6294b0a2970ff4745264c3e38997f6f81 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 17 Aug 2019 19:44:19 +0300 Subject: [PATCH 017/184] (#172) SAF fixes: Now ExternalFiles uses DocumentFile().listFiles() to find a file inside a directory. It's implemented this way because, apparently, it's the only way to get a fully working DocumentFile that supports such operations as exists(), getName(), canRead()/canWrite(), delete() etc and which getParent() returns an actual file and not the null. This slow, but I don't know another way to do this so that everything works as expected. --- .../chan/core/saf/FileManager.kt | 22 ++- .../chan/core/saf/file/AbstractFile.kt | 2 + .../chan/core/saf/file/ExternalFile.kt | 157 +++++++++++------- .../chan/core/saf/file/RawFile.kt | 5 + .../controller/MediaSettingsController.java | 127 +++++++++----- 5 files changed, 207 insertions(+), 106 deletions(-) diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt index 6b92c22fad..1f26538a58 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt @@ -17,6 +17,7 @@ import com.github.adamantcheese.chan.utils.IOUtils import com.github.adamantcheese.chan.utils.Logger import java.io.File import java.io.IOException +import java.lang.IllegalStateException class FileManager( private val appContext: Context @@ -84,23 +85,21 @@ class FileManager( * Use this method to convert external file uri (file that may be located at sd card) into an * AbstractFile. * */ - fun fromUri(uri: Uri): ExternalFile? { + fun fromUri(uri: Uri): ExternalFile { val documentFile = toDocumentFile(uri) if (documentFile == null) { - Logger.e(TAG, "fromUri() toDocumentFile() returned null") - return null + throw IllegalStateException("fromUri() toDocumentFile() returned null") } return if (documentFile.isFile) { - val filename = queryTreeName(uri) + val filename = documentFile.name if (filename == null) { - Logger.e(TAG, "fromUri() queryTreeName() returned null") - return null + throw IllegalStateException("fromUri() queryTreeName() returned null") } - ExternalFile(appContext, AbstractFile.Root.FileRoot(uri, filename)) + ExternalFile(appContext, AbstractFile.Root.FileRoot(documentFile, filename)) } else { - ExternalFile(appContext, AbstractFile.Root.DirRoot(uri)) + ExternalFile(appContext, AbstractFile.Root.DirRoot(documentFile)) } } @@ -113,9 +112,14 @@ class FileManager( fun newFile(): AbstractFile { val uri = ChanSettings.saveLocationUri.get() if (uri.isNotEmpty()) { + val rootDirectory = DocumentFile.fromTreeUri(appContext, Uri.parse(uri)) + if (rootDirectory == null) { + throw IllegalStateException("Root directory cannot be null!") + } + return ExternalFile( appContext, - AbstractFile.Root.DirRoot(Uri.parse(uri))) + AbstractFile.Root.DirRoot(rootDirectory)) } val path = ChanSettings.saveLocation.get() diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt index 215d1d588c..6ceb08acd8 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt @@ -1,5 +1,6 @@ package com.github.adamantcheese.chan.core.saf.file +import androidx.documentfile.provider.DocumentFile import java.io.InputStream import java.io.OutputStream @@ -47,6 +48,7 @@ abstract class AbstractFile( abstract fun getOutputStream(): OutputStream? abstract fun getFullRoot(): Root abstract fun getName(): String + abstract fun findFile(fileName: String): DocumentFile? fun segmentsCount(): Int = segments.size diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt index 2d3ff8c7aa..863a741bdc 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt @@ -15,7 +15,7 @@ import java.io.OutputStream class ExternalFile( private val appContext: Context, - private val root: Root + private val root: Root ) : AbstractFile() { private val mimeTypeMap = MimeTypeMap.getSingleton() @@ -63,13 +63,6 @@ class ExternalFile( throw IllegalStateException("root is already FileRoot, cannot append anything anymore") } - val rootDir = DocumentFile.fromTreeUri(appContext, root.holder) - if (rootDir == null) { - // Couldn't create a DocumentFile from the root - Logger.e(TAG, "DocumentFile.fromTreeUri returned null, root.uri = ${root.holder}") - return null - } - if (segments.isEmpty()) { // Root is probably already exists and there is no point in creating it again so just // return null here @@ -80,7 +73,7 @@ class ExternalFile( var newFile: DocumentFile? = null for (segment in segments) { - val file = newFile ?: rootDir + val file = newFile ?: root.holder val prevFile = file.findFile(segment.name) if (prevFile != null) { @@ -106,7 +99,7 @@ class ExternalFile( // Ignore any left segments (which we shouldn't have) after encountering fileName // segment - return ExternalFile(appContext, Root.FileRoot(newFile.uri, segment.name)) as T + return ExternalFile(appContext, Root.FileRoot(newFile, segment.name)) as T } } @@ -115,10 +108,31 @@ class ExternalFile( return null } - return ExternalFile(appContext, Root.DirRoot(newFile.uri)) as T + if (segments.size < 1) { + Logger.e(TAG, "Must be at least one segment!") + return null + } + + val lastSegment = segments.last() + val isLastSegmentFilename = lastSegment.isFileName + + val root = if (isLastSegmentFilename) { + Root.FileRoot(newFile, lastSegment.name) + } else { + Root.DirRoot(newFile) + } + + return ExternalFile(appContext, root) as T + } + + override fun exists(): Boolean { + if (segments.isEmpty()) { + return root.holder.exists() + } + + return toDocumentFile()?.exists() ?: false } - override fun exists(): Boolean = toDocumentFile()?.exists() ?: false override fun isFile(): Boolean = toDocumentFile()?.isFile ?: false override fun isDirectory(): Boolean = toDocumentFile()?.isDirectory ?: false override fun canRead(): Boolean = toDocumentFile()?.canRead() ?: false @@ -131,17 +145,17 @@ class ExternalFile( return this as T } - val parentUri = when (root) { - is Root.DirRoot -> DocumentFile.fromTreeUri(appContext, root.holder)?.parentFile?.uri - is Root.FileRoot -> DocumentFile.fromSingleUri(appContext, root.holder)?.parentFile?.uri + val parent = when (root) { + is Root.DirRoot -> root.holder.parentFile + is Root.FileRoot -> root.holder.parentFile } - if (parentUri == null) { + if (parent == null) { Logger.e(TAG, "getParent() parentUri == null") return null } - return ExternalFile(appContext, Root.DirRoot(parentUri)) as T + return ExternalFile(appContext, Root.DirRoot(parent)) as T } override fun getFullPath(): String { @@ -210,24 +224,26 @@ class ExternalFile( } override fun getFullRoot(): Root { - return if (segments.isEmpty()) { - root as Root - } else { - val uriBuilder = root.holder.buildUpon() - - for (segment in segments) { - uriBuilder.appendEncodedPath(segment.name) - } - - val lastSegment = segments.last() - if (lastSegment.isFileName) { - return Root.FileRoot( - uriBuilder.build() as T, - lastSegment.name) - } - - return Root.DirRoot(uriBuilder.build() as T) - } +// return if (segments.isEmpty()) { +// root as Root +// } else { +// val uriBuilder = root.holder.buildUpon() +// +// for (segment in segments) { +// uriBuilder.appendEncodedPath(segment.name) +// } +// +// val lastSegment = segments.last() +// if (lastSegment.isFileName) { +// return Root.FileRoot( +// uriBuilder.build() as T, +// lastSegment.name) +// } +// +// return Root.DirRoot(uriBuilder.build() as T) +// } + + TODO("Use listFiles() to build full root???") } override fun getName(): String { @@ -241,12 +257,48 @@ class ExternalFile( } return documentFile.name - ?:throw IllegalStateException("Could not extract file name from document file") + ?: throw IllegalStateException("Could not extract file name from document file") + } + + override fun findFile(fileName: String): DocumentFile? { + if (root is Root.FileRoot) { + throw IllegalStateException("Cannot use FileRoot as directory") + } + + val filteredSegments = segments + .map { it.name } + + var dirTree = root.holder + + for (segment in filteredSegments) { + // FIXME: SLOW!!! + for (documentFile in dirTree.listFiles()) { + if (documentFile.name != null && documentFile.name == segment) { + dirTree = documentFile + break + } + } + } + + // FIXME: SLOW!!! + for (documentFile in dirTree.listFiles()) { + if (documentFile.name != null && documentFile.name == fileName) { + return documentFile + } + } + + if (dirTree.name == fileName) { + return dirTree + } + + // Not found + return null } + fun getParcelFileDescriptor(fileDescriptorMode: FileDescriptorMode): ParcelFileDescriptor? { return appContext.contentResolver.openFileDescriptor( - root.holder, + root.holder.uri, fileDescriptorMode.mode) } @@ -271,29 +323,22 @@ class ExternalFile( } private fun toDocumentFile(): DocumentFile? { - val uri = if (segments.isEmpty()) { - root.holder - } else { - root.holder - .buildUpon() - .appendManyEncoded(segments.map { segment -> segment.name }) - .build() + if (segments.isEmpty()) { + return root.holder } - // If there are no segments check whether the root is a directory or a file - return if (segments.isEmpty()) { - when (root) { - is Root.DirRoot -> DocumentFile.fromTreeUri(appContext, uri) - is Root.FileRoot -> DocumentFile.fromSingleUri(appContext, uri) - } - } else { - // Otherwise if there are segments check whether the last segment is isFileName or not - if (!segments.last().isFileName) { - DocumentFile.fromTreeUri(appContext, uri) - } else { - DocumentFile.fromSingleUri(appContext, uri) + var documentFile: DocumentFile? = root.holder + + for (segment in segments) { + if (documentFile == null) { + break } + + documentFile = documentFile.listFiles() + .firstOrNull { file -> file.name == segment.name } } + + return documentFile } enum class FileDescriptorMode(val mode: String) { diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt index 6ce96c1cc6..77017cc254 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt @@ -1,5 +1,6 @@ package com.github.adamantcheese.chan.core.saf.file +import androidx.documentfile.provider.DocumentFile import com.github.adamantcheese.chan.core.appendMany import com.github.adamantcheese.chan.core.extension import com.github.adamantcheese.chan.utils.Logger @@ -166,6 +167,10 @@ class RawFile( return toFile().name } + override fun findFile(fileName: String): DocumentFile? { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + private fun toFile(): File { return if (segments.isEmpty()) { root.holder diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java index 092524a973..10463be3ae 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java @@ -20,6 +20,8 @@ import android.net.Uri; import android.widget.Toast; +import androidx.documentfile.provider.DocumentFile; + import com.github.adamantcheese.chan.R; import com.github.adamantcheese.chan.core.saf.FileManager; import com.github.adamantcheese.chan.core.saf.callback.DirectoryChooserCallback; @@ -247,7 +249,7 @@ public void onResult(@NotNull Uri uri) { saveLocation.setDescription(uri.toString()); -// testMethod(uri); + testMethod(uri); } @Override @@ -262,47 +264,90 @@ public void onCancel(@NotNull String reason) { } private void testMethod(@NotNull Uri uri) { - ExternalFile externalFile1 = fileManager.fromUri(uri) - .appendSubDirSegment("123") - .appendSubDirSegment("456") - .appendSubDirSegment("789") - .appendFileNameSegment("test123.txt") - .createNew(); - - boolean exists = fileManager.fromUri(uri) - .appendSubDirSegment("123") - .appendSubDirSegment("456") - .appendSubDirSegment("789") - .exists(); - - System.out.println(); - System.out.println(); - System.out.println(); - System.out.println(); - - AbstractFile file = fileManager.newFile() - .appendSubDirSegment("1234") - .appendSubDirSegment("4566") - .appendFileNameSegment("filename.json") - .createNew(); - - System.out.println(); - System.out.println(); - System.out.println(); - -// AbstractFile newDir = fileManager.newDir(externalFile, "test2"); -// AbstractFile createdDir = fileManager.createDir(newDir); -// -// AbstractFile newDir2 = fileManager.newDir(createdDir, "test2"); -// AbstractFile createdDir2 = fileManager.createDir(newDir2); -// -// AbstractFile newFile2 = fileManager.newFile(createdDir2, "test123.wav"); -// AbstractFile createdFile2 = fileManager.createFile(newFile2); -// -// System.out.println(createdFile2.isDirectory()); -// System.out.println(createdFile2.isFile()); -// System.out.println(createdFile2.isRawFile()); + { + ExternalFile externalFile = fileManager.fromUri(uri) + .appendSubDirSegment("123") + .appendSubDirSegment("456") + .appendSubDirSegment("789") + .appendFileNameSegment("test123.txt") + .createNew(); + + if (externalFile == null || !externalFile.exists()) { + throw new RuntimeException("Couldn't create test123.txt"); + } + + if (!externalFile.name().equals("test123.txt")) { + throw new RuntimeException("externalFile name != test123.txt"); + } + + boolean externalFile2Exists = fileManager.fromUri(uri) + .appendSubDirSegment("123") + .appendSubDirSegment("456") + .appendSubDirSegment("789") + .exists(); + + if (!externalFile2Exists) { + throw new RuntimeException("789 directory does not exist"); + } + + if (!externalFile.delete() && externalFile.exists()) { + throw new RuntimeException("Couldn't delete test123.txt"); + } + + AbstractFile parent1 = externalFile.getParent(); + if (!parent1.delete() && parent1.exists()) { + throw new RuntimeException("Couldn't delete 789"); + } + + AbstractFile parent2 = parent1.getParent(); + if (!parent2.delete() && parent2.exists()) { + throw new RuntimeException("Couldn't delete 456"); + } + + AbstractFile parent3 = parent2.getParent(); + if (!parent3.delete() && parent3.exists()) { + throw new RuntimeException("Couldn't delete 123"); + } + } + + { + AbstractFile externalFile = fileManager.newFile() + .appendSubDirSegment("1234") + .appendSubDirSegment("4566") + .appendFileNameSegment("filename.json") + .createNew(); + + if (externalFile == null || !externalFile.exists()) { + throw new RuntimeException("Couldn't create filename.json"); + } + + if (!externalFile.name().equals("filename.json")) { + throw new RuntimeException("externalFile1 name != filename.json"); + } + + AbstractFile dir = fileManager.newFile() + .appendSubDirSegment("1234") + .appendSubDirSegment("4566"); + + DocumentFile foundFile = dir.findFile("filename.json"); + if (foundFile == null || !foundFile.exists()) { + throw new RuntimeException("Couldn't find filename.json"); + } + + AbstractFile parent = externalFile.getParent().getParent(); + if (!parent.delete() && parent.exists()) { + throw new RuntimeException("Couldn't delete /1234/4566/filename.json"); + } + } + + { + ExternalFile externalFile = fileManager.fromUri(uri); + if (externalFile.getParent() != null) { + throw new RuntimeException("Root directory parent is not null!"); + } + } + System.out.println("All tests passed!"); } private void updateThreadFolderSetting() { From 29cce2a8a729e3a1d20161cb51a403c4025c0ba7 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 17 Aug 2019 20:48:47 +0300 Subject: [PATCH 018/184] (#172) Image saving now works with SAF! Add more tests for AbstractFile --- .../chan/core/saf/FileManager.kt | 23 +------ .../chan/core/saf/file/AbstractFile.kt | 12 +++- .../chan/core/saf/file/ExternalFile.kt | 61 ++++++++++--------- .../chan/core/saf/file/RawFile.kt | 26 ++------ .../chan/core/saver/ImageSaver.java | 3 +- .../controller/MediaSettingsController.java | 48 +++++++++++++++ 6 files changed, 98 insertions(+), 75 deletions(-) diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt index 1f26538a58..c71d74b01e 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt @@ -133,13 +133,12 @@ class FileManager( * */ fun fromAbstractFile(file: AbstractFile): AbstractFile { return when (file) { - is RawFile -> RawFile(file.getFullRoot()) - is ExternalFile -> ExternalFile(appContext, file.getFullRoot()) + is RawFile -> RawFile(file.root(), file.segments()) + is ExternalFile -> ExternalFile(appContext, file.root(), file.segments()) else -> throw IllegalArgumentException("Not implemented for ${file.javaClass.name}") } } - // TODO: may not work! private fun toDocumentFile(uri: Uri): DocumentFile? { if (!DocumentFile.isDocumentUri(appContext, uri)) { Logger.e(TAG, "Not a DocumentFile, uri = $uri") @@ -164,24 +163,6 @@ class FileManager( } } - // TODO: may not work! - private fun queryTreeName(uri: Uri): String? { - val contentResolver = appContext.contentResolver - - try { - return contentResolver.query(uri, FILENAME_PROJECTION, null, null, null)?.use { cursor -> - if (cursor.moveToNext()) { - return cursor.getString(0) - } - - return null - } - } catch (e: Throwable) { - Logger.e(TAG, "Error while trying to query for name, uri = $uri", e) - return null - } - } - fun copyFile(source: AbstractFile, destination: AbstractFile): Boolean { try { return source.getInputStream()?.use { inputStream -> diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt index 6ceb08acd8..8cc7f67b68 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt @@ -8,7 +8,7 @@ abstract class AbstractFile( /** * /test/123/test2/filename.txt -> 4 segments * */ - protected val segments: MutableList = mutableListOf() + protected val segments: MutableList ) { /** * We can't append anything if the last segment's isFileName is true. @@ -35,6 +35,8 @@ abstract class AbstractFile( return createNew() != null } + abstract fun root(): Root + abstract fun segments(): MutableList abstract fun exists(): Boolean abstract fun isFile(): Boolean abstract fun isDirectory(): Boolean @@ -46,7 +48,6 @@ abstract class AbstractFile( abstract fun delete(): Boolean abstract fun getInputStream(): InputStream? abstract fun getOutputStream(): OutputStream? - abstract fun getFullRoot(): Root abstract fun getName(): String abstract fun findFile(fileName: String): DocumentFile? @@ -83,6 +84,13 @@ abstract class AbstractFile( return null } + fun clone(): Root { + return when (this) { + is DirRoot<*> -> DirRoot(holder) + is FileRoot<*> -> FileRoot(holder, fileName) + } + } + /** * /test/123/test2 * or diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt index 863a741bdc..1557d807e6 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt @@ -15,8 +15,9 @@ import java.io.OutputStream class ExternalFile( private val appContext: Context, - private val root: Root -) : AbstractFile() { + private val root: Root, + segments: MutableList = mutableListOf() +) : AbstractFile(segments) { private val mimeTypeMap = MimeTypeMap.getSingleton() override fun appendSubDirSegment(name: String): T { @@ -125,6 +126,9 @@ class ExternalFile( return ExternalFile(appContext, root) as T } + override fun root(): Root = root.clone() as Root + override fun segments(): MutableList = segments.toMutableList() + override fun exists(): Boolean { if (segments.isEmpty()) { return root.holder.exists() @@ -223,29 +227,6 @@ class ExternalFile( return contentResolver.openOutputStream(documentFile.uri) } - override fun getFullRoot(): Root { -// return if (segments.isEmpty()) { -// root as Root -// } else { -// val uriBuilder = root.holder.buildUpon() -// -// for (segment in segments) { -// uriBuilder.appendEncodedPath(segment.name) -// } -// -// val lastSegment = segments.last() -// if (lastSegment.isFileName) { -// return Root.FileRoot( -// uriBuilder.build() as T, -// lastSegment.name) -// } -// -// return Root.DirRoot(uriBuilder.build() as T) -// } - - TODO("Use listFiles() to build full root???") - } - override fun getName(): String { if (segments.isNotEmpty() && segments.last().isFileName) { return segments.last().name @@ -327,20 +308,40 @@ class ExternalFile( return root.holder } - var documentFile: DocumentFile? = root.holder + var documentFile: DocumentFile = root.holder + var index = 0 - for (segment in segments) { - if (documentFile == null) { + for (i in 0 until segments.size) { + val segment = segments[i] + + val file = documentFile.listFiles() + .firstOrNull { file -> file.name == segment.name } + + if (file == null) { break } - documentFile = documentFile.listFiles() - .firstOrNull { file -> file.name == segment.name } + documentFile = file + ++index + } + + if (index != segments.size) { + return createDocumentFileFromUri(documentFile.uri, index) } return documentFile } + private fun createDocumentFileFromUri(uri: Uri, index: Int): DocumentFile? { + val builder = uri.buildUpon() + + for (i in index until segments.size) { + builder.appendEncodedPath(segments[i].name) + } + + return DocumentFile.fromSingleUri(appContext, builder.build()) + } + enum class FileDescriptorMode(val mode: String) { Read("r"), Write("w"), diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt index 77017cc254..f161ab68a8 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt @@ -9,8 +9,9 @@ import java.io.InputStream import java.io.OutputStream class RawFile( - private val root: Root -) : AbstractFile() { + private val root: Root, + segments: MutableList = mutableListOf() +) : AbstractFile(segments) { override fun appendSubDirSegment(name: String): T { if (root is Root.FileRoot) { @@ -76,6 +77,8 @@ class RawFile( return RawFile(Root.DirRoot(newFile)) as T } + override fun root(): Root = root.clone() as Root + override fun segments(): MutableList = segments.toMutableList() override fun exists(): Boolean = toFile().exists() override fun isFile(): Boolean = toFile().isFile override fun isDirectory(): Boolean = toFile().isDirectory @@ -144,25 +147,6 @@ class RawFile( return file.outputStream() } - override fun getFullRoot(): Root { - return if (segments.isEmpty()) { - root as Root - } else { - var newFile = File(root.holder.absolutePath) - - for (segment in segments) { - newFile = File(newFile, segment.name) - } - - val lastSegment = segments.last() - if (lastSegment.isFileName) { - return Root.FileRoot(newFile, lastSegment.name) as Root - } - - return Root.DirRoot(newFile) as Root - } - } - override fun getName(): String { return toFile().name } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaver.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaver.java index 659b408ae8..acac6948d8 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaver.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaver.java @@ -65,7 +65,8 @@ public void startDownloadTask(Context context, final ImageSaveTask task) { String fileName = filterName(name + "." + postImage.extension); AbstractFile saveLocation = getSaveLocation(task); - AbstractFile saveFile = saveLocation.appendFileNameSegment(fileName); + AbstractFile saveFile = fileManager.fromAbstractFile(saveLocation) + .appendFileNameSegment(fileName); while (saveFile.exists()) { String resultFileName = name + "_" + diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java index 10463be3ae..03a033cf08 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java @@ -272,6 +272,14 @@ private void testMethod(@NotNull Uri uri) { .appendFileNameSegment("test123.txt") .createNew(); + if (!externalFile.isFile()) { + throw new RuntimeException("test123.txt is not a file"); + } + + if (externalFile.isDirectory()) { + throw new RuntimeException("test123.txt is a directory"); + } + if (externalFile == null || !externalFile.exists()) { throw new RuntimeException("Couldn't create test123.txt"); } @@ -295,16 +303,36 @@ private void testMethod(@NotNull Uri uri) { } AbstractFile parent1 = externalFile.getParent(); + if (!parent1.getName().equals("789")) { + throw new RuntimeException("Parent1.name != 789, name = " + parent1.getName()); + } + + if (parent1.isFile()) { + throw new RuntimeException("789 is a file"); + } + + if (!parent1.isDirectory()) { + throw new RuntimeException("789 is not a directory"); + } + if (!parent1.delete() && parent1.exists()) { throw new RuntimeException("Couldn't delete 789"); } AbstractFile parent2 = parent1.getParent(); + if (!parent2.getName().equals("456")) { + throw new RuntimeException("Parent1.name != 456, name = " + parent2.getName()); + } + if (!parent2.delete() && parent2.exists()) { throw new RuntimeException("Couldn't delete 456"); } AbstractFile parent3 = parent2.getParent(); + if (!parent3.getName().equals("123")) { + throw new RuntimeException("Parent1.name != 123, name = " + parent3.getName()); + } + if (!parent3.delete() && parent3.exists()) { throw new RuntimeException("Couldn't delete 123"); } @@ -321,6 +349,14 @@ private void testMethod(@NotNull Uri uri) { throw new RuntimeException("Couldn't create filename.json"); } + if (!externalFile.isFile()) { + throw new RuntimeException("filename.json is not a file"); + } + + if (externalFile.isDirectory()) { + throw new RuntimeException("filename.json is not a directory"); + } + if (!externalFile.name().equals("filename.json")) { throw new RuntimeException("externalFile1 name != filename.json"); } @@ -329,12 +365,20 @@ private void testMethod(@NotNull Uri uri) { .appendSubDirSegment("1234") .appendSubDirSegment("4566"); + if (!dir.getName().equals("4566")) { + throw new RuntimeException("dir.name != 4566, name = " + dir.getName()); + } + DocumentFile foundFile = dir.findFile("filename.json"); if (foundFile == null || !foundFile.exists()) { throw new RuntimeException("Couldn't find filename.json"); } AbstractFile parent = externalFile.getParent().getParent(); + if (!parent.getName().equals("1234")) { + throw new RuntimeException("dir.name != 1234, name = " + parent.getName()); + } + if (!parent.delete() && parent.exists()) { throw new RuntimeException("Couldn't delete /1234/4566/filename.json"); } @@ -342,6 +386,10 @@ private void testMethod(@NotNull Uri uri) { { ExternalFile externalFile = fileManager.fromUri(uri); + if (!externalFile.getName().equals("Test")) { + throw new RuntimeException("externalFile.name != Test, name = " + externalFile.getName()); + } + if (externalFile.getParent() != null) { throw new RuntimeException("Root directory parent is not null!"); } From ff93a346f52c4cb7c8c9ee49638945db066d4a2c Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 18 Aug 2019 11:55:02 +0300 Subject: [PATCH 019/184] (#172) Remove fileManager.fromAbstractFile() method, add AbstractFile.clone() method --- .../chan/core/saf/FileManager.kt | 40 ++++++++----------- .../chan/core/saf/file/AbstractFile.kt | 12 ++++-- .../chan/core/saf/file/ExternalFile.kt | 6 ++- .../chan/core/saf/file/RawFile.kt | 6 ++- .../chan/core/saver/ImageSaveTask.java | 6 ++- .../chan/core/saver/ImageSaver.java | 6 ++- 6 files changed, 40 insertions(+), 36 deletions(-) diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt index c71d74b01e..20c758fa09 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt @@ -24,6 +24,9 @@ class FileManager( ) { private val fileChooser = FileChooser(appContext) + /** + * Used for calling Android File picker + * */ fun setCallbacks(startActivityCallbacks: StartActivityCallbacks) { fileChooser.setCallbacks(startActivityCallbacks) } @@ -62,7 +65,7 @@ class FileManager( /** * Create a raw file from a path. - * Use this method to convert a file by this path into an AbstractFile. + * Use this method to convert a java File by this path into an AbstractFile. * */ fun fromPath(path: String): RawFile { return fromRawFile(File(path)) @@ -70,7 +73,7 @@ class FileManager( /** * Create RawFile from Java File. - * Use this method to convert this file into an AbstractFile. + * Use this method to convert a java File into an AbstractFile. * */ fun fromRawFile(file: File): RawFile { if (file.isFile) { @@ -104,10 +107,8 @@ class FileManager( } /** - * Use this method to create a new file that may be located at any user selected directory (it - * may be stored at sd card or even in google drive, anywhere) or if user has not selected an - * app directory via the SAF API it will be stored in the default external app directory - * (like /storage/Kuroba) + * Instantiates a new AbstractFile with the root being in the app's base directory (the Kuroba + * directory in case of using raw file api and the user's selected directory in case of using SAF). * */ fun newFile(): AbstractFile { val uri = ChanSettings.saveLocationUri.get() @@ -126,19 +127,6 @@ class FileManager( return RawFile(AbstractFile.Root.DirRoot(File(path))) } - /** - * AbstractFiles are mutable, so if you want to append some directory to another AbstractFile to - * check whether it exists or not or something like this, you need to create a new AbstractFile - * from the other AbstractFile (Just like with regular files, e.g. File(oldFile, "subDir").exists() ) - * */ - fun fromAbstractFile(file: AbstractFile): AbstractFile { - return when (file) { - is RawFile -> RawFile(file.root(), file.segments()) - is ExternalFile -> ExternalFile(appContext, file.root(), file.segments()) - else -> throw IllegalArgumentException("Not implemented for ${file.javaClass.name}") - } - } - private fun toDocumentFile(uri: Uri): DocumentFile? { if (!DocumentFile.isDocumentUri(appContext, uri)) { Logger.e(TAG, "Not a DocumentFile, uri = $uri") @@ -146,8 +134,11 @@ class FileManager( } val treeUri = try { + // Will throw an exception if uri is not a treeUri. Hacky as fuck but I don't know + // another way to check it. DocumentFile.fromTreeUri(appContext, uri) - } catch (e: IllegalArgumentException) { + } catch (ignored: IllegalArgumentException) { + Logger.d(TAG, "Uri is not a treeUri, uri = $uri") null } @@ -158,12 +149,15 @@ class FileManager( return try { DocumentFile.fromSingleUri(appContext, uri) } catch (e: IllegalArgumentException) { - Logger.e(TAG, "provided uri is neither a treeUri nor singleUri, uri = ${uri}") + Logger.e(TAG, "Provided uri is neither a treeUri nor singleUri, uri = $uri") null } } - fun copyFile(source: AbstractFile, destination: AbstractFile): Boolean { + /** + * Copy one file's contents into another + * */ + fun copyFileContents(source: AbstractFile, destination: AbstractFile): Boolean { try { return source.getInputStream()?.use { inputStream -> return@use destination.getOutputStream()?.use { outputStream -> @@ -179,7 +173,5 @@ class FileManager( companion object { private const val TAG = "FileManager" - - private val FILENAME_PROJECTION = arrayOf(DocumentsContract.Document.COLUMN_DISPLAY_NAME) } } \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt index 8cc7f67b68..1bf271d0aa 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt @@ -35,8 +35,14 @@ abstract class AbstractFile( return createNew() != null } - abstract fun root(): Root - abstract fun segments(): MutableList + /** + * When doing something with an AbstractFile (like appending a subdir or a filename) the + * AbstractFile will change because it's mutable. So if you don't want to change the original + * AbstractFile you need to make a copy via this method (like, if you want to search for + * a couple of files in the same directory you would want to clone the directory + * AbstractFile and then append the filename to those copies) + * */ + abstract fun clone(): T abstract fun exists(): Boolean abstract fun isFile(): Boolean abstract fun isDirectory(): Boolean @@ -51,8 +57,6 @@ abstract class AbstractFile( abstract fun getName(): String abstract fun findFile(fileName: String): DocumentFile? - fun segmentsCount(): Int = segments.size - /** * Removes the last appended segment if there are any * e.g: /test/123/test2 -> /test/123 -> /test diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt index 1557d807e6..1fccf7d603 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt @@ -126,8 +126,10 @@ class ExternalFile( return ExternalFile(appContext, root) as T } - override fun root(): Root = root.clone() as Root - override fun segments(): MutableList = segments.toMutableList() + override fun clone(): T = ExternalFile( + appContext, + root.clone(), + segments.toMutableList()) as T override fun exists(): Boolean { if (segments.isEmpty()) { diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt index f161ab68a8..d76546d589 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt @@ -77,8 +77,10 @@ class RawFile( return RawFile(Root.DirRoot(newFile)) as T } - override fun root(): Root = root.clone() as Root - override fun segments(): MutableList = segments.toMutableList() + override fun clone(): T = RawFile( + root.clone(), + segments.toMutableList()) as T + override fun exists(): Boolean = toFile().exists() override fun isFile(): Boolean = toFile().isFile override fun isDirectory(): Boolean = toFile().isDirectory diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaveTask.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaveTask.java index aa473e0065..46c296688b 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaveTask.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaveTask.java @@ -153,7 +153,9 @@ private boolean copyToDestination(File source) { // If destination is a raw file then we need to check whether the parent directory exists. // Otherwise we don't if (createdDestinationFile instanceof RawFile) { - AbstractFile parent = fileManager.fromAbstractFile(createdDestinationFile).getParent(); + AbstractFile parent = createdDestinationFile + .clone() + .getParent(); if (parent == null || (!parent.create() && !parent.isDirectory())) { throw new IOException("Could not create parent directory"); } @@ -163,7 +165,7 @@ private boolean copyToDestination(File source) { throw new IOException("Destination file is already a directory"); } - if (!fileManager.copyFile(fileManager.fromRawFile(source), createdDestinationFile)) { + if (!fileManager.copyFileContents(fileManager.fromRawFile(source), createdDestinationFile)) { throw new IOException("Could not copy source file into destination"); } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaver.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaver.java index acac6948d8..cf68ca9c22 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaver.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaver.java @@ -65,7 +65,8 @@ public void startDownloadTask(Context context, final ImageSaveTask task) { String fileName = filterName(name + "." + postImage.extension); AbstractFile saveLocation = getSaveLocation(task); - AbstractFile saveFile = fileManager.fromAbstractFile(saveLocation) + AbstractFile saveFile = saveLocation + .clone() .appendFileNameSegment(fileName); while (saveFile.exists()) { @@ -74,7 +75,8 @@ public void startDownloadTask(Context context, final ImageSaveTask task) { + "." + postImage.extension; fileName = filterName(resultFileName); - saveFile = fileManager.fromAbstractFile(saveLocation) + saveFile = saveLocation + .clone() .appendFileNameSegment(fileName); } From 37c3a83e9f95686e7ea4a2d9edd5bdcb52e6a9f0 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 18 Aug 2019 13:02:50 +0300 Subject: [PATCH 020/184] (#172) ImageSaving now works with both RawFiles and ExternalFiles (via SAF) Bring back SaveLocationController so that people can choose whether they want to use SAF or not (or in case of a bug) --- .../chan/core/saf/FileManager.kt | 3 ++ .../chan/core/saf/file/RawFile.kt | 13 +++-- .../chan/core/saver/ImageSaveTask.java | 2 +- .../chan/core/settings/ChanSettings.java | 12 +++-- .../chan/core/settings/StringSetting.java | 7 +++ .../controller/MediaSettingsController.java | 51 +++++++++++++++++-- Kuroba/app/src/main/res/values/strings.xml | 4 ++ 7 files changed, 79 insertions(+), 13 deletions(-) diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt index 20c758fa09..41958be1a0 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt @@ -113,6 +113,9 @@ class FileManager( fun newFile(): AbstractFile { val uri = ChanSettings.saveLocationUri.get() if (uri.isNotEmpty()) { + // When we change saveLocation we also set saveLocationUri to an empty string, so we need + // to check whether the saveLocationUri is empty or not, because saveLocation is never + // empty val rootDirectory = DocumentFile.fromTreeUri(appContext, Uri.parse(uri)) if (rootDirectory == null) { throw IllegalStateException("Root directory cannot be null!") diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt index d76546d589..def0609e42 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt @@ -67,10 +67,15 @@ class RawFile( var newFile = root.holder for (segment in segments) { - if (!segment.isFileName) { - newFile = File(newFile, segment.name) - } else { - return RawFile(Root.FileRoot(File(newFile, segment.name), segment.name)) as T + newFile = File(newFile, segment.name) + + if (!newFile.createNewFile()) { + Logger.e(TAG, "Could not create a new file, path = " + newFile.absolutePath) + return null + } + + if (segment.isFileName) { + return RawFile(Root.FileRoot(newFile, segment.name)) as T } } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaveTask.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaveTask.java index 46c296688b..dc879ed02c 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaveTask.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaveTask.java @@ -154,7 +154,7 @@ private boolean copyToDestination(File source) { // Otherwise we don't if (createdDestinationFile instanceof RawFile) { AbstractFile parent = createdDestinationFile - .clone() + .clone() // TODO: do we need to clone this file? .getParent(); if (parent == null || (!parent.create() && !parent.isDirectory())) { throw new IOException("Could not create parent directory"); diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/ChanSettings.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/ChanSettings.java index d56027fcec..09554f35d6 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/ChanSettings.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/ChanSettings.java @@ -212,11 +212,17 @@ public String getKey() { postPinThread = new BooleanSetting(p, "preference_pin_on_post", false); shortPinInfo = new BooleanSetting(p, "preference_short_pin_info", true); - saveLocation = new StringSetting(p, "preference_image_save_location", Environment.getExternalStorageDirectory() + File.separator + getApplicationLabel()); + saveLocation = new StringSetting(p, "preference_image_save_location", + Environment.getExternalStorageDirectory() + File.separator + getApplicationLabel()); + saveLocation.addCallback((setting, value) -> { + EventBus.getDefault().post(new SettingChanged<>(saveLocation)); + }); + saveLocationUri = new StringSetting(p, "preference_image_save_location_uri", ""); + saveLocationUri.addCallback(((setting, value) -> { + EventBus.getDefault().post(new SettingChanged<>(saveLocationUri)); + })); - saveLocation.addCallback((setting, value) -> - EventBus.getDefault().post(new SettingChanged<>(saveLocation))); saveServerFilename = new BooleanSetting(p, "preference_image_save_original", false); shareUrl = new BooleanSetting(p, "preference_image_share_url", false); accessibleInfo = new BooleanSetting(p, "preference_enable_accessible_info", false); diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/StringSetting.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/StringSetting.java index d0316bd6d0..66b6fdf6f5 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/StringSetting.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/StringSetting.java @@ -44,6 +44,13 @@ public void set(String value) { } } + public void setNoUpdate(String value) { + if (!value.equals(get())) { + settingProvider.putString(key, value); + cached = value; + } + } + public void setSync(String value) { if (!value.equals(get())) { settingProvider.putStringSync(key, value); diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java index 03a033cf08..fb0758d5b3 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java @@ -16,8 +16,10 @@ */ package com.github.adamantcheese.chan.ui.controller; +import android.app.AlertDialog; import android.content.Context; import android.net.Uri; +import android.os.Environment; import android.widget.Toast; import androidx.documentfile.provider.DocumentFile; @@ -39,12 +41,14 @@ import org.greenrobot.eventbus.Subscribe; import org.jetbrains.annotations.NotNull; +import java.io.File; import java.util.ArrayList; import java.util.List; import javax.inject.Inject; import static com.github.adamantcheese.chan.Chan.inject; +import static com.github.adamantcheese.chan.utils.AndroidUtils.getApplicationLabel; import static com.github.adamantcheese.chan.utils.AndroidUtils.getString; public class MediaSettingsController extends SettingsController { @@ -101,7 +105,15 @@ public void onPreferenceChange(SettingView item) { @Subscribe public void onEvent(ChanSettings.SettingChanged setting) { if (setting.setting == ChanSettings.saveLocationUri) { + String defaultDir = Environment.getExternalStorageDirectory() + + File.separator + + getApplicationLabel(); + + ChanSettings.saveLocation.setNoUpdate(defaultDir); saveLocation.setDescription(ChanSettings.saveLocationUri.get()); + } else if (setting.setting == ChanSettings.saveLocation) { + ChanSettings.saveLocationUri.setNoUpdate(""); + saveLocation.setDescription(ChanSettings.saveLocation.get()); } } @@ -232,21 +244,50 @@ private void setupSaveLocationSetting(SettingsGroup media) { LinkSettingView chooseSaveLocationSetting = new LinkSettingView(this, R.string.save_location_screen, 0, - v -> handleDirChoose()); + v -> showDialog()); saveLocation = (LinkSettingView) media.add(chooseSaveLocationSetting); - saveLocation.setDescription(ChanSettings.saveLocationUri.get()); + saveLocation.setDescription(getSaveLocation()); + } + + private String getSaveLocation() { + if (!ChanSettings.saveLocationUri.get().isEmpty()) { + return ChanSettings.saveLocationUri.get(); + } + + return ChanSettings.saveLocation.get(); + } + + private void showDialog() { + AlertDialog alertDialog = new AlertDialog.Builder(context) + .setTitle(R.string.use_saf_dialog_title) + .setMessage(R.string.use_saf_dialog_message) + .setPositiveButton(R.string.use_saf_dialog_positive_button_text, (dialog, which) -> { + useSAFClicked(); + }) + .setNegativeButton(R.string.use_saf_dialog_negative_button_text, (dialog, which) -> { + useOldApiClicked(); + }) + .create(); + + alertDialog.show(); + } + + private void useOldApiClicked() { + navigationController.pushController(new SaveLocationController(context)); } - private void handleDirChoose() { + private void useSAFClicked() { boolean result = fileManager.openChooseDirectoryDialog(new DirectoryChooserCallback() { @Override public void onResult(@NotNull Uri uri) { ChanSettings.saveLocationUri.set(uri.toString()); - // We won't use it anymore - ChanSettings.saveLocation.set(""); + String defaultDir = Environment.getExternalStorageDirectory() + + File.separator + + getApplicationLabel(); + ChanSettings.saveLocation.setNoUpdate(defaultDir); saveLocation.setDescription(uri.toString()); testMethod(uri); diff --git a/Kuroba/app/src/main/res/values/strings.xml b/Kuroba/app/src/main/res/values/strings.xml index b5570cdf2f..e1b9056787 100644 --- a/Kuroba/app/src/main/res/values/strings.xml +++ b/Kuroba/app/src/main/res/values/strings.xml @@ -687,4 +687,8 @@ Don't have a 4chan Pass?
Can\'t share this thread, it\'s already been deleted 99+ Allow full sensor rotation + Use the new Storage Access Framework API to choose the base directory? + If you choose to use the SAF you will be able to choose the sd-card (or even something like Google Drive) as a place to store images/files/downloaded thread etc + Use SAF API + Use old API From b61d619ad3e1a4b5b1b698d383b70c8e39924b0e Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 18 Aug 2019 13:15:16 +0300 Subject: [PATCH 021/184] (#172) Add comments. Remove duplicate name() method --- .../chan/core/saf/file/AbstractFile.kt | 50 ++++++++++++++++++- .../chan/core/saf/file/ExternalFile.kt | 1 - .../chan/core/saf/file/RawFile.kt | 3 +- .../controller/MediaSettingsController.java | 4 +- 4 files changed, 52 insertions(+), 6 deletions(-) diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt index 1bf271d0aa..f470c58c73 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt @@ -43,21 +43,69 @@ abstract class AbstractFile( * AbstractFile and then append the filename to those copies) * */ abstract fun clone(): T + + /** + * Mutates this file. Should be used after clone() if you don't want this file to be changed. + * */ abstract fun exists(): Boolean + + /** + * Mutates this file. Should be used after clone() if you don't want this file to be changed. + * */ abstract fun isFile(): Boolean + + /** + * Mutates this file. Should be used after clone() if you don't want this file to be changed. + * */ abstract fun isDirectory(): Boolean + + /** + * Mutates this file. Should be used after clone() if you don't want this file to be changed. + * */ abstract fun canRead(): Boolean + + /** + * Mutates this file. Should be used after clone() if you don't want this file to be changed. + * */ abstract fun canWrite(): Boolean - abstract fun name(): String? + + /** + * Mutates this file. Should be used after clone() if you don't want this file to be changed. + * */ abstract fun getParent(): T? + + /** + * Does not mutate this file. Safe to use without clone() + * */ abstract fun getFullPath(): String + + /** + * Mutates this file. Should be used after clone() if you don't want this file to be changed. + * */ abstract fun delete(): Boolean + + /** + * Mutates this file. Should be used after clone() if you don't want this file to be changed. + * */ abstract fun getInputStream(): InputStream? + + /** + * Mutates this file. Should be used after clone() if you don't want this file to be changed. + * */ abstract fun getOutputStream(): OutputStream? + + /** + * Mutates this file. Should be used after clone() if you don't want this file to be changed. + * */ abstract fun getName(): String + + /** + * Does not mutate this file. Safe to use without clone() + * */ abstract fun findFile(fileName: String): DocumentFile? /** + * Mutates this file. Should be used after clone(). * Removes the last appended segment if there are any * e.g: /test/123/test2 -> /test/123 -> /test * */ diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt index 1fccf7d603..3cfa4a60c9 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt @@ -143,7 +143,6 @@ class ExternalFile( override fun isDirectory(): Boolean = toDocumentFile()?.isDirectory ?: false override fun canRead(): Boolean = toDocumentFile()?.canRead() ?: false override fun canWrite(): Boolean = toDocumentFile()?.canWrite() ?: false - override fun name(): String? = root.name() override fun getParent(): T? { if (segments.isNotEmpty()) { diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt index def0609e42..5c0a86ac23 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt @@ -91,7 +91,6 @@ class RawFile( override fun isDirectory(): Boolean = toFile().isDirectory override fun canRead(): Boolean = toFile().canRead() override fun canWrite(): Boolean = toFile().canWrite() - override fun name(): String? = root.name() override fun getParent(): T? { if (segments.isNotEmpty()) { @@ -103,7 +102,7 @@ class RawFile( } override fun getFullPath(): String { - return root.holder + return File(root.holder.absolutePath) .appendMany(segments.map { segment -> segment.name }) .absolutePath } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java index fb0758d5b3..7001f6d960 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java @@ -325,7 +325,7 @@ private void testMethod(@NotNull Uri uri) { throw new RuntimeException("Couldn't create test123.txt"); } - if (!externalFile.name().equals("test123.txt")) { + if (!externalFile.getName().equals("test123.txt")) { throw new RuntimeException("externalFile name != test123.txt"); } @@ -398,7 +398,7 @@ private void testMethod(@NotNull Uri uri) { throw new RuntimeException("filename.json is not a directory"); } - if (!externalFile.name().equals("filename.json")) { + if (!externalFile.getName().equals("filename.json")) { throw new RuntimeException("externalFile1 name != filename.json"); } From 2b692aba322d2226c1d0f1b976e43a711102e4a4 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 18 Aug 2019 14:28:21 +0300 Subject: [PATCH 022/184] (#172) Comments, refactoring and bug fixes. ImageSaving now work with sub directories (site name/board code/boar AbstractFile.findFile() now returns a new AbstractFile instead of DocumentFile Fix RawFile.createNew() which would create a file when it should create directory Implement RawFile.findFile() --- .../chan/core/saf/file/AbstractFile.kt | 148 ++++++++++++------ .../chan/core/saf/file/ExternalFile.kt | 51 +++--- .../chan/core/saf/file/ImmutableMethod.kt | 4 + .../chan/core/saf/file/MutableMethod.kt | 4 + .../chan/core/saf/file/RawFile.kt | 71 +++++---- .../controller/MediaSettingsController.java | 2 +- 6 files changed, 172 insertions(+), 108 deletions(-) create mode 100644 Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ImmutableMethod.kt create mode 100644 Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/MutableMethod.kt diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt index f470c58c73..965e2c09e6 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt @@ -1,36 +1,52 @@ package com.github.adamantcheese.chan.core.saf.file -import androidx.documentfile.provider.DocumentFile +import com.github.adamantcheese.chan.core.extension +import java.io.File import java.io.InputStream import java.io.OutputStream +/** + * An abstraction class over both the Java File and the new Storage Access Framework DocumentFile. + * + * Some methods are marked with [MutableMethod] annotation. This means that such method are gonna + * mutate the inner data of the [AbstractFile] (such as root or segments). Sometimes this behavior is + * not desirable. For example, when you have an AbstractFile representing some directory that may + * not even exists on the disk and you want to check whether it exists and if it does check some + * additional files inside that directory. In such case you may want to preserve the [AbstractFile] + * that represents that directory in it's original state. To do this you have to call the [clone] + * method on the file that represents the directory. It will create a copy of the file that you can + * safely work without worry that the original file may change. + * + * Other methods are marked with [ImmutableMethod] annotation. This means that those files create a + * copy of the [AbstractFile] internally and are safe to use without calling [clone] + * */ abstract class AbstractFile( /** * /test/123/test2/filename.txt -> 4 segments * */ protected val segments: MutableList ) { - /** - * We can't append anything if the last segment's isFileName is true. - * This is a terminal operation. - * */ - protected var isFilenameAppended = segments.lastOrNull()?.isFileName ?: false /** * Appends a new subdirectory to the root directory * */ + @MutableMethod abstract fun appendSubDirSegment(name: String): T /** * Appends a file name to the root directory * */ + @MutableMethod abstract fun appendFileNameSegment(name: String): T /** * Creates a new file that consists of the root directory and segments (sub dirs or the file name) + * Behave similarly to Java's mkdirs() method but work not only with directories but files as well. * */ + @ImmutableMethod abstract fun createNew(): T? + @ImmutableMethod fun create(): Boolean { return createNew() != null } @@ -44,71 +60,47 @@ abstract class AbstractFile( * */ abstract fun clone(): T - /** - * Mutates this file. Should be used after clone() if you don't want this file to be changed. - * */ + @MutableMethod abstract fun exists(): Boolean - /** - * Mutates this file. Should be used after clone() if you don't want this file to be changed. - * */ + @MutableMethod abstract fun isFile(): Boolean - /** - * Mutates this file. Should be used after clone() if you don't want this file to be changed. - * */ + @MutableMethod abstract fun isDirectory(): Boolean - /** - * Mutates this file. Should be used after clone() if you don't want this file to be changed. - * */ + @MutableMethod abstract fun canRead(): Boolean - /** - * Mutates this file. Should be used after clone() if you don't want this file to be changed. - * */ + @MutableMethod abstract fun canWrite(): Boolean - /** - * Mutates this file. Should be used after clone() if you don't want this file to be changed. - * */ + @MutableMethod abstract fun getParent(): T? - /** - * Does not mutate this file. Safe to use without clone() - * */ + @ImmutableMethod abstract fun getFullPath(): String - /** - * Mutates this file. Should be used after clone() if you don't want this file to be changed. - * */ + @MutableMethod abstract fun delete(): Boolean - /** - * Mutates this file. Should be used after clone() if you don't want this file to be changed. - * */ + @MutableMethod abstract fun getInputStream(): InputStream? - /** - * Mutates this file. Should be used after clone() if you don't want this file to be changed. - * */ + @MutableMethod abstract fun getOutputStream(): OutputStream? - /** - * Mutates this file. Should be used after clone() if you don't want this file to be changed. - * */ + @MutableMethod abstract fun getName(): String - /** - * Does not mutate this file. Safe to use without clone() - * */ - abstract fun findFile(fileName: String): DocumentFile? + @ImmutableMethod + abstract fun findFile(fileName: String): T? /** - * Mutates this file. Should be used after clone(). * Removes the last appended segment if there are any * e.g: /test/123/test2 -> /test/123 -> /test * */ + @MutableMethod fun removeLastSegment(): Boolean { if (segments.isEmpty()) { return false @@ -118,6 +110,74 @@ abstract class AbstractFile( return true } + protected fun appendSubDirSegmentInner(name: String): T { + if (isFilenameAppended()) { + throw IllegalStateException("Cannot append anything after file name has been appended") + } + + if (name.isBlank()) { + throw IllegalArgumentException("Bad name: $name") + } + + if (name.extension() != null) { + throw IllegalArgumentException("Directory name must not contain extension, " + + "extension = ${name.extension()}") + } + + val nameList = if (name.contains(File.separatorChar)) { + name.split(File.separatorChar) + } else { + listOf(name) + } + + nameList + .onEach { splitName -> + if (splitName.extension() != null) { + throw IllegalArgumentException("appendSubDirSegment does not allow segments " + + "with extensions! bad name = $splitName") + } + } + .map { splitName -> Segment(splitName) } + .forEach { segment -> segments += segment } + + return this as T + } + + protected fun appendFileNameSegmentInner(name: String): T { + if (isFilenameAppended()) { + throw IllegalStateException("Cannot append anything after file name has been appended") + } + + if (name.isBlank()) { + throw IllegalArgumentException("Bad name: $name") + } + + val nameList = if (name.contains(File.separatorChar)) { + val split = name.split(File.separatorChar) + if (split.size < 2) { + throw IllegalStateException("Should have at least two entries, name = $name") + } + + split + } else { + listOf(name) + } + + for ((index, splitName) in nameList.withIndex()) { + if (splitName.extension() != null && index != nameList.lastIndex) { + throw IllegalArgumentException("Only the last split segment may have a file name, " + + "bad segment index = ${index}/${nameList.lastIndex}, bad name = $splitName") + } + + val isFileName = index == nameList.lastIndex + segments += Segment(splitName, isFileName) + } + + return this as T + } + + private fun isFilenameAppended(): Boolean = segments.lastOrNull()?.isFileName ?: false + /** * We can have the root to be a directory or a file. * If it's a directory, that means that we can append sub directories to it. diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt index 3cfa4a60c9..816dceeec3 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt @@ -6,7 +6,6 @@ import android.os.ParcelFileDescriptor import android.webkit.MimeTypeMap import androidx.documentfile.provider.DocumentFile import com.github.adamantcheese.chan.core.appendManyEncoded -import com.github.adamantcheese.chan.core.extension import com.github.adamantcheese.chan.core.getMimeFromFilename import com.github.adamantcheese.chan.utils.Logger import java.io.FileDescriptor @@ -25,21 +24,7 @@ class ExternalFile( throw IllegalStateException("root is already FileRoot, cannot append anything anymore") } - if (isFilenameAppended) { - throw IllegalStateException("Cannot append anything after file name has been appended") - } - - if (name.isNullOrBlank()) { - throw IllegalArgumentException("Bad name: $name") - } - - if (name.extension() != null) { - throw IllegalArgumentException("Directory name must not contain extension, " + - "extension = ${name.extension()}") - } - - segments += Segment(name) - return this as T + return super.appendSubDirSegmentInner(name) } override fun appendFileNameSegment(name: String): T { @@ -47,16 +32,7 @@ class ExternalFile( throw IllegalStateException("root is already FileRoot, cannot append anything anymore") } - if (isFilenameAppended) { - throw IllegalStateException("Cannot append anything after file name has been appended") - } - - if (name.isNullOrBlank()) { - throw IllegalArgumentException("Bad name: $name") - } - - segments += Segment(name, true) - return this as T + return super.appendFileNameSegmentInner(name) } override fun createNew(): T? { @@ -242,7 +218,7 @@ class ExternalFile( ?: throw IllegalStateException("Could not extract file name from document file") } - override fun findFile(fileName: String): DocumentFile? { + override fun findFile(fileName: String): T? { if (root is Root.FileRoot) { throw IllegalStateException("Cannot use FileRoot as directory") } @@ -262,15 +238,32 @@ class ExternalFile( } } + // FIXME: SLOW!!! for (documentFile in dirTree.listFiles()) { if (documentFile.name != null && documentFile.name == fileName) { - return documentFile + val root = if (documentFile.isFile) { + Root.FileRoot(documentFile, documentFile.name!!) + } else { + Root.DirRoot(documentFile) + } + + return ExternalFile( + appContext, + root) as T } } if (dirTree.name == fileName) { - return dirTree + val root = if (dirTree.isFile) { + Root.FileRoot(dirTree, dirTree.name!!) + } else { + Root.DirRoot(dirTree) + } + + return ExternalFile( + appContext, + root) as T } // Not found diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ImmutableMethod.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ImmutableMethod.kt new file mode 100644 index 0000000000..808f63e707 --- /dev/null +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ImmutableMethod.kt @@ -0,0 +1,4 @@ +package com.github.adamantcheese.chan.core.saf.file + +@Retention(AnnotationRetention.SOURCE) +annotation class ImmutableMethod \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/MutableMethod.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/MutableMethod.kt new file mode 100644 index 0000000000..6b598040df --- /dev/null +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/MutableMethod.kt @@ -0,0 +1,4 @@ +package com.github.adamantcheese.chan.core.saf.file + +@Retention(AnnotationRetention.SOURCE) +annotation class MutableMethod \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt index 5c0a86ac23..6096e5a09c 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt @@ -1,8 +1,6 @@ package com.github.adamantcheese.chan.core.saf.file -import androidx.documentfile.provider.DocumentFile import com.github.adamantcheese.chan.core.appendMany -import com.github.adamantcheese.chan.core.extension import com.github.adamantcheese.chan.utils.Logger import java.io.File import java.io.InputStream @@ -13,43 +11,20 @@ class RawFile( segments: MutableList = mutableListOf() ) : AbstractFile(segments) { - override fun appendSubDirSegment(name: String): T { + override fun appendSubDirSegment(name: String): T { if (root is Root.FileRoot) { throw IllegalStateException("root is already FileRoot, cannot append anything anymore") } - if (isFilenameAppended) { - throw IllegalStateException("Cannot append anything after file name has been appended") - } - - if (name.isBlank()) { - throw IllegalArgumentException("Bad name: $name") - } - - if (name.extension() != null) { - throw IllegalArgumentException("Directory name must not contain extension, " + - "extension = ${name.extension()}") - } - - segments += Segment(name) - return this as T + return super.appendSubDirSegmentInner(name) } - override fun appendFileNameSegment(name: String): T { + override fun appendFileNameSegment(name: String): T { if (root is Root.FileRoot) { throw IllegalStateException("root is already FileRoot, cannot append anything anymore") } - if (isFilenameAppended) { - throw IllegalStateException("Cannot append anything after file name has been appended") - } - - if (name.isBlank()) { - throw IllegalArgumentException("Bad name: $name") - } - - segments += Segment(name, true) - return this as T + return super.appendFileNameSegmentInner(name) } override fun createNew(): T? { @@ -69,9 +44,16 @@ class RawFile( for (segment in segments) { newFile = File(newFile, segment.name) - if (!newFile.createNewFile()) { - Logger.e(TAG, "Could not create a new file, path = " + newFile.absolutePath) - return null + if (segment.isFileName) { + if (!newFile.exists() && !newFile.createNewFile()) { + Logger.e(TAG, "Could not create a new file, path = " + newFile.absolutePath) + return null + } + } else { + if (!newFile.exists() && !newFile.mkdir()) { + Logger.e(TAG, "Could not create a new directory, path = " + newFile.absolutePath) + return null + } } if (segment.isFileName) { @@ -157,8 +139,29 @@ class RawFile( return toFile().name } - override fun findFile(fileName: String): DocumentFile? { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + override fun findFile(fileName: String): T? { + if (root is Root.FileRoot) { + throw IllegalStateException("Cannot use FileRoot as directory") + } + + val copy = File(root.holder.absolutePath) + + if (segments.isNotEmpty()) { + copy.appendMany(segments.map { segment -> segment.name }) + } + + val resultFile = File(copy.absolutePath, fileName) + if (!resultFile.exists()) { + return null + } + + val newRoot = if (resultFile.isFile) { + Root.FileRoot(resultFile, resultFile.name) + } else { + Root.DirRoot(resultFile) + } + + return RawFile(newRoot) as T } private fun toFile(): File { diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java index 7001f6d960..ed5803384a 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java @@ -410,7 +410,7 @@ private void testMethod(@NotNull Uri uri) { throw new RuntimeException("dir.name != 4566, name = " + dir.getName()); } - DocumentFile foundFile = dir.findFile("filename.json"); + AbstractFile foundFile = dir.findFile("filename.json"); if (foundFile == null || !foundFile.exists()) { throw new RuntimeException("Couldn't find filename.json"); } From a75df6c02ba1387c9be8eb1e027e1f4971595939 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 18 Aug 2019 16:22:39 +0300 Subject: [PATCH 023/184] (#172) Fixes and refactoring Fix file/directory choosers would not remove the callback from the callback map when we have no activity to handle our intent. Also no message would have been shown in such case. Give the use a choice when exporting settings whether he wants to open an existing settings file to overwrite it or create a new separate file. --- .../adamantcheese/chan/StartActivity.java | 7 +-- .../ImportExportSettingsPresenter.java | 4 +- .../repository/ImportExportRepository.java | 13 +++-- .../chan/core/saf/FileChooser.kt | 44 +++++++++++----- .../chan/core/saf/FileManager.kt | 41 +++++++-------- .../{file => annotation}/ImmutableMethod.kt | 2 +- .../saf/{file => annotation}/MutableMethod.kt | 2 +- .../saf/callback/StartActivityCallbacks.kt | 2 +- .../chan/core/saf/file/AbstractFile.kt | 2 + .../chan/core/saf/file/ExternalFile.kt | 38 +++----------- .../chan/core/saf/file/RawFile.kt | 1 + .../ImportExportSettingsController.java | 51 ++++++++++++++----- .../controller/MediaSettingsController.java | 6 +-- Kuroba/app/src/main/res/values/strings.xml | 3 ++ 14 files changed, 119 insertions(+), 97 deletions(-) rename Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/{file => annotation}/ImmutableMethod.kt (55%) rename Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/{file => annotation}/MutableMethod.kt (54%) diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/StartActivity.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/StartActivity.java index d67f746f13..87b7316f82 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/StartActivity.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/StartActivity.java @@ -603,12 +603,7 @@ public void restartApp() { } @Override - public boolean myStartActivityForResult(@NotNull Intent intent, int requestCode) { - if (intent.resolveActivity(getPackageManager()) == null) { - return false; - } - + public void myStartActivityForResult(@NotNull Intent intent, int requestCode) { startActivityForResult(intent, requestCode); - return true; } } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/ImportExportSettingsPresenter.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/ImportExportSettingsPresenter.java index cf986b5a74..0d618197eb 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/ImportExportSettingsPresenter.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/ImportExportSettingsPresenter.java @@ -44,8 +44,8 @@ public void onDestroy() { this.callbacks = null; } - public void doExport(ExternalFile settingsFile) { - importExportRepository.exportTo(settingsFile, new ImportExportRepository.ImportExportCallbacks() { + public void doExport(ExternalFile settingsFile, boolean isNewFile) { + importExportRepository.exportTo(settingsFile, isNewFile, new ImportExportRepository.ImportExportCallbacks() { @Override public void onSuccess(ImportExportRepository.ImportExport importExport) { //called on background thread diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/ImportExportRepository.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/ImportExportRepository.java index 3e56be01ff..c8e9636270 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/ImportExportRepository.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/ImportExportRepository.java @@ -81,7 +81,7 @@ public ImportExportRepository( this.gson = gson; } - public void exportTo(ExternalFile settingsFile, ImportExportCallbacks callbacks) { + public void exportTo(ExternalFile settingsFile, boolean isNewFile, ImportExportCallbacks callbacks) { databaseManager.runTask(() -> { try { ExportedAppSettings appSettings = readSettingsFromDatabase(); @@ -99,8 +99,15 @@ public void exportTo(ExternalFile settingsFile, ImportExportCallbacks callbacks) ); } - try (ParcelFileDescriptor parcelFileDescriptor = settingsFile.getParcelFileDescriptor( - ExternalFile.FileDescriptorMode.Write)) { + // If the user has opened an old settings file we need to use WriteTruncate mode + // so that there no leftovers of the old file after writing the settings. + // Otherwise use Write mode + ExternalFile.FileDescriptorMode fdm = ExternalFile.FileDescriptorMode.WriteTruncate; + if (isNewFile) { + fdm = ExternalFile.FileDescriptorMode.Write; + } + + try (ParcelFileDescriptor parcelFileDescriptor = settingsFile.getParcelFileDescriptor(fdm)) { if (parcelFileDescriptor == null) { IllegalStateException exception = new IllegalStateException( diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileChooser.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileChooser.kt index a8d0ae4708..38153b86cf 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileChooser.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileChooser.kt @@ -8,6 +8,7 @@ import android.webkit.MimeTypeMap import com.github.adamantcheese.chan.core.getMimeFromFilename import com.github.adamantcheese.chan.core.saf.callback.* import com.github.adamantcheese.chan.utils.Logger +import java.lang.Exception import java.lang.IllegalArgumentException internal class FileChooser( @@ -27,8 +28,8 @@ internal class FileChooser( this.startActivityCallbacks = null } - internal fun openChooseDirectoryDialog(directoryChooserCallback: DirectoryChooserCallback): Boolean { - return startActivityCallbacks?.let { callbacks -> + internal fun openChooseDirectoryDialog(directoryChooserCallback: DirectoryChooserCallback) { + startActivityCallbacks?.let { callbacks -> val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) intent.addFlags( Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or @@ -40,12 +41,17 @@ internal class FileChooser( val nextRequestCode = ++requestCode callbacksMap[nextRequestCode] = directoryChooserCallback as ChooserCallback - return@let callbacks.myStartActivityForResult(intent, nextRequestCode) - } ?: false + try { + callbacks.myStartActivityForResult(intent, nextRequestCode) + } catch (e: Exception) { + callbacksMap.remove(nextRequestCode) + directoryChooserCallback.onCancel(e.message ?: "openChooseDirectoryDialog() Unknown error") + } + } } - internal fun openChooseFileDialog(fileChooserCallback: FileChooserCallback): Boolean { - return startActivityCallbacks?.let { callbacks -> + internal fun openChooseFileDialog(fileChooserCallback: FileChooserCallback) { + startActivityCallbacks?.let { callbacks -> val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) intent.addFlags( Intent.FLAG_GRANT_READ_URI_PERMISSION or @@ -58,15 +64,20 @@ internal class FileChooser( val nextRequestCode = ++requestCode callbacksMap[nextRequestCode] = fileChooserCallback as ChooserCallback - return@let callbacks.myStartActivityForResult(intent, nextRequestCode) - } ?: false + try { + callbacks.myStartActivityForResult(intent, nextRequestCode) + } catch (e: Exception) { + callbacksMap.remove(nextRequestCode) + fileChooserCallback.onCancel(e.message ?: "openChooseFileDialog() Unknown error") + } + } } internal fun openCreateFileDialog( - fileName: String? = null, + fileName: String, fileCreateCallback: FileCreateCallback - ): Boolean { - return startActivityCallbacks?.let { callbacks -> + ) { + startActivityCallbacks?.let { callbacks -> val intent = Intent(Intent.ACTION_CREATE_DOCUMENT) intent.addFlags( Intent.FLAG_GRANT_READ_URI_PERMISSION or @@ -83,14 +94,19 @@ internal class FileChooser( val nextRequestCode = ++requestCode callbacksMap[nextRequestCode] = fileCreateCallback as ChooserCallback - return@let callbacks.myStartActivityForResult(intent, nextRequestCode) - } ?: false + try { + callbacks.myStartActivityForResult(intent, nextRequestCode) + } catch (e: Exception) { + callbacksMap.remove(nextRequestCode) + fileCreateCallback.onCancel(e.message ?: "openCreateFileDialog() Unknown error") + } + } } internal fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { val callback = callbacksMap[requestCode] if (callback == null) { - Logger.d(TAG, "Callback is already removed from the map") + Logger.d(TAG, "Callback is already removed from the map, resultCode = $requestCode") return false } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt index 41958be1a0..ca4ce2722f 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt @@ -18,6 +18,7 @@ import com.github.adamantcheese.chan.utils.Logger import java.io.File import java.io.IOException import java.lang.IllegalStateException +import java.lang.RuntimeException class FileManager( private val appContext: Context @@ -39,20 +40,16 @@ class FileManager( // Api to open file/directory chooser and handling the result //======================================================= - fun openChooseDirectoryDialog(callback: DirectoryChooserCallback): Boolean { - return fileChooser.openChooseDirectoryDialog(callback) + fun openChooseDirectoryDialog(callback: DirectoryChooserCallback) { + fileChooser.openChooseDirectoryDialog(callback) } - fun openChooseFileDialog(callback: FileChooserCallback): Boolean { - return fileChooser.openChooseFileDialog(callback) + fun openChooseFileDialog(callback: FileChooserCallback) { + fileChooser.openChooseFileDialog(callback) } - fun openCreateFileDialog(callback: FileCreateCallback): Boolean { - return openCreateFileDialog(null, callback) - } - - fun openCreateFileDialog(filename: String?, callback: FileCreateCallback): Boolean { - return fileChooser.openCreateFileDialog(filename, callback) + fun openCreateFileDialog(filename: String, callback: FileCreateCallback) { + fileChooser.openCreateFileDialog(filename, callback) } fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { @@ -107,10 +104,15 @@ class FileManager( } /** - * Instantiates a new AbstractFile with the root being in the app's base directory (the Kuroba - * directory in case of using raw file api and the user's selected directory in case of using SAF). + * Instantiates a new AbstractFile with the root being in the app's base directory (either the Kuroba + * directory in case of using raw file api or the user's selected directory in case of using SAF). * */ fun newFile(): AbstractFile { + if (ChanSettings.saveLocationUri.get().isEmpty() && ChanSettings.saveLocation.get().isEmpty()) { + // wtf? + throw RuntimeException("Both save locations are empty!") + } + val uri = ChanSettings.saveLocationUri.get() if (uri.isNotEmpty()) { // When we change saveLocation we also set saveLocationUri to an empty string, so we need @@ -141,7 +143,6 @@ class FileManager( // another way to check it. DocumentFile.fromTreeUri(appContext, uri) } catch (ignored: IllegalArgumentException) { - Logger.d(TAG, "Uri is not a treeUri, uri = $uri") null } @@ -161,16 +162,16 @@ class FileManager( * Copy one file's contents into another * */ fun copyFileContents(source: AbstractFile, destination: AbstractFile): Boolean { - try { - return source.getInputStream()?.use { inputStream -> - return@use destination.getOutputStream()?.use { outputStream -> + return try { + source.getInputStream()?.use { inputStream -> + destination.getOutputStream()?.use { outputStream -> IOUtils.copy(inputStream, outputStream) - return@use true - } ?: false + true + } } ?: false } catch (e: IOException) { - Logger.e(TAG, "IOException while coping one file to another", e) - return false + Logger.e(TAG, "IOException while copying one file into another", e) + false } } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ImmutableMethod.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/annotation/ImmutableMethod.kt similarity index 55% rename from Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ImmutableMethod.kt rename to Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/annotation/ImmutableMethod.kt index 808f63e707..e8755cc8a8 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ImmutableMethod.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/annotation/ImmutableMethod.kt @@ -1,4 +1,4 @@ -package com.github.adamantcheese.chan.core.saf.file +package com.github.adamantcheese.chan.core.saf.annotation @Retention(AnnotationRetention.SOURCE) annotation class ImmutableMethod \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/MutableMethod.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/annotation/MutableMethod.kt similarity index 54% rename from Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/MutableMethod.kt rename to Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/annotation/MutableMethod.kt index 6b598040df..638d51c87b 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/MutableMethod.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/annotation/MutableMethod.kt @@ -1,4 +1,4 @@ -package com.github.adamantcheese.chan.core.saf.file +package com.github.adamantcheese.chan.core.saf.annotation @Retention(AnnotationRetention.SOURCE) annotation class MutableMethod \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/callback/StartActivityCallbacks.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/callback/StartActivityCallbacks.kt index 3c473f4f49..9f6db98d1f 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/callback/StartActivityCallbacks.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/callback/StartActivityCallbacks.kt @@ -3,5 +3,5 @@ package com.github.adamantcheese.chan.core.saf.callback import android.content.Intent interface StartActivityCallbacks { - fun myStartActivityForResult(intent: Intent, requestCode: Int): Boolean + fun myStartActivityForResult(intent: Intent, requestCode: Int) } \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt index 965e2c09e6..1ee9d3b12e 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt @@ -1,6 +1,8 @@ package com.github.adamantcheese.chan.core.saf.file import com.github.adamantcheese.chan.core.extension +import com.github.adamantcheese.chan.core.saf.annotation.ImmutableMethod +import com.github.adamantcheese.chan.core.saf.annotation.MutableMethod import java.io.File import java.io.InputStream import java.io.OutputStream diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt index 816dceeec3..0ff7971ef4 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt @@ -37,6 +37,7 @@ class ExternalFile( override fun createNew(): T? { if (root is Root.FileRoot) { + // TODO: do we need this check? throw IllegalStateException("root is already FileRoot, cannot append anything anymore") } @@ -107,14 +108,7 @@ class ExternalFile( root.clone(), segments.toMutableList()) as T - override fun exists(): Boolean { - if (segments.isEmpty()) { - return root.holder.exists() - } - - return toDocumentFile()?.exists() ?: false - } - + override fun exists(): Boolean = toDocumentFile()?.exists() ?: false override fun isFile(): Boolean = toDocumentFile()?.isFile ?: false override fun isDirectory(): Boolean = toDocumentFile()?.isDirectory ?: false override fun canRead(): Boolean = toDocumentFile()?.canRead() ?: false @@ -238,7 +232,6 @@ class ExternalFile( } } - // FIXME: SLOW!!! for (documentFile in dirTree.listFiles()) { if (documentFile.name != null && documentFile.name == fileName) { @@ -277,26 +270,6 @@ class ExternalFile( fileDescriptorMode.mode) } - /** - * An extension function that allows to do something with a FileDescriptor without having - * to worry about not closing it in the end. - * - * Example: - * withFileDescriptor(FileDescriptorMode.Read) { fd -> - * // Do anything here with FileDescriptor here, it will be closed automatically upon - * // exiting the lambda - * } - * */ - fun withFileDescriptor( - fileDescriptorMode: FileDescriptorMode, - func: (FileDescriptor) -> Unit - ): Boolean { - return getParcelFileDescriptor(fileDescriptorMode)?.use { pfd -> - func(pfd.fileDescriptor) - return@use true - } ?: false - } - private fun toDocumentFile(): DocumentFile? { if (segments.isEmpty()) { return root.holder @@ -339,9 +312,14 @@ class ExternalFile( enum class FileDescriptorMode(val mode: String) { Read("r"), Write("w"), + // When overwriting an existing file it is a really good ide to use truncate mode, + // because otherwise if a new file's length is less than the old one's then there will be + // old file's data left at the end of the file + WriteTruncate("wt"), // It is recommended to prefer either Read or Write modes in the documentation. // Use ReadWrite only when it is really necessary. - ReadWrite("rw") + ReadWrite("rw"), + ReadWriteTruncate("rwt") } companion object { diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt index 6096e5a09c..55c35ac532 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt @@ -29,6 +29,7 @@ class RawFile( override fun createNew(): T? { if (root is Root.FileRoot) { + // TODO: do we need this check? throw IllegalStateException("root is already FileRoot, cannot append anything anymore") } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ImportExportSettingsController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ImportExportSettingsController.java index 4544e5d54c..db5d1c3d6a 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ImportExportSettingsController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ImportExportSettingsController.java @@ -16,6 +16,7 @@ */ package com.github.adamantcheese.chan.ui.controller; +import android.app.AlertDialog; import android.content.Context; import android.net.Uri; import android.widget.Toast; @@ -115,18 +116,38 @@ private void populatePreferences() { } private void onExportClicked() { - fileManager.openCreateFileDialog(EXPORT_FILE_NAME, new FileCreateCallback() { + AlertDialog alertDialog = new AlertDialog.Builder(context) + .setTitle(R.string.import_or_export_dialog_title) + .setPositiveButton(R.string.import_or_export_dialog_positive_button_text, (dialog, which) -> { + overwriteExisting(); + }) + .setNegativeButton(R.string.import_or_export_dialog_negative_button_text, (dialog, which) -> { + createNew(); + }) + .create(); + + alertDialog.show(); + } + + private void overwriteExisting() { + fileManager.openChooseFileDialog(new FileChooserCallback() { @Override public void onResult(@NotNull Uri uri) { - ExternalFile externalFile = fileManager.fromUri(uri); - if (externalFile == null) { - showMessage("fileManager.fromUri() returned null externalFile, " + - "uri = " + uri.toString()); - return; - } + onFileChosen(uri, false); + } - navigationController.presentController(loadingViewController); - presenter.doExport(externalFile); + @Override + public void onCancel(@NotNull String reason) { + showMessage(reason); + } + }); + } + + private void createNew() { + fileManager.openCreateFileDialog(EXPORT_FILE_NAME, new FileCreateCallback() { + @Override + public void onResult(@NotNull Uri uri) { + onFileChosen(uri, true); } @Override @@ -136,16 +157,18 @@ public void onCancel(@NotNull String reason) { }); } + private void onFileChosen(Uri uri, boolean isNewFile) { + ExternalFile externalFile = fileManager.fromUri(uri); + + navigationController.presentController(loadingViewController); + presenter.doExport(externalFile, isNewFile); + } + private void onImportClicked() { fileManager.openChooseFileDialog(new FileChooserCallback() { @Override public void onResult(@NotNull Uri uri) { ExternalFile externalFile = fileManager.fromUri(uri); - if (externalFile == null) { - showMessage("fileManager.fromUri() returned null externalFile, " + - "uri = " + uri.toString()); - return; - } navigationController.presentController(loadingViewController); presenter.doImport(externalFile); diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java index ed5803384a..506fd28677 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java @@ -278,7 +278,7 @@ private void useOldApiClicked() { } private void useSAFClicked() { - boolean result = fileManager.openChooseDirectoryDialog(new DirectoryChooserCallback() { + fileManager.openChooseDirectoryDialog(new DirectoryChooserCallback() { @Override public void onResult(@NotNull Uri uri) { ChanSettings.saveLocationUri.set(uri.toString()); @@ -298,10 +298,6 @@ public void onCancel(@NotNull String reason) { Toast.makeText(context, reason, Toast.LENGTH_LONG).show(); } }); - - if (!result) { - Toast.makeText(context, "Could not start activity for result", Toast.LENGTH_SHORT).show(); - } } private void testMethod(@NotNull Uri uri) { diff --git a/Kuroba/app/src/main/res/values/strings.xml b/Kuroba/app/src/main/res/values/strings.xml index e1b9056787..de90239651 100644 --- a/Kuroba/app/src/main/res/values/strings.xml +++ b/Kuroba/app/src/main/res/values/strings.xml @@ -691,4 +691,7 @@ Don't have a 4chan Pass?
If you choose to use the SAF you will be able to choose the sd-card (or even something like Google Drive) as a place to store images/files/downloaded thread etc Use SAF API Use old API + Overwrite existing file or create a new one? + Overwrite + Create new From 11bde352dc8fbed29549d5a79736fa0af202f055 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 24 Aug 2019 10:29:05 +0300 Subject: [PATCH 024/184] (#172) Add comments --- .../core/repository/ImportExportRepository.java | 6 +++--- .../adamantcheese/chan/core/saf/FileManager.kt | 2 +- .../ImportExportSettingsController.java | 16 ++++++++++++++++ 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/ImportExportRepository.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/ImportExportRepository.java index c8e9636270..0bfba8866e 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/ImportExportRepository.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/ImportExportRepository.java @@ -66,9 +66,9 @@ public class ImportExportRepository { // Also, don't forget to handle the change in the onUpgrade or onDowngrade methods public static final int CURRENT_EXPORT_SETTINGS_VERSION = 3; - private DatabaseManager databaseManager; - private DatabaseHelper databaseHelper; - private Gson gson; + private final DatabaseManager databaseManager; + private final DatabaseHelper databaseHelper; + private final Gson gson; @Inject public ImportExportRepository( diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt index ca4ce2722f..6485c6bccb 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt @@ -110,7 +110,7 @@ class FileManager( fun newFile(): AbstractFile { if (ChanSettings.saveLocationUri.get().isEmpty() && ChanSettings.saveLocation.get().isEmpty()) { // wtf? - throw RuntimeException("Both save locations are empty!") + throw RuntimeException("Both save locations are empty! Something went terribly wrong.") } val uri = ChanSettings.saveLocationUri.get() diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ImportExportSettingsController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ImportExportSettingsController.java index db5d1c3d6a..d46b0241e4 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ImportExportSettingsController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ImportExportSettingsController.java @@ -115,6 +115,12 @@ private void populatePreferences() { } } + /** + * SAF is kinda retarded so it cannot be used to overwrite a file that already exist on the disk + * (or at some network location). When trying to do so, a new file with appended "(1)" at the + * end will appear. That's why there are two methods (one for overwriting an existing file and + * the other one for creating a new file) instead of one that does everything. + * */ private void onExportClicked() { AlertDialog alertDialog = new AlertDialog.Builder(context) .setTitle(R.string.import_or_export_dialog_title) @@ -129,6 +135,9 @@ private void onExportClicked() { alertDialog.show(); } + /** + * Opens an existing file (any file) for overwriting with the settings. + * */ private void overwriteExisting() { fileManager.openChooseFileDialog(new FileChooserCallback() { @Override @@ -143,6 +152,11 @@ public void onCancel(@NotNull String reason) { }); } + /** + * Creates a new file with the default name (that can be changed in the file chooser) with the + * settings. Cannot be used for overwriting an old settings file (when trying to do so a new file + * with appended "(1)" at the end will appear, e.g. "test (1).txt") + * */ private void createNew() { fileManager.openCreateFileDialog(EXPORT_FILE_NAME, new FileCreateCallback() { @Override @@ -158,6 +172,8 @@ public void onCancel(@NotNull String reason) { } private void onFileChosen(Uri uri, boolean isNewFile) { + // We use SAF here by default because settings importing/exporting does not depend on the + // Kuroba default directory location. There is just no need to use old java files. ExternalFile externalFile = fileManager.fromUri(uri); navigationController.presentController(loadingViewController); From 0b5de5918725de5c0ed52727b799efb9f55e2e9c Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 24 Aug 2019 10:45:51 +0300 Subject: [PATCH 025/184] (#172) Add local threads location setting (which is selected by the user via SAF) --- .../chan/core/settings/ChanSettings.java | 8 +- .../controller/MediaSettingsController.java | 134 +++++++++++------- .../ui/controller/ViewThreadController.java | 10 ++ Kuroba/app/src/main/res/values/strings.xml | 3 + 4 files changed, 99 insertions(+), 56 deletions(-) diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/ChanSettings.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/ChanSettings.java index 09554f35d6..feb09e62c8 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/ChanSettings.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/ChanSettings.java @@ -124,6 +124,7 @@ public String getKey() { @Deprecated public static final StringSetting saveLocation; public static final StringSetting saveLocationUri; + public static final StringSetting localThreadsLocationUri; public static final BooleanSetting saveServerFilename; public static final BooleanSetting shareUrl; public static final BooleanSetting enableReplyFab; @@ -223,6 +224,11 @@ public String getKey() { EventBus.getDefault().post(new SettingChanged<>(saveLocationUri)); })); + localThreadsLocationUri = new StringSetting(p, "local_threads_location_uri", ""); + localThreadsLocationUri.addCallback((settings, value) -> { + EventBus.getDefault().post(new SettingChanged<>(localThreadsLocationUri)); + }); + saveServerFilename = new BooleanSetting(p, "preference_image_save_original", false); shareUrl = new BooleanSetting(p, "preference_image_share_url", false); accessibleInfo = new BooleanSetting(p, "preference_enable_accessible_info", false); @@ -292,9 +298,7 @@ public String getKey() { enableEmoji = new BooleanSetting(p, "enable_emoji", false); highResCells = new BooleanSetting(p, "high_res_cells", false); padThumbs = new BooleanSetting(p, "pad_thumbnails", true); - incrementalThreadDownloadingEnabled = new BooleanSetting(p, "incremental_thread_downloading", false); - fullUserRotationEnable = new BooleanSetting(p, "full_user_rotation_enable", true); } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java index 506fd28677..34c7634f15 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java @@ -22,8 +22,6 @@ import android.os.Environment; import android.widget.Toast; -import androidx.documentfile.provider.DocumentFile; - import com.github.adamantcheese.chan.R; import com.github.adamantcheese.chan.core.saf.FileManager; import com.github.adamantcheese.chan.core.saf.callback.DirectoryChooserCallback; @@ -56,6 +54,7 @@ public class MediaSettingsController extends SettingsController { private BooleanSettingView boardFolderSetting; private BooleanSettingView threadFolderSetting; private LinkSettingView saveLocation; + private LinkSettingView localThreadsLocation; private ListSettingView imageAutoLoadView; private ListSettingView videoAutoLoadView; @@ -123,6 +122,7 @@ private void populatePreferences() { SettingsGroup media = new SettingsGroup(R.string.settings_group_media); setupSaveLocationSetting(media); + setupLocalThreadLocationSetting(media); boardFolderSetting = (BooleanSettingView) media.add(new BooleanSettingView(this, ChanSettings.saveBoardFolder, @@ -184,60 +184,30 @@ private void populatePreferences() { } } - private void setupMediaLoadTypesSetting(SettingsGroup loading) { - List imageAutoLoadTypes = new ArrayList<>(); - List videoAutoLoadTypes = new ArrayList<>(); - for (ChanSettings.MediaAutoLoadMode mode : ChanSettings.MediaAutoLoadMode.values()) { - int name = 0; - switch (mode) { - case ALL: - name = R.string.setting_image_auto_load_all; - break; - case WIFI: - name = R.string.setting_image_auto_load_wifi; - break; - case NONE: - name = R.string.setting_image_auto_load_none; - break; - } - - imageAutoLoadTypes.add(new ListSettingView.Item<>(getString(name), mode)); - videoAutoLoadTypes.add(new ListSettingView.Item<>(getString(name), mode)); + private void setupLocalThreadLocationSetting(SettingsGroup media) { + if (!ChanSettings.incrementalThreadDownloadingEnabled.get()) { + return; } - imageAutoLoadView = new ListSettingView<>(this, - ChanSettings.imageAutoLoadNetwork, R.string.setting_image_auto_load, - imageAutoLoadTypes); - loading.add(imageAutoLoadView); + LinkSettingView localThreadsLocationSetting = new LinkSettingView(this, + R.string.media_settings_local_threads_location_title, + 0, + v -> onLocalThreadsLocationSettingClicked()); - videoAutoLoadView = new ListSettingView<>(this, - ChanSettings.videoAutoLoadNetwork, R.string.setting_video_auto_load, - videoAutoLoadTypes); - loading.add(videoAutoLoadView); + String localThreadsLocationString; - updateVideoLoadModes(); + if (ChanSettings.localThreadsLocationUri.get().isEmpty()) { + localThreadsLocationString = context.getString(R.string.media_settings_local_threads_setting_not_set); + } else { + localThreadsLocationString = ChanSettings.localThreadsLocationUri.get(); + } + + localThreadsLocation = (LinkSettingView) media.add(localThreadsLocationSetting); + localThreadsLocation.setDescription(localThreadsLocationString); } - private void updateVideoLoadModes() { - ChanSettings.MediaAutoLoadMode currentImageLoadMode = ChanSettings.imageAutoLoadNetwork.get(); - ChanSettings.MediaAutoLoadMode[] modes = ChanSettings.MediaAutoLoadMode.values(); - boolean enabled = false; - boolean resetVideoMode = false; - for (int i = 0; i < modes.length; i++) { - if (modes[i].getKey().equals(currentImageLoadMode.getKey())) { - enabled = true; - if (resetVideoMode) { - ChanSettings.videoAutoLoadNetwork.set(modes[i]); - videoAutoLoadView.updateSelection(); - onPreferenceChange(videoAutoLoadView); - } - } - videoAutoLoadView.items.get(i).enabled = enabled; - if (!enabled && ChanSettings.videoAutoLoadNetwork.get().getKey() - .equals(modes[i].getKey())) { - resetVideoMode = true; - } - } + private void onLocalThreadsLocationSettingClicked() { + } private void setupSaveLocationSetting(SettingsGroup media) { @@ -263,21 +233,21 @@ private void showDialog() { .setTitle(R.string.use_saf_dialog_title) .setMessage(R.string.use_saf_dialog_message) .setPositiveButton(R.string.use_saf_dialog_positive_button_text, (dialog, which) -> { - useSAFClicked(); + onSaveLocationUseSAFClicked(); }) .setNegativeButton(R.string.use_saf_dialog_negative_button_text, (dialog, which) -> { - useOldApiClicked(); + onSaveLocationUseOldApiClicked(); }) .create(); alertDialog.show(); } - private void useOldApiClicked() { + private void onSaveLocationUseOldApiClicked() { navigationController.pushController(new SaveLocationController(context)); } - private void useSAFClicked() { + private void onSaveLocationUseSAFClicked() { fileManager.openChooseDirectoryDialog(new DirectoryChooserCallback() { @Override public void onResult(@NotNull Uri uri) { @@ -435,6 +405,62 @@ private void testMethod(@NotNull Uri uri) { System.out.println("All tests passed!"); } + private void setupMediaLoadTypesSetting(SettingsGroup loading) { + List imageAutoLoadTypes = new ArrayList<>(); + List videoAutoLoadTypes = new ArrayList<>(); + for (ChanSettings.MediaAutoLoadMode mode : ChanSettings.MediaAutoLoadMode.values()) { + int name = 0; + switch (mode) { + case ALL: + name = R.string.setting_image_auto_load_all; + break; + case WIFI: + name = R.string.setting_image_auto_load_wifi; + break; + case NONE: + name = R.string.setting_image_auto_load_none; + break; + } + + imageAutoLoadTypes.add(new ListSettingView.Item<>(getString(name), mode)); + videoAutoLoadTypes.add(new ListSettingView.Item<>(getString(name), mode)); + } + + imageAutoLoadView = new ListSettingView<>(this, + ChanSettings.imageAutoLoadNetwork, R.string.setting_image_auto_load, + imageAutoLoadTypes); + loading.add(imageAutoLoadView); + + videoAutoLoadView = new ListSettingView<>(this, + ChanSettings.videoAutoLoadNetwork, R.string.setting_video_auto_load, + videoAutoLoadTypes); + loading.add(videoAutoLoadView); + + updateVideoLoadModes(); + } + + private void updateVideoLoadModes() { + ChanSettings.MediaAutoLoadMode currentImageLoadMode = ChanSettings.imageAutoLoadNetwork.get(); + ChanSettings.MediaAutoLoadMode[] modes = ChanSettings.MediaAutoLoadMode.values(); + boolean enabled = false; + boolean resetVideoMode = false; + for (int i = 0; i < modes.length; i++) { + if (modes[i].getKey().equals(currentImageLoadMode.getKey())) { + enabled = true; + if (resetVideoMode) { + ChanSettings.videoAutoLoadNetwork.set(modes[i]); + videoAutoLoadView.updateSelection(); + onPreferenceChange(videoAutoLoadView); + } + } + videoAutoLoadView.items.get(i).enabled = enabled; + if (!enabled && ChanSettings.videoAutoLoadNetwork.get().getKey() + .equals(modes[i].getKey())) { + resetVideoMode = true; + } + } + } + private void updateThreadFolderSetting() { if (ChanSettings.saveBoardFolder.get()) { threadFolderSetting.setEnabled(true); diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ViewThreadController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ViewThreadController.java index 59d6e9e86e..7748263182 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ViewThreadController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ViewThreadController.java @@ -156,6 +156,16 @@ private void pinClicked(ToolbarMenuItem item) { } private void saveClicked(ToolbarMenuItem item) { + if (ChanSettings.localThreadsLocationUri.get().isEmpty()) { + // TODO: show the SAF directory chooser right here instead of just showing a toast? Or + // open up the media settings controller? + Toast.makeText( + context, + R.string.view_thread_controller_local_threads_location_is_not_set, + Toast.LENGTH_LONG).show(); + return; + } + RuntimePermissionsHelper runtimePermissionsHelper = ((StartActivity) context).getRuntimePermissionsHelper(); if (runtimePermissionsHelper.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { saveClickedInternal(); diff --git a/Kuroba/app/src/main/res/values/strings.xml b/Kuroba/app/src/main/res/values/strings.xml index de90239651..8456c47a93 100644 --- a/Kuroba/app/src/main/res/values/strings.xml +++ b/Kuroba/app/src/main/res/values/strings.xml @@ -694,4 +694,7 @@ Don't have a 4chan Pass?
Overwrite existing file or create a new one? Overwrite Create new + Local threads location + Not set + Local threads location]]> From 99460687d04b0bacb1daba3398c673b30f3aabee Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 24 Aug 2019 12:56:59 +0300 Subject: [PATCH 026/184] (#172) Threads should now work with SAF (not tested yet) --- .../chan/core/di/ManagerModule.java | 13 +- .../manager/SavedThreadLoaderManager.java | 39 +++- .../chan/core/manager/ThreadSaveManager.java | 187 ++++++++++++------ .../core/model/save/SerializableThread.java | 11 +- .../SavedThreadLoaderRepository.java | 135 +++++++++---- .../chan/core/saf/FileManager.kt | 34 ++-- .../chan/core/saf/file/AbstractFile.kt | 128 ++++++++---- .../chan/core/saf/file/ExternalFile.kt | 23 +-- .../chan/core/saf/file/RawFile.kt | 25 ++- 9 files changed, 404 insertions(+), 191 deletions(-) diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/ManagerModule.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/ManagerModule.java index eac17ae367..0cbc9db390 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/ManagerModule.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/ManagerModule.java @@ -33,6 +33,7 @@ import com.github.adamantcheese.chan.core.pool.ChanLoaderFactory; import com.github.adamantcheese.chan.core.repository.BoardRepository; import com.github.adamantcheese.chan.core.repository.SavedThreadLoaderRepository; +import com.github.adamantcheese.chan.core.saf.FileManager; import com.github.adamantcheese.chan.core.settings.json.JsonSettings; import com.github.adamantcheese.chan.core.site.Site; import com.github.adamantcheese.chan.core.site.sites.chan4.Chan4; @@ -127,10 +128,12 @@ public ArchivesManager provideArchivesManager() throws Exception { @Singleton public ThreadSaveManager provideSaveThreadManager( DatabaseManager databaseManager, - SavedThreadLoaderRepository savedThreadLoaderRepository) { + SavedThreadLoaderRepository savedThreadLoaderRepository, + FileManager fileManager) { return new ThreadSaveManager( databaseManager, - savedThreadLoaderRepository); + savedThreadLoaderRepository, + fileManager); } @Provides @@ -138,10 +141,12 @@ public ThreadSaveManager provideSaveThreadManager( public SavedThreadLoaderManager provideSavedThreadLoaderManager( Gson gson, DatabaseManager databaseManager, - SavedThreadLoaderRepository savedThreadLoaderRepository) { + SavedThreadLoaderRepository savedThreadLoaderRepository, + FileManager fileManager) { return new SavedThreadLoaderManager( gson, databaseManager, - savedThreadLoaderRepository); + savedThreadLoaderRepository, + fileManager); } } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/SavedThreadLoaderManager.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/SavedThreadLoaderManager.java index dc6beaeb3c..f30d492271 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/SavedThreadLoaderManager.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/SavedThreadLoaderManager.java @@ -1,5 +1,7 @@ package com.github.adamantcheese.chan.core.manager; +import android.net.Uri; + import androidx.annotation.Nullable; import com.github.adamantcheese.chan.core.database.DatabaseManager; @@ -8,6 +10,8 @@ import com.github.adamantcheese.chan.core.model.orm.Loadable; import com.github.adamantcheese.chan.core.model.save.SerializableThread; import com.github.adamantcheese.chan.core.repository.SavedThreadLoaderRepository; +import com.github.adamantcheese.chan.core.saf.FileManager; +import com.github.adamantcheese.chan.core.saf.file.ExternalFile; import com.github.adamantcheese.chan.core.settings.ChanSettings; import com.github.adamantcheese.chan.utils.BackgroundUtils; import com.github.adamantcheese.chan.utils.Logger; @@ -21,18 +25,21 @@ public class SavedThreadLoaderManager { private final static String TAG = "SavedThreadLoaderManager"; - private Gson gson; - private DatabaseManager databaseManager; - private SavedThreadLoaderRepository savedThreadLoaderRepository; + private final Gson gson; + private final DatabaseManager databaseManager; + private final SavedThreadLoaderRepository savedThreadLoaderRepository; + private final FileManager fileManager; @Inject public SavedThreadLoaderManager( Gson gson, DatabaseManager databaseManager, - SavedThreadLoaderRepository savedThreadLoaderRepository) { + SavedThreadLoaderRepository savedThreadLoaderRepository, + FileManager fileManager) { this.gson = gson; this.databaseManager = databaseManager; this.savedThreadLoaderRepository = savedThreadLoaderRepository; + this.fileManager = fileManager; } @Nullable @@ -41,31 +48,43 @@ public ChanThread loadSavedThread(Loadable loadable) { throw new RuntimeException("Cannot be executed on the main thread!"); } + if (ChanSettings.localThreadsLocationUri.get().isEmpty()) { + throw new IllegalStateException("Local threads location is not set!"); + } + String threadSubDir = ThreadSaveManager.getThreadSubDir(loadable); - File threadSaveDir = new File(ChanSettings.saveLocation.get(), threadSubDir); + Uri localThreadsLocationUri = Uri.parse(ChanSettings.localThreadsLocationUri.get()); + ExternalFile threadSaveDir = fileManager.fromUri(localThreadsLocationUri) + .appendSubDirSegment(threadSubDir); if (!threadSaveDir.exists() || !threadSaveDir.isDirectory()) { Logger.e(TAG, "threadSaveDir does not exist or is not a directory: " - + "(path = " + threadSaveDir.getAbsolutePath() + + "(path = " + threadSaveDir.getFullPath() + ", exists = " + threadSaveDir.exists() + ", isDir = " + threadSaveDir.isDirectory() + ")"); return null; } - File threadFile = new File(threadSaveDir, SavedThreadLoaderRepository.THREAD_FILE_NAME); + ExternalFile threadFile = threadSaveDir + .clone() + .appendFileNameSegment(SavedThreadLoaderRepository.THREAD_FILE_NAME); + if (!threadFile.exists() || !threadFile.isFile() || !threadFile.canRead()) { Logger.e(TAG, "threadFile does not exist or not a file or cannot be read: " + - "(path = " + threadFile.getAbsolutePath() + "(path = " + threadFile.getFullPath() + ", exists = " + threadFile.exists() + ", isFile = " + threadFile.isFile() + ", canRead = " + threadFile.canRead() + ")"); return null; } - File threadSaveDirImages = new File(threadSaveDir, "images"); + ExternalFile threadSaveDirImages = threadSaveDir + .clone() + .appendSubDirSegment("images"); + if (!threadSaveDirImages.exists() || !threadSaveDirImages.isDirectory()) { Logger.e(TAG, "threadSaveDirImages does not exist or is not a directory: " - + "(path = " + threadSaveDirImages.getAbsolutePath() + + "(path = " + threadSaveDirImages.getFullPath() + ", exists = " + threadSaveDirImages.exists() + ", isDir = " + threadSaveDirImages.isDirectory() + ")"); return null; diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/ThreadSaveManager.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/ThreadSaveManager.java index c174623d64..10e09738ab 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/ThreadSaveManager.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/ThreadSaveManager.java @@ -1,6 +1,7 @@ package com.github.adamantcheese.chan.core.manager; import android.annotation.SuppressLint; +import android.net.Uri; import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; @@ -13,6 +14,8 @@ import com.github.adamantcheese.chan.core.model.orm.Loadable; import com.github.adamantcheese.chan.core.model.save.SerializableThread; import com.github.adamantcheese.chan.core.repository.SavedThreadLoaderRepository; +import com.github.adamantcheese.chan.core.saf.FileManager; +import com.github.adamantcheese.chan.core.saf.file.ExternalFile; import com.github.adamantcheese.chan.core.settings.ChanSettings; import com.github.adamantcheese.chan.utils.BackgroundUtils; import com.github.adamantcheese.chan.utils.IOUtils; @@ -64,9 +67,10 @@ public class ThreadSaveManager { public static final String ORIGINAL_FILE_NAME = "original"; public static final String NO_MEDIA_FILE_NAME = ".nomedia"; - private DatabaseManager databaseManager; - private DatabaseSavedThreadManager databaseSavedThreadManager; - private SavedThreadLoaderRepository savedThreadLoaderRepository; + private final DatabaseManager databaseManager; + private final DatabaseSavedThreadManager databaseSavedThreadManager; + private final SavedThreadLoaderRepository savedThreadLoaderRepository; + private final FileManager fileManager; @GuardedBy("itself") private final Map activeDownloads = new HashMap<>(); @@ -99,10 +103,12 @@ private static int getThreadsCountForDownloaderExecutor() { @Inject public ThreadSaveManager( DatabaseManager databaseManager, - SavedThreadLoaderRepository savedThreadLoaderRepository) { + SavedThreadLoaderRepository savedThreadLoaderRepository, + FileManager fileManager) { this.databaseManager = databaseManager; this.savedThreadLoaderRepository = savedThreadLoaderRepository; this.databaseSavedThreadManager = databaseManager.getDatabaseSavedThreadManager(); + this.fileManager = fileManager; initRxWorkerQueue(); } @@ -309,6 +315,10 @@ private Single saveThreadInternal(@NonNull Loadable loadable, List saveThreadInternal(@NonNull Loadable loadable, List newPosts = filterAndSortPosts(threadSaveDirImages, loadable, postsToSave); @@ -359,10 +357,8 @@ private Single saveThreadInternal(@NonNull Loadable loadable, List saveThreadInternal(@NonNull Loadable loadable, List newPosts) { * If a post has at least one image that has not been downloaded yet it will be * redownloaded again * */ - private List filterAndSortPosts(File threadSaveDirImages, Loadable loadable, List inputPosts) { + private List filterAndSortPosts(ExternalFile threadSaveDirImages, Loadable loadable, List inputPosts) { // Filter out already saved posts (by lastSavedPostNo) int lastSavedPostNo = databaseManager.runTask(databaseSavedThreadManager.getLastSavedPostNo(loadable.id)); @@ -543,13 +574,18 @@ private List filterAndSortPosts(File threadSaveDirImages, Loadable loadabl return posts; } - private boolean checkWhetherAllPostImagesAreAlreadySaved(File threadSaveDirImages, Post post) { + private boolean checkWhetherAllPostImagesAreAlreadySaved( + ExternalFile threadSaveDirImages, + Post post) { for (PostImage postImage : post.images) { { String originalImageFilename = postImage.originalName + "_" + ORIGINAL_FILE_NAME + "." + postImage.extension; - File originalImage = new File(threadSaveDirImages, originalImageFilename); + ExternalFile originalImage = threadSaveDirImages + .clone() + .appendFileNameSegment(originalImageFilename); + if (!originalImage.exists()) { return false; } @@ -557,15 +593,21 @@ private boolean checkWhetherAllPostImagesAreAlreadySaved(File threadSaveDirImage if (!originalImage.canRead()) { if (!originalImage.delete()) { Logger.e(TAG, "Could not delete originalImage with path " - + originalImage.getAbsolutePath()); + + originalImage.getFullPath()); } return false; } - if (originalImage.length() == 0L) { + long length = originalImage.getLength(); + if (length == -1L) { + throw new IllegalStateException("originalImage.getLength() returned -1, " + + "originalImagePath = " + originalImage.getFullPath()); + } + + if (length == 0L) { if (!originalImage.delete()) { Logger.e(TAG, "Could not delete originalImage with path " - + originalImage.getAbsolutePath()); + + originalImage.getFullPath()); } return false; } @@ -577,7 +619,10 @@ private boolean checkWhetherAllPostImagesAreAlreadySaved(File threadSaveDirImage String thumbnailImageFilename = postImage.originalName + "_" + THUMBNAIL_FILE_NAME + "." + thumbnailExtension; - File thumbnailImage = new File(threadSaveDirImages, thumbnailImageFilename); + ExternalFile thumbnailImage = threadSaveDirImages + .clone() + .appendFileNameSegment(thumbnailImageFilename); + if (!thumbnailImage.exists()) { return false; } @@ -585,15 +630,21 @@ private boolean checkWhetherAllPostImagesAreAlreadySaved(File threadSaveDirImage if (!thumbnailImage.canRead()) { if (!thumbnailImage.delete()) { Logger.e(TAG, "Could not delete thumbnailImage with path " - + thumbnailImage.getAbsolutePath()); + + thumbnailImage.getFullPath()); } return false; } - if (thumbnailImage.length() == 0L) { + long length = thumbnailImage.getLength(); + if (length == -1L) { + throw new IllegalStateException("thumbnailImage.getLength() returned -1, " + + "thumbnailImagePath = " + thumbnailImage.getFullPath()); + } + + if (length == 0L) { if (!thumbnailImage.delete()) { Logger.e(TAG, "Could not delete thumbnailImage with path " - + thumbnailImage.getAbsolutePath()); + + thumbnailImage.getFullPath()); } return false; } @@ -605,7 +656,7 @@ private boolean checkWhetherAllPostImagesAreAlreadySaved(File threadSaveDirImage private boolean downloadSpoilerImage( Loadable loadable, - File threadSaveDirImages, + ExternalFile threadSaveDirImages, HttpUrl spoilerImageUrl) throws IOException { // If the board uses spoiler image - download it if (loadable.board.spoilers && spoilerImageUrl != null) { @@ -618,8 +669,10 @@ private boolean downloadSpoilerImage( } String spoilerImageName = SPOILER_FILE_NAME + "." + spoilerImageExtension; - File spoilerImageFullPath = new File(threadSaveDirImages, spoilerImageName); + ExternalFile spoilerImageFullPath = threadSaveDirImages + .clone() + .appendFileNameSegment(spoilerImageName); if (spoilerImageFullPath.exists()) { // Do nothing if already downloaded return false; @@ -655,7 +708,7 @@ private HttpUrl getSpoilerImageUrl(List posts) { private Flowable downloadImages( Loadable loadable, - File threadSaveDirImages, + ExternalFile threadSaveDirImages, Post post, AtomicInteger currentImageDownloadIndex, int postsWithImagesCount, @@ -795,22 +848,26 @@ private void logThreadDownloadingProgress( } private void deleteImageCompletely( - File threadSaveDirImages, + ExternalFile threadSaveDirImages, String filename, String extension) { Logger.d(TAG, "Deleting a file with name " + filename); boolean error = false; - File originalFile = new File(threadSaveDirImages, - filename + "_" + ORIGINAL_FILE_NAME + "." + extension); + ExternalFile originalFile = threadSaveDirImages + .clone() + .appendFileNameSegment(filename + "_" + ORIGINAL_FILE_NAME + "." + extension); + if (originalFile.exists()) { if (!originalFile.delete()) { error = true; } } - File thumbnailFile = new File(threadSaveDirImages, - filename + "_" + THUMBNAIL_FILE_NAME + "." + extension); + ExternalFile thumbnailFile = threadSaveDirImages + .clone() + .appendFileNameSegment(filename + "_" + THUMBNAIL_FILE_NAME + "." + extension); + if (thumbnailFile.exists()) { if (!thumbnailFile.delete()) { error = true; @@ -826,7 +883,7 @@ private void deleteImageCompletely( * Downloads an image with it's thumbnail and stores them to the disk * */ private void downloadImageIntoFile( - File threadSaveDirImages, + ExternalFile threadSaveDirImages, String filename, String originalExtension, String thumbnailExtension, @@ -871,7 +928,7 @@ private boolean shouldDownloadImages() { * */ private void downloadImage( Loadable loadable, - File threadSaveDirImages, + ExternalFile threadSaveDirImages, String filename, HttpUrl imageUrl) throws IOException, ImageWasAlreadyDeletedException { if (!shouldDownloadImages()) { @@ -890,7 +947,10 @@ private void downloadImage( return; } - File imageFile = new File(threadSaveDirImages, filename); + ExternalFile imageFile = threadSaveDirImages + .clone() + .appendFileNameSegment(filename); + if (!imageFile.exists()) { Request request = new Request.Builder().url(imageUrl).build(); @@ -960,11 +1020,11 @@ private boolean isCurrentDownloadRunning(Loadable loadable) { * Writes image's bytes to a file * */ private void storeImageToFile( - File imageFile, + ExternalFile imageFile, Response response) throws IOException { - if (!imageFile.createNewFile()) { + if (!imageFile.create()) { throw new IOException("Could not create a file to save an image to (path: " - + imageFile.getAbsolutePath() + ")"); + + imageFile.getFullPath() + ")"); } try (ResponseBody body = response.body()) { @@ -977,7 +1037,12 @@ private void storeImageToFile( } try (InputStream is = body.byteStream()) { - try (OutputStream os = new FileOutputStream(imageFile)) { + try (OutputStream os = imageFile.getOutputStream()) { + if (os == null) { + throw new IOException("Could not get OutputStream from imageFile, " + + "imageFilePath = " + imageFile.getFullPath()); + } + IOUtils.copy(is, os); } } @@ -1142,29 +1207,29 @@ public NoNewPostsToSaveException() { } class CouldNotCreateThreadDirectoryException extends Exception { - public CouldNotCreateThreadDirectoryException(File threadSaveDir) { + public CouldNotCreateThreadDirectoryException(ExternalFile threadSaveDir) { super("Could not create a directory to save the thread " + - "to (full path: " + threadSaveDir.getAbsolutePath() + ")"); + "to (full path: " + threadSaveDir.getFullPath() + ")"); } } class CouldNotCreateNoMediaFile extends Exception { - public CouldNotCreateNoMediaFile(File threadSaveDirImages) { - super("Could not create .nomedia file in directory " + threadSaveDirImages.getAbsolutePath()); + public CouldNotCreateNoMediaFile(ExternalFile threadSaveDirImages) { + super("Could not create .nomedia file in directory " + threadSaveDirImages.getFullPath()); } } class CouldNotCreateImagesDirectoryException extends Exception { - public CouldNotCreateImagesDirectoryException(File threadSaveDirImages) { + public CouldNotCreateImagesDirectoryException(ExternalFile threadSaveDirImages) { super("Could not create a directory to save the thread images" + - "to (full path: " + threadSaveDirImages.getAbsolutePath() + ")"); + "to (full path: " + threadSaveDirImages.getFullPath() + ")"); } } class CouldNotCreateSpoilerImageDirectoryException extends Exception { - public CouldNotCreateSpoilerImageDirectoryException(File boardSaveDir) { + public CouldNotCreateSpoilerImageDirectoryException(ExternalFile boardSaveDir) { super("Could not create a directory to save the spoiler image " + - "to (full path: " + boardSaveDir.getAbsolutePath() + ")"); + "to (full path: " + boardSaveDir.getFullPath() + ")"); } } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/model/save/SerializableThread.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/model/save/SerializableThread.java index 65887babd1..31e7083047 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/model/save/SerializableThread.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/model/save/SerializableThread.java @@ -2,6 +2,7 @@ import com.github.adamantcheese.chan.core.mapper.PostMapper; import com.github.adamantcheese.chan.core.model.Post; +import com.github.adamantcheese.chan.utils.BackgroundUtils; import com.google.gson.annotations.SerializedName; import java.util.ArrayList; @@ -27,6 +28,10 @@ public List getPostList() { * Merge old posts with new posts avoiding duplicates and then sort merged list * */ public SerializableThread merge(List posts) { + if (BackgroundUtils.isMainThread()) { + throw new RuntimeException("Cannot be executed on the main thread!"); + } + Set postsSet = new HashSet<>(posts.size() + postList.size()); postsSet.addAll(postList); @@ -36,6 +41,7 @@ public SerializableThread merge(List posts) { List filteredPosts = new ArrayList<>(postsSet.size()); filteredPosts.addAll(postsSet); + postsSet.clear(); Collections.sort(filteredPosts, postComparator); @@ -44,7 +50,6 @@ public SerializableThread merge(List posts) { return this; } - private static final Comparator postComparator = (o1, o2) -> { - return Integer.compare(o1.getNo(), o2.getNo()); - }; + private static final Comparator postComparator + = (o1, o2) -> Integer.compare(o1.getNo(), o2.getNo()); } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/SavedThreadLoaderRepository.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/SavedThreadLoaderRepository.java index 7a0af291e5..61ba3041ed 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/SavedThreadLoaderRepository.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/SavedThreadLoaderRepository.java @@ -1,23 +1,29 @@ package com.github.adamantcheese.chan.core.repository; +import android.os.ParcelFileDescriptor; + import androidx.annotation.Nullable; import com.github.adamantcheese.chan.core.mapper.ThreadMapper; import com.github.adamantcheese.chan.core.model.Post; import com.github.adamantcheese.chan.core.model.save.SerializableThread; +import com.github.adamantcheese.chan.core.saf.file.ExternalFile; +import com.github.adamantcheese.chan.utils.BackgroundUtils; +import com.github.adamantcheese.chan.utils.Logger; import com.google.gson.Gson; -import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; import java.io.IOException; -import java.io.RandomAccessFile; -import java.nio.charset.StandardCharsets; import java.util.List; import javax.inject.Inject; public class SavedThreadLoaderRepository { - public static final String THREAD_FILE_NAME = "thread.json"; + private static final String TAG = "SavedThreadLoaderRepository"; private static final int MAX_THREAD_SIZE_BYTES = 50 * 1024 * 1024; // 50mb + private static final int THREAD_FILE_HEADER_SIZE = 4; + public static final String THREAD_FILE_NAME = "thread.json"; private Gson gson; @@ -34,55 +40,112 @@ public SavedThreadLoaderRepository(Gson gson) { @Nullable public SerializableThread loadOldThreadFromJsonFile( - File threadSaveDir) throws IOException, OldThreadTakesTooMuchSpace { - File threadFile = new File(threadSaveDir, THREAD_FILE_NAME); + ExternalFile threadSaveDir) throws IOException, OldThreadTakesTooMuchSpace { + if (BackgroundUtils.isMainThread()) { + throw new RuntimeException("Cannot be executed on the main thread!"); + } + + ExternalFile threadFile = threadSaveDir + .clone() + .appendFileNameSegment(THREAD_FILE_NAME); if (!threadFile.exists()) { + Logger.d(TAG, "threadFile does not exist, threadFilePath = " + threadFile.getFullPath()); return null; } - String json; - - try (RandomAccessFile raf = new RandomAccessFile(threadFile, "rw")) { - int size = raf.readInt(); - if (size <= 0 || size > MAX_THREAD_SIZE_BYTES) { - throw new OldThreadTakesTooMuchSpace(size); + try (ParcelFileDescriptor parcelFileDescriptor = threadFile.getParcelFileDescriptor( + ExternalFile.FileDescriptorMode.Read)) { + if (parcelFileDescriptor == null) { + Logger.d(TAG, "getParcelFileDescriptor() returned null, threadFilePath = " + + threadFile.getFullPath()); + return null; } - byte[] bytes = new byte[size]; - raf.read(bytes); + try (FileReader fileReader = new FileReader(parcelFileDescriptor.getFileDescriptor())) { + int fileLength = getThreadFileLength(fileReader); + if (fileLength <= 0 || fileLength > MAX_THREAD_SIZE_BYTES) { + throw new OldThreadTakesTooMuchSpace(fileLength); + } - json = new String(bytes, StandardCharsets.UTF_8); - } + long skipped = fileReader.skip(THREAD_FILE_HEADER_SIZE); + if (skipped != THREAD_FILE_HEADER_SIZE) { + throw new IOException("Could not skip " + THREAD_FILE_HEADER_SIZE + " bytes"); + } - return gson.fromJson(json, SerializableThread.class); + return gson.fromJson(fileReader, SerializableThread.class); + } + } } public void savePostsToJsonFile( @Nullable SerializableThread oldSerializableThread, List posts, - File threadSaveDir) throws IOException, CouldNotCreateThreadFile { - SerializableThread serializableThread; - - if (oldSerializableThread != null) { - // Merge with old posts if there are any - serializableThread = oldSerializableThread.merge(posts); - } else { - // Use only the new posts - serializableThread = ThreadMapper.toSerializableThread(posts); + ExternalFile threadSaveDir + ) throws IOException, CouldNotCreateThreadFile, CouldNotGetParcelFileDescriptor { + if (BackgroundUtils.isMainThread()) { + throw new RuntimeException("Cannot be executed on the main thread!"); } - String threadJson = gson.toJson(serializableThread); + ExternalFile threadFile = threadSaveDir + .clone() + .appendFileNameSegment(THREAD_FILE_NAME); - File threadFile = new File(threadSaveDir, THREAD_FILE_NAME); - if (!threadFile.exists() && !threadFile.createNewFile()) { + if (!threadFile.exists() && !threadFile.create()) { throw new CouldNotCreateThreadFile(threadFile); } - // Update the thread file - try (RandomAccessFile raf = new RandomAccessFile(threadFile, "rw")) { - byte[] bytes = threadJson.getBytes(StandardCharsets.UTF_8); - raf.writeInt(bytes.length); - raf.write(bytes); + try (ParcelFileDescriptor parcelFileDescriptor = threadFile.getParcelFileDescriptor( + ExternalFile.FileDescriptorMode.WriteTruncate)) { + if (parcelFileDescriptor == null) { + throw new CouldNotGetParcelFileDescriptor(threadFile); + } + + + // Update the thread file + try (FileWriter fileWriter = new FileWriter(parcelFileDescriptor.getFileDescriptor())) { + SerializableThread serializableThread; + + if (oldSerializableThread != null) { + // Merge with old posts if there are any + serializableThread = oldSerializableThread.merge(posts); + } else { + // Use only the new posts + serializableThread = ThreadMapper.toSerializableThread(posts); + } + + char[] threadJsonBytes = gson.toJson(serializableThread).toCharArray(); + char[] lengthChars = String.valueOf(threadJsonBytes.length).toCharArray(); + + // TODO: may not work! + fileWriter.write(lengthChars); + fileWriter.write(threadJsonBytes); + } + } + } + + private int getThreadFileLength(FileReader fileReader) throws IOException { + char[] sizeBytes = new char[THREAD_FILE_HEADER_SIZE]; + int readCount = fileReader.read(sizeBytes); + + if (readCount != THREAD_FILE_HEADER_SIZE) { + throw new IOException("Could not read the length of the thread from the thread file header"); + } + + String sizeBytesString = String.valueOf(sizeBytes); + + try { + return Integer.parseInt(sizeBytesString); + } catch (NumberFormatException nfe) { + // Convert the NumberFormatException into an IOException + throw new IOException("Couldn't convert file size string into an int, sizeBytesString = " + + sizeBytesString); + } + } + + public class CouldNotGetParcelFileDescriptor extends Exception { + public CouldNotGetParcelFileDescriptor(ExternalFile threadFile) { + super("getParcelFileDescriptor() returned null, threadFilePath = " + + threadFile.getFullPath()); } } @@ -95,9 +158,9 @@ public OldThreadTakesTooMuchSpace(int size) { } public class CouldNotCreateThreadFile extends Exception { - public CouldNotCreateThreadFile(File threadFile) { + public CouldNotCreateThreadFile(ExternalFile threadFile) { super("Could not create the thread file " + - "(path: " + threadFile.getAbsolutePath() + ")"); + "(path: " + threadFile.getFullPath() + ")"); } } } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt index 6485c6bccb..299dc320ae 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt @@ -132,6 +132,23 @@ class FileManager( return RawFile(AbstractFile.Root.DirRoot(File(path))) } + /** + * Copy one file's contents into another + * */ + fun copyFileContents(source: AbstractFile, destination: AbstractFile): Boolean { + return try { + source.getInputStream()?.use { inputStream -> + destination.getOutputStream()?.use { outputStream -> + IOUtils.copy(inputStream, outputStream) + true + } + } ?: false + } catch (e: IOException) { + Logger.e(TAG, "IOException while copying one file into another", e) + false + } + } + private fun toDocumentFile(uri: Uri): DocumentFile? { if (!DocumentFile.isDocumentUri(appContext, uri)) { Logger.e(TAG, "Not a DocumentFile, uri = $uri") @@ -158,23 +175,6 @@ class FileManager( } } - /** - * Copy one file's contents into another - * */ - fun copyFileContents(source: AbstractFile, destination: AbstractFile): Boolean { - return try { - source.getInputStream()?.use { inputStream -> - destination.getOutputStream()?.use { outputStream -> - IOUtils.copy(inputStream, outputStream) - true - } - } ?: false - } catch (e: IOException) { - Logger.e(TAG, "IOException while copying one file into another", e) - false - } - } - companion object { private const val TAG = "FileManager" } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt index 1ee9d3b12e..565edfed8b 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt @@ -21,6 +21,77 @@ import java.io.OutputStream * * Other methods are marked with [ImmutableMethod] annotation. This means that those files create a * copy of the [AbstractFile] internally and are safe to use without calling [clone] + * + * Examples. + * + * Usually you want to create an [AbstractFile] pointing to some directory (like the Kuroba base dir) + * and then create either subdirectories or files inside that directory. You can start with one of the + * following methods: + * + * // Creates an [AbstractFile] from base SAF directory. Be aware that the Uri must not be created + * // manually! This won't work with SAF since one file may have it's Uri changed when Android decides to + * // do so. Usually you want to call file or directory chooser via SAF API (there are methods for + * // that in the [FileManager] class) which will return an Uri that you can then pass into fromUri() + * // method. But usually you don't even need to do this since we usually do this once to get the + * // Kuroba base directory and then just do our work inside of that directory. + * AbstractFile baseDir = fileManager.fromUri(Uri.parse(ChanSettings.saveLocationUri.get())); + * + * // Creates an [AbstractFile] from base raw directory + * AbstractFile baseDir = fileManager.fromRawFile(new File(ChanSettings.saveLocation.get())); + * + * // Same as above + * AbstractFile baseDir = fileManager.fromPath(ChanSettings.saveLocation.get()); + * + * + * Then you can start appending subdirectories or a filename: + * + * // This will create a "test.txt" file located at /dir1/dir2/dir3, i.e. + * // /dir1/dir2/dir3/test.txt + * AbstractFile newFile = baseDir + * .appendSubDirSegment("dir1") + * .appendSubDirSegment("dir2") + * .appendSubDirSegment("dir3") + * .appendFileNameSegment("test.txt") + * .createNew(); + * + * Then you can call methods that are similar to the standard Java File API, e.g. [exists], + * [getName], [getLength], [isFile], [isDirectory], [canRead], [canWrite] etc. + * + * If you want to work with multiple files in a directory (or sub directories) you may want + * to [clone] the file that represents that directory, e.g: + * + * AbstractFile clonedFile = baseDir.clone(); + * + * AbstractFile f1 = clonedFile + * .appendFileNameSegment("f1.txt") + * .createNew(); + * AbstractFile f2 = clonedFile + * .appendFileNameSegment("f2.txt") + * .createNew(); + * AbstractFile f3 = clonedFile + * .appendFileNameSegment("f3.txt") + * .createNew(); + * + * You have to do this because some methods may mutate the internal state of the [AbstractFile], so + * after calling, let's say: + * + * AbstractFile f1 = baseDir + * .appendFileNameSegment("f1.txt") + * .createNew(); + * + * Without cloning it first baseDir will be start pointing to /f1.txt instead of + * just . The same thing applies to any method marked with [MutableMethod] annotation. + * methods marked with [ImmutableMethod] do this stuff internally so they are safe to use without + * cloning. + * + * Sometimes you don't know which external directory to choose to store a new file (the SAF or the + * old raw Java File external directory). In this case you can use: + * + * AbstractFile baseDir = fileManager.newFile(); + * + * Method which will create an [AbstractFile] with root pointing to either Kuroba SAF base directory + * (if user has set it) or if he didn't then to the default external directory (Backed by raw Java File). + * * */ abstract class AbstractFile( /** @@ -28,7 +99,6 @@ abstract class AbstractFile( * */ protected val segments: MutableList ) { - /** * Appends a new subdirectory to the root directory * */ @@ -53,12 +123,15 @@ abstract class AbstractFile( return createNew() != null } + // TODO: make exists(), isFile(), isDirectory(), canRead(), canWrite(), delete(), getInputStream(), + // getOutputStream(), getName() and getLength() immutable, update documentation and comments/annotations + /** - * When doing something with an AbstractFile (like appending a subdir or a filename) the - * AbstractFile will change because it's mutable. So if you don't want to change the original - * AbstractFile you need to make a copy via this method (like, if you want to search for + * When doing something with an [AbstractFile] (like appending a subdir or a filename) the + * [AbstractFile] will change because it's mutable. So if you don't want to change the original + * [AbstractFile] you need to make a copy via this method (like, if you want to search for * a couple of files in the same directory you would want to clone the directory - * AbstractFile and then append the filename to those copies) + * [AbstractFile] and then append the filename to those copies) * */ abstract fun clone(): T @@ -98,6 +171,9 @@ abstract class AbstractFile( @ImmutableMethod abstract fun findFile(fileName: String): T? + @MutableMethod + abstract fun getLength(): Long + /** * Removes the last appended segment if there are any * e.g: /test/123/test2 -> /test/123 -> /test @@ -112,19 +188,12 @@ abstract class AbstractFile( return true } + @Suppress("UNCHECKED_CAST") protected fun appendSubDirSegmentInner(name: String): T { - if (isFilenameAppended()) { - throw IllegalStateException("Cannot append anything after file name has been appended") - } - - if (name.isBlank()) { - throw IllegalArgumentException("Bad name: $name") - } - - if (name.extension() != null) { - throw IllegalArgumentException("Directory name must not contain extension, " + - "extension = ${name.extension()}") - } + check(!isFilenameAppended()) { "Cannot append anything after file name has been appended" } + require(!name.isBlank()) { "Bad name: $name" } + require(name.extension() == null) { "Directory name must not contain extension, " + + "extension = ${name.extension()}" } val nameList = if (name.contains(File.separatorChar)) { name.split(File.separatorChar) @@ -134,9 +203,8 @@ abstract class AbstractFile( nameList .onEach { splitName -> - if (splitName.extension() != null) { - throw IllegalArgumentException("appendSubDirSegment does not allow segments " + - "with extensions! bad name = $splitName") + require(splitName.extension() == null) { + "appendSubDirSegment does not allow segments with extensions! bad name = $splitName" } } .map { splitName -> Segment(splitName) } @@ -145,20 +213,14 @@ abstract class AbstractFile( return this as T } + @Suppress("UNCHECKED_CAST") protected fun appendFileNameSegmentInner(name: String): T { - if (isFilenameAppended()) { - throw IllegalStateException("Cannot append anything after file name has been appended") - } - - if (name.isBlank()) { - throw IllegalArgumentException("Bad name: $name") - } + check(!isFilenameAppended()) { "Cannot append anything after file name has been appended" } + require(!name.isBlank()) { "Bad name: $name" } val nameList = if (name.contains(File.separatorChar)) { val split = name.split(File.separatorChar) - if (split.size < 2) { - throw IllegalStateException("Should have at least two entries, name = $name") - } + check(split.size >= 2) { "Should have at least two entries, name = $name" } split } else { @@ -166,9 +228,9 @@ abstract class AbstractFile( } for ((index, splitName) in nameList.withIndex()) { - if (splitName.extension() != null && index != nameList.lastIndex) { - throw IllegalArgumentException("Only the last split segment may have a file name, " + - "bad segment index = ${index}/${nameList.lastIndex}, bad name = $splitName") + require(!(splitName.extension() != null && index != nameList.lastIndex)) { + "Only the last split segment may have a file name, " + + "bad segment index = ${index}/${nameList.lastIndex}, bad name = $splitName" } val isFileName = index == nameList.lastIndex diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt index 0ff7971ef4..587daaec14 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt @@ -20,25 +20,20 @@ class ExternalFile( private val mimeTypeMap = MimeTypeMap.getSingleton() override fun appendSubDirSegment(name: String): T { - if (root is Root.FileRoot) { - throw IllegalStateException("root is already FileRoot, cannot append anything anymore") - } - + check(root !is Root.FileRoot) { "root is already FileRoot, cannot append anything anymore" } return super.appendSubDirSegmentInner(name) } override fun appendFileNameSegment(name: String): T { - if (root is Root.FileRoot) { - throw IllegalStateException("root is already FileRoot, cannot append anything anymore") - } - + check(root !is Root.FileRoot) { "root is already FileRoot, cannot append anything anymore" } return super.appendFileNameSegmentInner(name) } + @Suppress("UNCHECKED_CAST") override fun createNew(): T? { - if (root is Root.FileRoot) { + check(root !is Root.FileRoot) { // TODO: do we need this check? - throw IllegalStateException("root is already FileRoot, cannot append anything anymore") + "root is already FileRoot, cannot append anything anymore" } if (segments.isEmpty()) { @@ -103,6 +98,7 @@ class ExternalFile( return ExternalFile(appContext, root) as T } + @Suppress("UNCHECKED_CAST") override fun clone(): T = ExternalFile( appContext, root.clone(), @@ -114,6 +110,7 @@ class ExternalFile( override fun canRead(): Boolean = toDocumentFile()?.canRead() ?: false override fun canWrite(): Boolean = toDocumentFile()?.canWrite() ?: false + @Suppress("UNCHECKED_CAST") override fun getParent(): T? { if (segments.isNotEmpty()) { removeLastSegment() @@ -212,10 +209,9 @@ class ExternalFile( ?: throw IllegalStateException("Could not extract file name from document file") } + @Suppress("UNCHECKED_CAST") override fun findFile(fileName: String): T? { - if (root is Root.FileRoot) { - throw IllegalStateException("Cannot use FileRoot as directory") - } + check(root !is Root.FileRoot) { "Cannot use FileRoot as directory" } val filteredSegments = segments .map { it.name } @@ -263,6 +259,7 @@ class ExternalFile( return null } + override fun getLength(): Long = toDocumentFile()?.length() ?: -1L fun getParcelFileDescriptor(fileDescriptorMode: FileDescriptorMode): ParcelFileDescriptor? { return appContext.contentResolver.openFileDescriptor( diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt index 55c35ac532..20a2699b7f 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt @@ -12,25 +12,20 @@ class RawFile( ) : AbstractFile(segments) { override fun appendSubDirSegment(name: String): T { - if (root is Root.FileRoot) { - throw IllegalStateException("root is already FileRoot, cannot append anything anymore") - } - + check(root !is Root.FileRoot) { "root is already FileRoot, cannot append anything anymore" } return super.appendSubDirSegmentInner(name) } override fun appendFileNameSegment(name: String): T { - if (root is Root.FileRoot) { - throw IllegalStateException("root is already FileRoot, cannot append anything anymore") - } - + check(root !is Root.FileRoot) { "root is already FileRoot, cannot append anything anymore" } return super.appendFileNameSegmentInner(name) } + @Suppress("UNCHECKED_CAST") override fun createNew(): T? { - if (root is Root.FileRoot) { + check(root !is Root.FileRoot) { // TODO: do we need this check? - throw IllegalStateException("root is already FileRoot, cannot append anything anymore") + "root is already FileRoot, cannot append anything anymore" } if (segments.isEmpty()) { @@ -65,6 +60,7 @@ class RawFile( return RawFile(Root.DirRoot(newFile)) as T } + @Suppress("UNCHECKED_CAST") override fun clone(): T = RawFile( root.clone(), segments.toMutableList()) as T @@ -75,6 +71,7 @@ class RawFile( override fun canRead(): Boolean = toFile().canRead() override fun canWrite(): Boolean = toFile().canWrite() + @Suppress("UNCHECKED_CAST") override fun getParent(): T? { if (segments.isNotEmpty()) { removeLastSegment() @@ -140,13 +137,11 @@ class RawFile( return toFile().name } + @Suppress("UNCHECKED_CAST") override fun findFile(fileName: String): T? { - if (root is Root.FileRoot) { - throw IllegalStateException("Cannot use FileRoot as directory") - } + check(root !is Root.FileRoot) { "Cannot use FileRoot as directory" } val copy = File(root.holder.absolutePath) - if (segments.isNotEmpty()) { copy.appendMany(segments.map { segment -> segment.name }) } @@ -165,6 +160,8 @@ class RawFile( return RawFile(newRoot) as T } + override fun getLength(): Long = toFile().length() + private fun toFile(): File { return if (segments.isEmpty()) { root.holder From b4f288b530015b381614cf86290c4d8b618bcd4f Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 24 Aug 2019 13:13:48 +0300 Subject: [PATCH 027/184] (#172) Make most of the methods in AbstractFile immutable because it makes more sense this way --- .../chan/core/saf/file/AbstractFile.kt | 23 ++++++++----------- .../chan/core/saf/file/ExternalFile.kt | 20 ++++++++-------- .../chan/core/saf/file/RawFile.kt | 20 ++++++++-------- .../controller/MediaSettingsController.java | 6 ++++- 4 files changed, 35 insertions(+), 34 deletions(-) diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt index 565edfed8b..74cc1f7453 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt @@ -123,9 +123,6 @@ abstract class AbstractFile( return createNew() != null } - // TODO: make exists(), isFile(), isDirectory(), canRead(), canWrite(), delete(), getInputStream(), - // getOutputStream(), getName() and getLength() immutable, update documentation and comments/annotations - /** * When doing something with an [AbstractFile] (like appending a subdir or a filename) the * [AbstractFile] will change because it's mutable. So if you don't want to change the original @@ -135,19 +132,19 @@ abstract class AbstractFile( * */ abstract fun clone(): T - @MutableMethod + @ImmutableMethod abstract fun exists(): Boolean - @MutableMethod + @ImmutableMethod abstract fun isFile(): Boolean - @MutableMethod + @ImmutableMethod abstract fun isDirectory(): Boolean - @MutableMethod + @ImmutableMethod abstract fun canRead(): Boolean - @MutableMethod + @ImmutableMethod abstract fun canWrite(): Boolean @MutableMethod @@ -156,22 +153,22 @@ abstract class AbstractFile( @ImmutableMethod abstract fun getFullPath(): String - @MutableMethod + @ImmutableMethod abstract fun delete(): Boolean - @MutableMethod + @ImmutableMethod abstract fun getInputStream(): InputStream? - @MutableMethod + @ImmutableMethod abstract fun getOutputStream(): OutputStream? - @MutableMethod + @ImmutableMethod abstract fun getName(): String @ImmutableMethod abstract fun findFile(fileName: String): T? - @MutableMethod + @ImmutableMethod abstract fun getLength(): Long /** diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt index 587daaec14..b5ec8e9eab 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt @@ -104,11 +104,11 @@ class ExternalFile( root.clone(), segments.toMutableList()) as T - override fun exists(): Boolean = toDocumentFile()?.exists() ?: false - override fun isFile(): Boolean = toDocumentFile()?.isFile ?: false - override fun isDirectory(): Boolean = toDocumentFile()?.isDirectory ?: false - override fun canRead(): Boolean = toDocumentFile()?.canRead() ?: false - override fun canWrite(): Boolean = toDocumentFile()?.canWrite() ?: false + override fun exists(): Boolean = clone().toDocumentFile()?.exists() ?: false + override fun isFile(): Boolean = clone().toDocumentFile()?.isFile ?: false + override fun isDirectory(): Boolean = clone().toDocumentFile()?.isDirectory ?: false + override fun canRead(): Boolean = clone().toDocumentFile()?.canRead() ?: false + override fun canWrite(): Boolean = clone().toDocumentFile()?.canWrite() ?: false @Suppress("UNCHECKED_CAST") override fun getParent(): T? { @@ -138,12 +138,12 @@ class ExternalFile( } override fun delete(): Boolean { - return toDocumentFile()?.delete() ?: false + return clone().toDocumentFile()?.delete() ?: false } override fun getInputStream(): InputStream? { val contentResolver = appContext.contentResolver - val documentFile = toDocumentFile() + val documentFile = clone().toDocumentFile() if (documentFile == null) { Logger.e(TAG, "getInputStream() toDocumentFile() returned null") @@ -170,7 +170,7 @@ class ExternalFile( override fun getOutputStream(): OutputStream? { val contentResolver = appContext.contentResolver - val documentFile = toDocumentFile() + val documentFile = clone().toDocumentFile() if (documentFile == null) { Logger.e(TAG, "getOutputStream() toDocumentFile() returned null") @@ -200,7 +200,7 @@ class ExternalFile( return segments.last().name } - val documentFile = toDocumentFile() + val documentFile = clone().toDocumentFile() if (documentFile == null) { throw IllegalStateException("getName() toDocumentFile() returned null") } @@ -259,7 +259,7 @@ class ExternalFile( return null } - override fun getLength(): Long = toDocumentFile()?.length() ?: -1L + override fun getLength(): Long = clone().toDocumentFile()?.length() ?: -1L fun getParcelFileDescriptor(fileDescriptorMode: FileDescriptorMode): ParcelFileDescriptor? { return appContext.contentResolver.openFileDescriptor( diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt index 20a2699b7f..60db3b6635 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt @@ -65,11 +65,11 @@ class RawFile( root.clone(), segments.toMutableList()) as T - override fun exists(): Boolean = toFile().exists() - override fun isFile(): Boolean = toFile().isFile - override fun isDirectory(): Boolean = toFile().isDirectory - override fun canRead(): Boolean = toFile().canRead() - override fun canWrite(): Boolean = toFile().canWrite() + override fun exists(): Boolean = clone().toFile().exists() + override fun isFile(): Boolean = clone().toFile().isFile + override fun isDirectory(): Boolean = clone().toFile().isDirectory + override fun canRead(): Boolean = clone().toFile().canRead() + override fun canWrite(): Boolean = clone().toFile().canWrite() @Suppress("UNCHECKED_CAST") override fun getParent(): T? { @@ -88,11 +88,11 @@ class RawFile( } override fun delete(): Boolean { - return toFile().delete() + return clone().toFile().delete() } override fun getInputStream(): InputStream? { - val file = toFile() + val file = clone().toFile() if (!file.exists()) { Logger.e(TAG, "getInputStream() file does not exist, path = ${file.absolutePath}") @@ -113,7 +113,7 @@ class RawFile( } override fun getOutputStream(): OutputStream? { - val file = toFile() + val file = clone().toFile() if (!file.exists()) { Logger.e(TAG, "getOutputStream() file does not exist, path = ${file.absolutePath}") @@ -134,7 +134,7 @@ class RawFile( } override fun getName(): String { - return toFile().name + return clone().toFile().name } @Suppress("UNCHECKED_CAST") @@ -160,7 +160,7 @@ class RawFile( return RawFile(newRoot) as T } - override fun getLength(): Long = toFile().length() + override fun getLength(): Long = clone().toFile().length() private fun toFile(): File { return if (segments.isEmpty()) { diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java index 34c7634f15..a35eccdb28 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java @@ -34,6 +34,7 @@ import com.github.adamantcheese.chan.ui.settings.SettingView; import com.github.adamantcheese.chan.ui.settings.SettingsController; import com.github.adamantcheese.chan.ui.settings.SettingsGroup; +import com.github.adamantcheese.chan.utils.Logger; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; @@ -50,6 +51,8 @@ import static com.github.adamantcheese.chan.utils.AndroidUtils.getString; public class MediaSettingsController extends SettingsController { + private static final String TAG = "MediaSettingsController"; + // Special setting views private BooleanSettingView boardFolderSetting; private BooleanSettingView threadFolderSetting; @@ -186,6 +189,7 @@ private void populatePreferences() { private void setupLocalThreadLocationSetting(SettingsGroup media) { if (!ChanSettings.incrementalThreadDownloadingEnabled.get()) { + Logger.d(TAG, "setupLocalThreadLocationSetting() incrementalThreadDownloadingEnabled is disabled"); return; } @@ -207,7 +211,7 @@ private void setupLocalThreadLocationSetting(SettingsGroup media) { } private void onLocalThreadsLocationSettingClicked() { - + // TODO } private void setupSaveLocationSetting(SettingsGroup media) { From 3e4f8a733dd791965d01d2121522e63ca560f734 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 24 Aug 2019 15:35:23 +0300 Subject: [PATCH 028/184] (#172) Tests for write to a SAF file, checking it's length and then reading back from it --- .../repository/ImportExportRepository.java | 1 + .../SavedThreadLoaderRepository.java | 1 + .../chan/core/saf/file/AbstractFile.kt | 18 +++++++ .../chan/core/saf/file/ExternalFile.kt | 22 ++++----- .../chan/core/saf/file/RawFile.kt | 25 ++++++++-- .../controller/MediaSettingsController.java | 47 +++++++++++++++++++ 6 files changed, 98 insertions(+), 16 deletions(-) diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/ImportExportRepository.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/ImportExportRepository.java index 0bfba8866e..8a407b58ea 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/ImportExportRepository.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/ImportExportRepository.java @@ -132,6 +132,7 @@ public void exportTo(ExternalFile settingsFile, boolean isNewFile, ImportExportC try (FileWriter writer = new FileWriter(fileDescriptor)) { writer.write(json); + writer.flush(); } Logger.d(TAG, "Exporting done!"); diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/SavedThreadLoaderRepository.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/SavedThreadLoaderRepository.java index 61ba3041ed..062a9ba9a2 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/SavedThreadLoaderRepository.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/SavedThreadLoaderRepository.java @@ -119,6 +119,7 @@ public void savePostsToJsonFile( // TODO: may not work! fileWriter.write(lengthChars); fileWriter.write(threadJsonBytes); + fileWriter.flush(); } } } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt index 74cc1f7453..9c6efdaec2 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt @@ -4,6 +4,7 @@ import com.github.adamantcheese.chan.core.extension import com.github.adamantcheese.chan.core.saf.annotation.ImmutableMethod import com.github.adamantcheese.chan.core.saf.annotation.MutableMethod import java.io.File +import java.io.FileDescriptor import java.io.InputStream import java.io.OutputStream @@ -171,6 +172,11 @@ abstract class AbstractFile( @ImmutableMethod abstract fun getLength(): Long + @ImmutableMethod + abstract fun withFileDescriptor( + fileDescriptorMode: FileDescriptorMode, + func: (FileDescriptor) -> Unit) + /** * Removes the last appended segment if there are any * e.g: /test/123/test2 -> /test/123 -> /test @@ -292,4 +298,16 @@ abstract class AbstractFile( val name: String, val isFileName: Boolean = false ) + + enum class FileDescriptorMode(val mode: String) { + Read("r"), + Write("w"), + // When overwriting an existing file it is a really good ide to use truncate mode, + // because otherwise if a new file's length is less than the old one's then there will be + // old file's data left at the end of the file. Truncate flags will make sure that the file + // is truncated at the end to fit the new length. + WriteTruncate("wt") + + // ReadWrite and ReadWriteTruncate are not supported! + } } \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt index b5ec8e9eab..f1dc3d062c 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt @@ -261,6 +261,15 @@ class ExternalFile( override fun getLength(): Long = clone().toDocumentFile()?.length() ?: -1L + override fun withFileDescriptor( + fileDescriptorMode: FileDescriptorMode, + func: (FileDescriptor) -> Unit) { + getParcelFileDescriptor(fileDescriptorMode)?.use { pfd -> + func(pfd.fileDescriptor) + } ?: throw IllegalStateException("Could not get ParcelFileDescriptor " + + "from root with uri = ${root.holder.uri}") + } + fun getParcelFileDescriptor(fileDescriptorMode: FileDescriptorMode): ParcelFileDescriptor? { return appContext.contentResolver.openFileDescriptor( root.holder.uri, @@ -306,19 +315,6 @@ class ExternalFile( return DocumentFile.fromSingleUri(appContext, builder.build()) } - enum class FileDescriptorMode(val mode: String) { - Read("r"), - Write("w"), - // When overwriting an existing file it is a really good ide to use truncate mode, - // because otherwise if a new file's length is less than the old one's then there will be - // old file's data left at the end of the file - WriteTruncate("wt"), - // It is recommended to prefer either Read or Write modes in the documentation. - // Use ReadWrite only when it is really necessary. - ReadWrite("rw"), - ReadWriteTruncate("rwt") - } - companion object { private const val TAG = "FileManager" } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt index 60db3b6635..9a22628a58 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt @@ -2,9 +2,7 @@ package com.github.adamantcheese.chan.core.saf.file import com.github.adamantcheese.chan.core.appendMany import com.github.adamantcheese.chan.utils.Logger -import java.io.File -import java.io.InputStream -import java.io.OutputStream +import java.io.* class RawFile( private val root: Root, @@ -162,6 +160,27 @@ class RawFile( override fun getLength(): Long = clone().toFile().length() + override fun withFileDescriptor(fileDescriptorMode: FileDescriptorMode, func: (FileDescriptor) -> Unit) { + val fileCopy = clone().toFile() + + when (fileDescriptorMode) { + FileDescriptorMode.Read -> FileInputStream(fileCopy).use { fis -> func(fis.fd) } + FileDescriptorMode.Write, + FileDescriptorMode.WriteTruncate -> { + val fileOutputStream = when (fileDescriptorMode) { + FileDescriptorMode.Write -> FileOutputStream(fileCopy, false) + FileDescriptorMode.WriteTruncate -> FileOutputStream(fileCopy, true) + else -> throw NotImplementedError("Not implemented for " + + "fileDescriptorMode = ${fileDescriptorMode.name}") + } + + fileOutputStream.use { fos -> func(fos.fd) } + } + else -> throw NotImplementedError("Not implemented for " + + "fileDescriptorMode = ${fileDescriptorMode.name}") + } + } + private fun toFile(): File { return if (segments.isEmpty()) { root.holder diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java index a35eccdb28..2225386df9 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java @@ -41,11 +41,18 @@ import org.jetbrains.annotations.NotNull; import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; import java.util.ArrayList; import java.util.List; import javax.inject.Inject; +import kotlin.Unit; + import static com.github.adamantcheese.chan.Chan.inject; import static com.github.adamantcheese.chan.utils.AndroidUtils.getApplicationLabel; import static com.github.adamantcheese.chan.utils.AndroidUtils.getString; @@ -385,6 +392,46 @@ private void testMethod(@NotNull Uri uri) { throw new RuntimeException("Couldn't find filename.json"); } + String testString = "Hello world"; + + foundFile.withFileDescriptor(AbstractFile.FileDescriptorMode.WriteTruncate, (fd) -> { + try (OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(fd))) { + osw.write(testString); + osw.flush(); + } catch (IOException e) { + e.printStackTrace(); + } + + return Unit.INSTANCE; + }); + + if (foundFile.getLength() != testString.length()) { + throw new RuntimeException("file length != testString.length(), file length = " + + foundFile.getLength()); + } + + foundFile.withFileDescriptor(AbstractFile.FileDescriptorMode.Read, (fd) -> { + try (InputStreamReader isr = new InputStreamReader(new FileInputStream(fd))) { + char[] stringBytes = new char[testString.length()]; + int read = isr.read(stringBytes); + + if (read != testString.length()) { + throw new RuntimeException("read bytes != testString.length(), read = " + read); + } + + String resultString = new String(stringBytes); + if (!resultString.equals(testString)){ + throw new RuntimeException("resultString != testString, resultString = " + + resultString); + } + + } catch (IOException e) { + e.printStackTrace(); + } + + return Unit.INSTANCE; + }); + AbstractFile parent = externalFile.getParent().getParent(); if (!parent.getName().equals("1234")) { throw new RuntimeException("dir.name != 1234, name = " + parent.getName()); From 4044b72106c762e394d1bae84c75ae4dbd01b7cc Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 25 Aug 2019 13:49:31 +0300 Subject: [PATCH 029/184] (#172) Lots of improvements/fixes etc Separate local threads download directory from the save location (app base dir) directory. Basically user can now choose separate directories where images or local threads will be stored (both work with SAF and the old API). Make ThreadSaveManager work with AbstractFile instead of ExternalFile so that the user can decide for themselves whether to use SAF or the old API. Local threads now work with AbstractFiles. Some classes had to be changed to use RawFile instead of simple File. Convert couple of classes to Kotlin. More tests for the testMethod. --- Kuroba/app/build.gradle | 5 + .../adamantcheese/chan/core/Extensions.kt | 23 + .../chan/core/cache/CacheHandler.java | 76 ++- .../chan/core/cache/FileCache.java | 76 ++- .../chan/core/cache/FileCacheDownloader.java | 18 +- .../chan/core/cache/FileCacheListener.java | 4 +- .../database/DatabaseSavedThreadManager.java | 44 +- .../adamantcheese/chan/core/di/AppModule.java | 5 +- .../chan/core/di/ManagerModule.java | 4 - .../adamantcheese/chan/core/di/NetModule.java | 5 +- .../chan/core/image/ImageLoaderV2.java | 125 ++-- .../manager/SavedThreadLoaderManager.java | 108 ---- .../core/manager/SavedThreadLoaderManager.kt | 83 +++ .../chan/core/manager/ThreadSaveManager.java | 165 ++--- .../chan/core/manager/UpdateManager.java | 5 +- .../chan/core/manager/WatchManager.java | 6 +- .../model/export/ExportedAppSettings.java | 11 +- .../chan/core/presenter/ReplyPresenter.java | 7 +- .../chan/core/presenter/ThreadPresenter.java | 66 +- .../repository/ImportExportRepository.java | 582 ------------------ .../core/repository/ImportExportRepository.kt | 510 +++++++++++++++ .../SavedThreadLoaderRepository.java | 167 ----- .../repository/SavedThreadLoaderRepository.kt | 107 ++++ .../chan/core/saf/FileManager.kt | 73 ++- .../chan/core/saf/file/AbstractFile.kt | 80 +-- .../chan/core/saf/file/ExternalFile.kt | 85 +-- .../chan/core/saf/file/FileDescriptorMode.kt | 13 + .../chan/core/saf/file/RawFile.kt | 104 ++-- .../chan/core/saver/ImageSaveTask.java | 4 +- .../chan/core/saver/ImageSaver.java | 87 ++- .../chan/core/settings/ChanSettings.java | 40 +- .../controller/AlbumDownloadController.java | 19 +- .../ExperimentalSettingsController.java | 16 +- .../ui/controller/ImageViewerController.java | 8 +- .../ImportExportSettingsController.java | 16 + .../controller/MediaSettingsController.java | 208 ++++++- .../ui/controller/ViewThreadController.java | 10 - .../chan/ui/helper/ImagePickDelegate.java | 27 +- .../chan/ui/service/WatchNotification.java | 4 +- .../chan/ui/view/MultiImageView.java | 17 +- Kuroba/app/src/main/res/values/strings.xml | 10 +- .../chan/core/ExtensionsKtTest.kt | 29 + 42 files changed, 1759 insertions(+), 1293 deletions(-) delete mode 100644 Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/SavedThreadLoaderManager.java create mode 100644 Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/SavedThreadLoaderManager.kt delete mode 100644 Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/ImportExportRepository.java create mode 100644 Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/ImportExportRepository.kt delete mode 100644 Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/SavedThreadLoaderRepository.java create mode 100644 Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/SavedThreadLoaderRepository.kt create mode 100644 Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/FileDescriptorMode.kt create mode 100644 Kuroba/app/src/test/java/com/github/adamantcheese/chan/core/ExtensionsKtTest.kt diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 4117407367..b42bbffacb 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -71,6 +71,10 @@ android { targetCompatibility JavaVersion.VERSION_1_8 } + kotlinOptions { + freeCompilerArgs = ["-Xallow-result-return-type"] + } + lintOptions { abortOnError false } @@ -158,4 +162,5 @@ dependencies { implementation 'io.reactivex.rxjava2:rxjava:2.2.9' implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + testImplementation 'junit:junit:4.12' } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/Extensions.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/Extensions.kt index 47234e5e6b..5052ca3f93 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/Extensions.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/Extensions.kt @@ -64,4 +64,27 @@ fun File.appendMany(segments: List): File { } return newFile +} + +fun Int.toCharArray(): CharArray { + val charArray = CharArray(4) + + charArray[0] = ((this shr 24) and 0x000000FF).toChar() + charArray[1] = ((this shr 16) and 0x000000FF).toChar() + charArray[2] = ((this shr 8) and 0x000000FF).toChar() + charArray[3] = ((this) and 0x000000FF).toChar() + + return charArray +} + +fun CharArray.toInt(): Int { + check(this.size == 4) { "CharArray must have length of exactly 4 bytes" } + + var value: Int = 0 + value = value or (this[0].toInt() shl 24) + value = value or (this[1].toInt() shl 16) + value = value or (this[2].toInt() shl 8) + value = value or (this[3].toInt()) + + return value } \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/CacheHandler.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/CacheHandler.java index fc80875547..d1dd2848e5 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/CacheHandler.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/CacheHandler.java @@ -22,10 +22,13 @@ import androidx.annotation.MainThread; import androidx.annotation.WorkerThread; +import com.github.adamantcheese.chan.core.saf.file.AbstractFile; +import com.github.adamantcheese.chan.core.saf.file.RawFile; import com.github.adamantcheese.chan.core.settings.ChanSettings; import com.github.adamantcheese.chan.utils.Logger; import java.io.File; +import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -40,8 +43,7 @@ public class CacheHandler { private static final long FILE_CACHE_DISK_SIZE = (ChanSettings.autoLoadThreadImages.get() ? 1000 : 100) * 1024 * 1024; private final ExecutorService pool = Executors.newSingleThreadExecutor(); - - private final File directory; + private final RawFile cacheDirFile; /** * An estimation of the current size of the directory. Used to check if trim must be run @@ -50,8 +52,8 @@ public class CacheHandler { private AtomicLong size = new AtomicLong(); private AtomicBoolean trimRunning = new AtomicBoolean(false); - public CacheHandler(File directory) { - this.directory = directory; + public CacheHandler(RawFile cacheDirFile) { + this.cacheDirFile = cacheDirFile; createDirectories(); backgroundRecalculateSize(); @@ -63,9 +65,29 @@ public boolean exists(String key) { } @MainThread - public File get(String key) { + public RawFile get(String key) { createDirectories(); - return new File(directory, String.valueOf(key.hashCode())); + + return cacheDirFile.clone() + .appendSubDirSegment(String.valueOf(key.hashCode())); + } + + public File randomCacheFile() throws IOException { + createDirectories(); + + File cacheDir = new File(cacheDirFile.getFullPath()); + File newFile = new File(cacheDir, String.valueOf(System.nanoTime())); + + while (newFile.exists()) { + newFile = new File(cacheDir, String.valueOf(System.nanoTime())); + } + + if (!newFile.createNewFile()) { + throw new IOException("Could not create new file in cache directory, newFile = " + + newFile.getAbsolutePath()); + } + + return newFile; } @MainThread @@ -94,8 +116,8 @@ protected void fileWasAdded(long fileLen) { public void clearCache() { Logger.d(TAG, "Clearing cache"); - if (directory.exists() && directory.isDirectory()) { - for (File file : directory.listFiles()) { + if (cacheDirFile.exists() && cacheDirFile.isDirectory()) { + for (AbstractFile file : cacheDirFile.listFiles()) { if (!file.delete()) { Logger.d(TAG, "Could not delete cache file while clearing cache " + file.getName()); @@ -108,10 +130,9 @@ public void clearCache() { @MainThread public void createDirectories() { - if (!directory.exists()) { - if (!directory.mkdirs()) { - Logger.e(TAG, "Unable to create file cache dir " + - directory.getAbsolutePath()); + if (!cacheDirFile.exists()) { + if (!cacheDirFile.create()) { + Logger.e(TAG, "Unable to create file cache dir " + cacheDirFile.getFullPath()); } } } @@ -125,11 +146,9 @@ private void backgroundRecalculateSize() { private void recalculateSize() { long calculatedSize = 0; - File[] files = directory.listFiles(); - if (files != null) { - for (File file : files) { - calculatedSize += file.length(); - } + List files = cacheDirFile.listFiles(); + for (RawFile file : files) { + calculatedSize += file.getLength(); } size.set(calculatedSize); @@ -137,16 +156,16 @@ private void recalculateSize() { @WorkerThread private void trim() { - File[] directoryFiles = directory.listFiles(); + List directoryFiles = cacheDirFile.listFiles(); // Don't try to trim empty directories or just one file in it. - if (directoryFiles == null || directoryFiles.length <= 1) { + if (directoryFiles.size() <= 1) { return; } // Get all files with their last modified times. - List> files = new ArrayList<>(directoryFiles.length); - for (File file : directoryFiles) { + List> files = new ArrayList<>(directoryFiles.size()); + for (RawFile file : directoryFiles) { files.add(new Pair<>(file, file.lastModified())); } @@ -154,17 +173,17 @@ private void trim() { Collections.sort(files, (o1, o2) -> Long.signum(o1.second - o2.second)); //Pre-trim based on time, trash anything older than 6 hours - List> removed = new ArrayList<>(); - for (Pair fileLongPair : files) { + List> removed = new ArrayList<>(); + for (Pair fileLongPair : files) { if (fileLongPair.second + 6 * 60 * 60 * 1000 < System.currentTimeMillis()) { - Logger.d(TAG, "Delete for trim " + fileLongPair.first.getAbsolutePath()); + Logger.d(TAG, "Delete for trim " + fileLongPair.first.getFullPath()); if (!fileLongPair.first.delete()) { Logger.e(TAG, "Failed to delete cache file for trim"); } removed.add(fileLongPair); } else break; //only because we sorted earlier } - for (Pair deleted : removed) { + for (Pair deleted : removed) { files.remove(deleted); } recalculateSize(); @@ -172,15 +191,16 @@ private void trim() { // Trim as long as the directory size exceeds the threshold (note that oldest is still first) long workingSize = size.get(); for (int i = 0; workingSize >= FILE_CACHE_DISK_SIZE; i++) { - File file = files.get(i).first; + AbstractFile file = files.get(i).first; - Logger.d(TAG, "Delete for trim " + file.getAbsolutePath()); - workingSize -= file.length(); + Logger.d(TAG, "Delete for trim " + file.getFullPath()); + workingSize -= file.getLength(); if (!file.delete()) { Logger.e(TAG, "Failed to delete cache file for trim"); } } + recalculateSize(); } } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/FileCache.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/FileCache.java index a73adefe2b..c8882fbab9 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/FileCache.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/FileCache.java @@ -22,10 +22,13 @@ import com.github.adamantcheese.chan.core.manager.ThreadSaveManager; import com.github.adamantcheese.chan.core.model.PostImage; import com.github.adamantcheese.chan.core.model.orm.Loadable; -import com.github.adamantcheese.chan.core.settings.ChanSettings; +import com.github.adamantcheese.chan.core.saf.FileManager; +import com.github.adamantcheese.chan.core.saf.file.AbstractFile; +import com.github.adamantcheese.chan.core.saf.file.RawFile; import com.github.adamantcheese.chan.utils.Logger; import java.io.File; +import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutorService; @@ -33,15 +36,21 @@ public class FileCache implements FileCacheDownloader.Callback { private static final String TAG = "FileCache"; + private static final String FILE_CACHE_DIR = "filecache"; private final ExecutorService downloadPool = Executors.newCachedThreadPool(); - private final CacheHandler cacheHandler; + private final FileManager fileManager; private List downloaders = new ArrayList<>(); - public FileCache(File directory) { - cacheHandler = new CacheHandler(directory); + public FileCache(File cacheDir, FileManager fileManager) { + this.fileManager = fileManager; + + RawFile cacheDirFile = fileManager.fromRawFile( + new File(cacheDir, FILE_CACHE_DIR)); + + cacheHandler = new CacheHandler(cacheDirFile); } public void clearCache() { @@ -59,11 +68,25 @@ public FileCacheDownloader downloadFile( FileCacheListener listener) { if (loadable.isSavedCopy) { String filename = ThreadSaveManager.formatOriginalImageName( - postImage.originalName, postImage.extension); + postImage.originalName, + postImage.extension); + + if (!fileManager.baseLocalThreadsDirectoryExists()) { + Logger.e(TAG, "Base local threads directory does not exist"); + return null; + } + + AbstractFile baseDirFile = fileManager.newLocalThreadFile(); + if (baseDirFile == null) { + Logger.e(TAG, "fileManager.newLocalThreadFile() returned null"); + return null; + } String imageDir = ThreadSaveManager.getImagesSubDir(loadable); - File fullImagePath = new File(ChanSettings.saveLocation.get(), imageDir); - File imageOnDiskFile = new File(fullImagePath, filename); + + AbstractFile imageOnDiskFile = baseDirFile + .appendSubDirSegment(imageDir) + .appendFileNameSegment(filename); if (imageOnDiskFile.exists() && imageOnDiskFile.isFile() @@ -71,7 +94,7 @@ public FileCacheDownloader downloadFile( handleFileImmediatelyAvailable(listener, imageOnDiskFile); } else { Logger.e(TAG, "Cannot load saved image from the disk, path: " - + imageOnDiskFile.getAbsolutePath()); + + imageOnDiskFile.getFullPath()); if (listener != null) { listener.onFail(true); @@ -106,7 +129,7 @@ public FileCacheDownloader downloadFile(@NonNull String url, FileCacheListener l return runningDownloaderForKey; } - File file = get(url); + RawFile file = get(url); if (file.exists()) { handleFileImmediatelyAvailable(listener, file); return null; @@ -138,7 +161,7 @@ public boolean exists(String key) { return cacheHandler.exists(key); } - public File get(String key) { + public RawFile get(String key) { return cacheHandler.get(key); } @@ -146,19 +169,40 @@ public long getFileCacheSize() { return cacheHandler.getSize().get(); } - private void handleFileImmediatelyAvailable(FileCacheListener listener, File file) { + private void handleFileImmediatelyAvailable(FileCacheListener listener, AbstractFile file) { // TODO: setLastModified doesn't seem to work on Android... - if (!file.setLastModified(System.currentTimeMillis())) { - Logger.e(TAG, "Could not set last modified time on file"); - } +// if (!file.setLastModified(System.currentTimeMillis())) { +// Logger.e(TAG, "Could not set last modified time on file"); +// } + if (listener != null) { - listener.onSuccess(file); + if (file instanceof RawFile) { + listener.onSuccess((RawFile) file); + } else { + try { + RawFile resultFile = fileManager.fromRawFile(cacheHandler.randomCacheFile()); + if (!fileManager.copyFileContents(file, resultFile)) { + throw new IOException("Could not copy external SAF file into internal " + + "cache file, externalFile = " + file.getFullPath() + + ", resultFile = " + resultFile.getFullPath()); + } + + listener.onSuccess(resultFile); + } catch (IOException e) { + Logger.e(TAG, "Error while trying to create a new random cache file", e); + listener.onFail(false); + } + } + listener.onEnd(); } } private FileCacheDownloader handleStartDownload( - FileCacheListener listener, File file, String url) { + FileCacheListener listener, + RawFile file, + String url + ) { FileCacheDownloader downloader = new FileCacheDownloader(this, url, file); if (listener != null) { downloader.addListener(listener); diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/FileCacheDownloader.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/FileCacheDownloader.java index 5a583a4fdd..02adf71007 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/FileCacheDownloader.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/FileCacheDownloader.java @@ -25,11 +25,12 @@ import com.github.adamantcheese.chan.Chan; import com.github.adamantcheese.chan.core.di.NetModule; +import com.github.adamantcheese.chan.core.saf.file.RawFile; import com.github.adamantcheese.chan.utils.Logger; import java.io.Closeable; -import java.io.File; import java.io.IOException; +import java.io.OutputStream; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; @@ -51,7 +52,7 @@ public class FileCacheDownloader implements Runnable { private static final long NOTIFY_SIZE = BUFFER_SIZE * 8; private final String url; - private final File output; + private final RawFile output; private final Handler handler; // Main thread only. @@ -66,7 +67,7 @@ public class FileCacheDownloader implements Runnable { private Call call; private ResponseBody body; - public FileCacheDownloader(Callback callback, String url, File output) { + public FileCacheDownloader(Callback callback, String url, RawFile output) { this.callback = callback; this.url = url; this.output = output; @@ -123,6 +124,7 @@ public void run() { private void execute() { Closeable sourceCloseable = null; Closeable sinkCloseable = null; + OutputStream outputFileOutputStream = null; try { checkCancel(); @@ -132,7 +134,12 @@ private void execute() { Source source = body.source(); sourceCloseable = source; - BufferedSink sink = Okio.buffer(Okio.sink(output)); + outputFileOutputStream = output.getOutputStream(); + if (outputFileOutputStream == null) { + throw new IOException("Couldn't get output file's OutputStream"); + } + + BufferedSink sink = Okio.buffer(Okio.sink(outputFileOutputStream)); sinkCloseable = sink; checkCancel(); @@ -143,7 +150,7 @@ private void execute() { log("done"); - long fileLen = output.length(); + long fileLen = output.getLength(); handler.post(() -> { if (callback != null) { @@ -190,6 +197,7 @@ private void execute() { } finally { Util.closeQuietly(sourceCloseable); Util.closeQuietly(sinkCloseable); + Util.closeQuietly(outputFileOutputStream); if (call != null) { call.cancel(); diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/FileCacheListener.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/FileCacheListener.java index ccf2f04a60..6584343bb0 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/FileCacheListener.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/FileCacheListener.java @@ -16,7 +16,7 @@ */ package com.github.adamantcheese.chan.core.cache; -import java.io.File; +import com.github.adamantcheese.chan.core.saf.file.RawFile; public abstract class FileCacheListener { public void onProgress(long downloaded, long total) { @@ -25,7 +25,7 @@ public void onProgress(long downloaded, long total) { /** * Called when the file download was completed. */ - public void onSuccess(File file) { + public void onSuccess(RawFile file) { } /** diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/database/DatabaseSavedThreadManager.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/database/DatabaseSavedThreadManager.java index cc03a38b8e..13110d99b9 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/database/DatabaseSavedThreadManager.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/database/DatabaseSavedThreadManager.java @@ -1,10 +1,15 @@ package com.github.adamantcheese.chan.core.database; +import android.net.Uri; + import com.github.adamantcheese.chan.core.manager.ThreadSaveManager; import com.github.adamantcheese.chan.core.model.orm.Loadable; import com.github.adamantcheese.chan.core.model.orm.SavedThread; +import com.github.adamantcheese.chan.core.saf.FileManager; +import com.github.adamantcheese.chan.core.saf.file.AbstractFile; import com.github.adamantcheese.chan.core.settings.ChanSettings; import com.github.adamantcheese.chan.utils.IOUtils; +import com.github.adamantcheese.chan.utils.Logger; import com.j256.ormlite.stmt.DeleteBuilder; import java.io.File; @@ -16,8 +21,12 @@ import static com.github.adamantcheese.chan.Chan.inject; public class DatabaseSavedThreadManager { + private static final String TAG = "DatabaseSavedThreadManager"; + @Inject DatabaseHelper helper; + @Inject + FileManager fileManager; public DatabaseSavedThreadManager() { inject(this); @@ -155,16 +164,43 @@ public Callable deleteSavedThread(Loadable loadable) { db.where().eq(SavedThread.LOADABLE_ID, loadable.id); db.delete(); + deleteThreadFromDisk(loadable, ChanSettings.isLocalThreadsDirUsesSAF()); + return null; + }; + } + + public void deleteThreadFromDisk(Loadable loadable, boolean usesSAF) { + if (usesSAF) { + String threadSubDir = ThreadSaveManager.getThreadSubDir(loadable); + Uri uri = Uri.parse(ChanSettings.localThreadsLocationUri.get()); + + AbstractFile localThreadsDir = fileManager.fromUri(uri); + if (localThreadsDir == null || !localThreadsDir.exists() || !localThreadsDir.isDirectory()) { + // Probably already deleted + return; + } + + AbstractFile threadDir = localThreadsDir.appendSubDirSegment(threadSubDir); + if (!threadDir.exists() || !threadDir.isDirectory()) { + // Probably already deleted + return; + } + + if (!threadDir.delete()) { + Logger.d(TAG, "deleteThreadFromDisk() Could not delete SAF directory " + + threadDir.getFullPath()); + } + } else { String threadSubDir = ThreadSaveManager.getThreadSubDir(loadable); - File threadSaveDir = new File(ChanSettings.saveLocation.get(), threadSubDir); + File threadSaveDir = new File(ChanSettings.localThreadLocation.get(), threadSubDir); if (!threadSaveDir.exists() || !threadSaveDir.isDirectory()) { - return null; + // Probably already deleted + return; } IOUtils.deleteDirWithContents(threadSaveDir); - return null; - }; + } } public Callable deleteSavedThreads(List loadableList) { diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/AppModule.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/AppModule.java index ee7be46f20..4731830c5c 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/AppModule.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/AppModule.java @@ -51,7 +51,8 @@ public Context provideApplicationContext() { @Singleton public ImageLoaderV2 provideImageLoaderV2(RequestQueue requestQueue, Context applicationContext, - ThemeHelper themeHelper) { + ThemeHelper themeHelper, + FileManager fileManager) { final int runtimeMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); final int lruImageCacheSize = runtimeMemory / 8; ImageLoader imageLoader = new ImageLoader( @@ -60,7 +61,7 @@ public ImageLoaderV2 provideImageLoaderV2(RequestQueue requestQueue, themeHelper, new BitmapLruImageCache(lruImageCacheSize)); - return new ImageLoaderV2(imageLoader); + return new ImageLoaderV2(imageLoader, fileManager); } @Provides diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/ManagerModule.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/ManagerModule.java index 0cbc9db390..d9de44385d 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/ManagerModule.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/ManagerModule.java @@ -139,13 +139,9 @@ public ThreadSaveManager provideSaveThreadManager( @Provides @Singleton public SavedThreadLoaderManager provideSavedThreadLoaderManager( - Gson gson, - DatabaseManager databaseManager, SavedThreadLoaderRepository savedThreadLoaderRepository, FileManager fileManager) { return new SavedThreadLoaderManager( - gson, - databaseManager, savedThreadLoaderRepository, fileManager); } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/NetModule.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/NetModule.java index 72a0f47d47..5b94eb54e9 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/NetModule.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/NetModule.java @@ -21,6 +21,7 @@ import com.github.adamantcheese.chan.BuildConfig; import com.github.adamantcheese.chan.core.cache.FileCache; import com.github.adamantcheese.chan.core.net.ProxiedHurlStack; +import com.github.adamantcheese.chan.core.saf.FileManager; import com.github.adamantcheese.chan.core.settings.ChanSettings; import com.github.adamantcheese.chan.core.site.http.HttpCallManager; @@ -51,8 +52,8 @@ public RequestQueue provideRequestQueue() { @Provides @Singleton - public FileCache provideFileCache() { - return new FileCache(new File(getCacheDir(), "filecache")); + public FileCache provideFileCache(FileManager fileManager) { + return new FileCache(getCacheDir(), fileManager); } private File getCacheDir() { diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/image/ImageLoaderV2.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/image/ImageLoaderV2.java index 9143afa801..e8709fc0e0 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/image/ImageLoaderV2.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/image/ImageLoaderV2.java @@ -10,11 +10,13 @@ import com.github.adamantcheese.chan.core.manager.ThreadSaveManager; import com.github.adamantcheese.chan.core.model.PostImage; import com.github.adamantcheese.chan.core.model.orm.Loadable; -import com.github.adamantcheese.chan.core.settings.ChanSettings; +import com.github.adamantcheese.chan.core.saf.FileManager; +import com.github.adamantcheese.chan.core.saf.file.AbstractFile; import com.github.adamantcheese.chan.utils.Logger; import com.github.adamantcheese.chan.utils.StringUtils; -import java.io.File; +import java.io.IOException; +import java.io.InputStream; import java.util.concurrent.Executor; import java.util.concurrent.Executors; @@ -22,11 +24,14 @@ public class ImageLoaderV2 { private static final String TAG = "ImageLoaderV2"; private ImageLoader imageLoader; + private FileManager fileManager; + private Executor diskLoaderExecutor = Executors.newSingleThreadExecutor(); private Handler mainThreadHandler = new Handler(Looper.getMainLooper()); - public ImageLoaderV2(ImageLoader imageLoader) { + public ImageLoaderV2(ImageLoader imageLoader, FileManager fileManager) { this.imageLoader = imageLoader; + this.fileManager = fileManager; } public ImageContainer getImage( @@ -92,63 +97,89 @@ public ImageContainer getFromDisk( ImageListener imageListener, int width, int height) { - ImageContainer container = new ImageContainer(null, null, null, imageListener); + ImageContainer container = new ImageContainer( + null, + null, + null, + imageListener); diskLoaderExecutor.execute(() -> { - String imageDir; - if (isSpoiler) { - imageDir = ThreadSaveManager.getBoardSubDir(loadable); - } else { - imageDir = ThreadSaveManager.getImagesSubDir(loadable); - } + try { + if (!fileManager.baseLocalThreadsDirectoryExists()) { + throw new IOException("Base local threads directory does not exist"); + } - File fullImagePath = new File(ChanSettings.saveLocation.get(), imageDir); - File imageOnDiskFile = new File(fullImagePath, filename); - String imageOnDisk = imageOnDiskFile.getAbsolutePath(); - - if (!imageOnDiskFile.exists() || !imageOnDiskFile.isFile() || !imageOnDiskFile.canRead()) { - String errorMessage = "Could not load image from the disk: " + - "(path = " + imageOnDiskFile.getAbsolutePath() + - ", exists = " + imageOnDiskFile.exists() + - ", isFile = " + imageOnDiskFile.isFile() + - ", canRead = " + imageOnDiskFile.canRead() + ")"; - Logger.e(TAG, errorMessage); - - mainThreadHandler.post(() -> { - if (container.getListener() != null) { - container.getListener().onErrorResponse(new VolleyError(errorMessage)); - } - }); - return; - } + AbstractFile baseDirFile = fileManager.newLocalThreadFile(); + if (baseDirFile == null) { + throw new IOException("fileManager.newLocalThreadFile() returned null"); + } - // Image exists on the disk - try to load it and put in the cache - BitmapFactory.Options bitmapOptions = new BitmapFactory.Options(); - bitmapOptions.outWidth = width; - bitmapOptions.outHeight = height; + String imageDir; + if (isSpoiler) { + imageDir = ThreadSaveManager.getBoardSubDir(loadable); + } else { + imageDir = ThreadSaveManager.getImagesSubDir(loadable); + } - Bitmap bitmap = BitmapFactory.decodeFile(imageOnDisk, bitmapOptions); - if (bitmap == null) { - Logger.e(TAG, "Could not decode bitmap"); + AbstractFile imageOnDiskFile = baseDirFile + .appendSubDirSegment(imageDir) + .appendFileNameSegment(filename); + + if (!imageOnDiskFile.exists() + || !imageOnDiskFile.isFile() + || !imageOnDiskFile.canRead()) { + String errorMessage = "Could not load image from the disk: " + + "(path = " + imageOnDiskFile.getFullPath() + + ", exists = " + imageOnDiskFile.exists() + + ", isFile = " + imageOnDiskFile.isFile() + + ", canRead = " + imageOnDiskFile.canRead() + ")"; + + Logger.e(TAG, errorMessage); + postError(container, errorMessage); + return; + } - mainThreadHandler.post(() -> { - if (container.getListener() != null) { - container.getListener().onErrorResponse(new VolleyError("Could not decode bitmap")); + try (InputStream inputStream = imageOnDiskFile.getInputStream()) { + // Image exists on the disk - try to load it and put in the cache + BitmapFactory.Options bitmapOptions = new BitmapFactory.Options(); + bitmapOptions.outWidth = width; + bitmapOptions.outHeight = height; + + Bitmap bitmap = BitmapFactory.decodeStream( + inputStream, + null, + bitmapOptions); + + if (bitmap == null) { + Logger.e(TAG, "Could not decode bitmap"); + postError(container, "Could not decode bitmap"); + return; } - }); - return; - } - mainThreadHandler.post(() -> { - container.setBitmap(bitmap); - container.setRequestUrl(imageDir); - imageListener.onResponse(container, true); - }); + mainThreadHandler.post(() -> { + container.setBitmap(bitmap); + container.setRequestUrl(imageDir); + imageListener.onResponse(container, true); + }); + } + } catch (Exception e) { + String message = "Could not get an image from the disk, error message = " + + e.getMessage(); + postError(container, message); + } }); return container; } + private void postError(ImageContainer container, String message) { + mainThreadHandler.post(() -> { + if (container.getListener() != null) { + container.getListener().onErrorResponse(new VolleyError(message)); + } + }); + } + public void cancelRequest(ImageContainer container) { imageLoader.cancelRequest(container); } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/SavedThreadLoaderManager.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/SavedThreadLoaderManager.java deleted file mode 100644 index f30d492271..0000000000 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/SavedThreadLoaderManager.java +++ /dev/null @@ -1,108 +0,0 @@ -package com.github.adamantcheese.chan.core.manager; - -import android.net.Uri; - -import androidx.annotation.Nullable; - -import com.github.adamantcheese.chan.core.database.DatabaseManager; -import com.github.adamantcheese.chan.core.mapper.ThreadMapper; -import com.github.adamantcheese.chan.core.model.ChanThread; -import com.github.adamantcheese.chan.core.model.orm.Loadable; -import com.github.adamantcheese.chan.core.model.save.SerializableThread; -import com.github.adamantcheese.chan.core.repository.SavedThreadLoaderRepository; -import com.github.adamantcheese.chan.core.saf.FileManager; -import com.github.adamantcheese.chan.core.saf.file.ExternalFile; -import com.github.adamantcheese.chan.core.settings.ChanSettings; -import com.github.adamantcheese.chan.utils.BackgroundUtils; -import com.github.adamantcheese.chan.utils.Logger; -import com.google.gson.Gson; - -import java.io.File; -import java.io.IOException; - -import javax.inject.Inject; - -public class SavedThreadLoaderManager { - private final static String TAG = "SavedThreadLoaderManager"; - - private final Gson gson; - private final DatabaseManager databaseManager; - private final SavedThreadLoaderRepository savedThreadLoaderRepository; - private final FileManager fileManager; - - @Inject - public SavedThreadLoaderManager( - Gson gson, - DatabaseManager databaseManager, - SavedThreadLoaderRepository savedThreadLoaderRepository, - FileManager fileManager) { - this.gson = gson; - this.databaseManager = databaseManager; - this.savedThreadLoaderRepository = savedThreadLoaderRepository; - this.fileManager = fileManager; - } - - @Nullable - public ChanThread loadSavedThread(Loadable loadable) { - if (BackgroundUtils.isMainThread()) { - throw new RuntimeException("Cannot be executed on the main thread!"); - } - - if (ChanSettings.localThreadsLocationUri.get().isEmpty()) { - throw new IllegalStateException("Local threads location is not set!"); - } - - String threadSubDir = ThreadSaveManager.getThreadSubDir(loadable); - Uri localThreadsLocationUri = Uri.parse(ChanSettings.localThreadsLocationUri.get()); - ExternalFile threadSaveDir = fileManager.fromUri(localThreadsLocationUri) - .appendSubDirSegment(threadSubDir); - - if (!threadSaveDir.exists() || !threadSaveDir.isDirectory()) { - Logger.e(TAG, "threadSaveDir does not exist or is not a directory: " - + "(path = " + threadSaveDir.getFullPath() - + ", exists = " + threadSaveDir.exists() - + ", isDir = " + threadSaveDir.isDirectory() + ")"); - return null; - } - - ExternalFile threadFile = threadSaveDir - .clone() - .appendFileNameSegment(SavedThreadLoaderRepository.THREAD_FILE_NAME); - - if (!threadFile.exists() || !threadFile.isFile() || !threadFile.canRead()) { - Logger.e(TAG, "threadFile does not exist or not a file or cannot be read: " + - "(path = " + threadFile.getFullPath() - + ", exists = " + threadFile.exists() - + ", isFile = " + threadFile.isFile() - + ", canRead = " + threadFile.canRead() + ")"); - return null; - } - - ExternalFile threadSaveDirImages = threadSaveDir - .clone() - .appendSubDirSegment("images"); - - if (!threadSaveDirImages.exists() || !threadSaveDirImages.isDirectory()) { - Logger.e(TAG, "threadSaveDirImages does not exist or is not a directory: " - + "(path = " + threadSaveDirImages.getFullPath() - + ", exists = " + threadSaveDirImages.exists() - + ", isDir = " + threadSaveDirImages.isDirectory() + ")"); - return null; - } - - try { - SerializableThread serializableThread = savedThreadLoaderRepository - .loadOldThreadFromJsonFile(threadSaveDir); - if (serializableThread == null) { - Logger.e(TAG, "Could not load thread from json"); - return null; - } - - return ThreadMapper.fromSerializedThread(loadable, serializableThread); - } catch (IOException | SavedThreadLoaderRepository.OldThreadTakesTooMuchSpace e) { - Logger.e(TAG, "Could not load saved thread", e); - return null; - } - } - -} diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/SavedThreadLoaderManager.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/SavedThreadLoaderManager.kt new file mode 100644 index 0000000000..cba5bc377f --- /dev/null +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/SavedThreadLoaderManager.kt @@ -0,0 +1,83 @@ +package com.github.adamantcheese.chan.core.manager + +import com.github.adamantcheese.chan.core.mapper.ThreadMapper +import com.github.adamantcheese.chan.core.model.ChanThread +import com.github.adamantcheese.chan.core.model.orm.Loadable +import com.github.adamantcheese.chan.core.repository.SavedThreadLoaderRepository +import com.github.adamantcheese.chan.core.saf.FileManager +import com.github.adamantcheese.chan.utils.BackgroundUtils +import com.github.adamantcheese.chan.utils.Logger +import java.io.IOException +import javax.inject.Inject + +class SavedThreadLoaderManager @Inject +constructor( + private val savedThreadLoaderRepository: SavedThreadLoaderRepository, + private val fileManager: FileManager) { + + fun loadSavedThread(loadable: Loadable): ChanThread? { + if (BackgroundUtils.isMainThread()) { + throw RuntimeException("Cannot be executed on the main thread!") + } + + val threadSubDir = ThreadSaveManager.getThreadSubDir(loadable) + val baseDir = fileManager.newLocalThreadFile() + if (baseDir == null) { + Logger.e(TAG, "fileManager.newLocalThreadFile() returned null") + return null + } + + val threadSaveDir = baseDir.appendSubDirSegment(threadSubDir) + if (!threadSaveDir.exists() || !threadSaveDir.isDirectory()) { + Logger.e(TAG, "threadSaveDir does not exist or is not a directory: " + + "(path = " + threadSaveDir.getFullPath() + + ", exists = " + threadSaveDir.exists() + + ", isDir = " + threadSaveDir.isDirectory() + ")") + return null + } + + val threadFile = threadSaveDir + .clone() + .appendFileNameSegment(SavedThreadLoaderRepository.THREAD_FILE_NAME) + + if (!threadFile.exists() || !threadFile.isFile() || !threadFile.canRead()) { + Logger.e(TAG, "threadFile does not exist or not a file or cannot be read: " + + "(path = " + threadFile.getFullPath() + + ", exists = " + threadFile.exists() + + ", isFile = " + threadFile.isFile() + + ", canRead = " + threadFile.canRead() + ")") + return null + } + + val threadSaveDirImages = threadSaveDir + .clone() + .appendSubDirSegment("images") + + if (!threadSaveDirImages.exists() || !threadSaveDirImages.isDirectory()) { + Logger.e(TAG, "threadSaveDirImages does not exist or is not a directory: " + + "(path = " + threadSaveDirImages.getFullPath() + + ", exists = " + threadSaveDirImages.exists() + + ", isDir = " + threadSaveDirImages.isDirectory() + ")") + return null + } + + try { + val serializableThread = savedThreadLoaderRepository + .loadOldThreadFromJsonFile(threadSaveDir) + if (serializableThread == null) { + Logger.e(TAG, "Could not load thread from json") + return null + } + + return ThreadMapper.fromSerializedThread(loadable, serializableThread) + } catch (e: IOException) { + Logger.e(TAG, "Could not load saved thread", e) + return null + } + } + + companion object { + private const val TAG = "SavedThreadLoaderManager" + } + +} diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/ThreadSaveManager.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/ThreadSaveManager.java index 10e09738ab..be3e9d2f4c 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/ThreadSaveManager.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/ThreadSaveManager.java @@ -1,7 +1,6 @@ package com.github.adamantcheese.chan.core.manager; import android.annotation.SuppressLint; -import android.net.Uri; import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; @@ -15,7 +14,7 @@ import com.github.adamantcheese.chan.core.model.save.SerializableThread; import com.github.adamantcheese.chan.core.repository.SavedThreadLoaderRepository; import com.github.adamantcheese.chan.core.saf.FileManager; -import com.github.adamantcheese.chan.core.saf.file.ExternalFile; +import com.github.adamantcheese.chan.core.saf.file.AbstractFile; import com.github.adamantcheese.chan.core.settings.ChanSettings; import com.github.adamantcheese.chan.utils.BackgroundUtils; import com.github.adamantcheese.chan.utils.IOUtils; @@ -23,7 +22,6 @@ import com.github.adamantcheese.chan.utils.StringUtils; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -167,27 +165,34 @@ private void initRxWorkerQueue() { .onErrorReturnItem(false); }).subscribe( (res) -> {}, - (error) -> Logger.e(TAG, "Uncaught exception!!! workerQueue is in error state now!!! This should not happen!!!", error), + (error) -> Logger.e(TAG, "Uncaught exception!!! workerQueue is in error state now!!! " + + "This should not happen!!!", error), () -> Logger.e(TAG, "workerQueue stream has completed!!! This should not happen!!!")); } /** * Enqueues a thread's posts with all the images/webm/etc to be saved to the disk. * */ - public void enqueueThreadToSave( + public boolean enqueueThreadToSave( Loadable loadable, List postsToSave) { if (!BackgroundUtils.isMainThread()) { throw new RuntimeException("Must be executed on the main thread"); } + if (!fileManager.baseLocalThreadsDirectoryExists()) { + Logger.e(TAG, "Base local threads directory does not exist, can't start downloading"); + return false; + } + synchronized (activeDownloads) { // Check if a thread is already being downloaded if (activeDownloads.containsKey(loadable)) { if (VERBOSE_LOG) { Logger.d(TAG, "Downloader is already running for " + loadableToString(loadable)); } - return; + + return true; } } @@ -206,6 +211,7 @@ public void enqueueThreadToSave( // Enqueue the download workerQueue.onNext(loadable); + return true; } /** @@ -315,8 +321,11 @@ private Single saveThreadInternal(@NonNull Loadable loadable, List saveThreadInternal(@NonNull Loadable loadable, List saveThreadInternal(@NonNull Loadable loadable, List saveThreadInternal(@NonNull Loadable loadable, List saveThreadInternal(@NonNull Loadable loadable, List saveThreadInternal(@NonNull Loadable loadable, List newPosts) { * If a post has at least one image that has not been downloaded yet it will be * redownloaded again * */ - private List filterAndSortPosts(ExternalFile threadSaveDirImages, Loadable loadable, List inputPosts) { + private List filterAndSortPosts(AbstractFile threadSaveDirImages, Loadable loadable, List inputPosts) { // Filter out already saved posts (by lastSavedPostNo) int lastSavedPostNo = databaseManager.runTask(databaseSavedThreadManager.getLastSavedPostNo(loadable.id)); @@ -575,14 +598,14 @@ private List filterAndSortPosts(ExternalFile threadSaveDirImages, Loadable } private boolean checkWhetherAllPostImagesAreAlreadySaved( - ExternalFile threadSaveDirImages, + AbstractFile threadSaveDirImages, Post post) { for (PostImage postImage : post.images) { { String originalImageFilename = postImage.originalName + "_" + ORIGINAL_FILE_NAME + "." + postImage.extension; - ExternalFile originalImage = threadSaveDirImages + AbstractFile originalImage = threadSaveDirImages .clone() .appendFileNameSegment(originalImageFilename); @@ -619,7 +642,7 @@ private boolean checkWhetherAllPostImagesAreAlreadySaved( String thumbnailImageFilename = postImage.originalName + "_" + THUMBNAIL_FILE_NAME + "." + thumbnailExtension; - ExternalFile thumbnailImage = threadSaveDirImages + AbstractFile thumbnailImage = threadSaveDirImages .clone() .appendFileNameSegment(thumbnailImageFilename); @@ -656,7 +679,7 @@ private boolean checkWhetherAllPostImagesAreAlreadySaved( private boolean downloadSpoilerImage( Loadable loadable, - ExternalFile threadSaveDirImages, + AbstractFile threadSaveDirImages, HttpUrl spoilerImageUrl) throws IOException { // If the board uses spoiler image - download it if (loadable.board.spoilers && spoilerImageUrl != null) { @@ -670,7 +693,7 @@ private boolean downloadSpoilerImage( String spoilerImageName = SPOILER_FILE_NAME + "." + spoilerImageExtension; - ExternalFile spoilerImageFullPath = threadSaveDirImages + AbstractFile spoilerImageFullPath = threadSaveDirImages .clone() .appendFileNameSegment(spoilerImageName); if (spoilerImageFullPath.exists()) { @@ -708,7 +731,7 @@ private HttpUrl getSpoilerImageUrl(List posts) { private Flowable downloadImages( Loadable loadable, - ExternalFile threadSaveDirImages, + AbstractFile threadSaveDirImages, Post post, AtomicInteger currentImageDownloadIndex, int postsWithImagesCount, @@ -848,13 +871,13 @@ private void logThreadDownloadingProgress( } private void deleteImageCompletely( - ExternalFile threadSaveDirImages, + AbstractFile threadSaveDirImages, String filename, String extension) { Logger.d(TAG, "Deleting a file with name " + filename); boolean error = false; - ExternalFile originalFile = threadSaveDirImages + AbstractFile originalFile = threadSaveDirImages .clone() .appendFileNameSegment(filename + "_" + ORIGINAL_FILE_NAME + "." + extension); @@ -864,7 +887,7 @@ private void deleteImageCompletely( } } - ExternalFile thumbnailFile = threadSaveDirImages + AbstractFile thumbnailFile = threadSaveDirImages .clone() .appendFileNameSegment(filename + "_" + THUMBNAIL_FILE_NAME + "." + extension); @@ -883,7 +906,7 @@ private void deleteImageCompletely( * Downloads an image with it's thumbnail and stores them to the disk * */ private void downloadImageIntoFile( - ExternalFile threadSaveDirImages, + AbstractFile threadSaveDirImages, String filename, String originalExtension, String thumbnailExtension, @@ -928,7 +951,7 @@ private boolean shouldDownloadImages() { * */ private void downloadImage( Loadable loadable, - ExternalFile threadSaveDirImages, + AbstractFile threadSaveDirImages, String filename, HttpUrl imageUrl) throws IOException, ImageWasAlreadyDeletedException { if (!shouldDownloadImages()) { @@ -947,7 +970,7 @@ private void downloadImage( return; } - ExternalFile imageFile = threadSaveDirImages + AbstractFile imageFile = threadSaveDirImages .clone() .appendFileNameSegment(filename); @@ -1020,7 +1043,7 @@ private boolean isCurrentDownloadRunning(Loadable loadable) { * Writes image's bytes to a file * */ private void storeImageToFile( - ExternalFile imageFile, + AbstractFile imageFile, Response response) throws IOException { if (!imageFile.create()) { throw new IOException("Could not create a file to save an image to (path: " @@ -1029,7 +1052,7 @@ private void storeImageToFile( try (ResponseBody body = response.body()) { if (body == null) { - throw new NullPointerException("Response body is null"); + throw new IOException("Response body is null"); } if (body.contentLength() <= 0) { @@ -1097,22 +1120,16 @@ public static String formatSpoilerImageName(String extension) { } public static String getThreadSubDir(Loadable loadable) { - // saved_threads/4chan/g/11223344 - - return SAVED_THREADS_DIR_NAME + - File.separator + - loadable.site.name() + + // 4chan/g/11223344 + return loadable.site.name() + File.separator + loadable.boardCode + File.separator + loadable.no; } public static String getImagesSubDir(Loadable loadable) { - // saved_threads/4chan/g/11223344/images - - return SAVED_THREADS_DIR_NAME + - File.separator + - loadable.site.name() + + // 4chan/g/11223344/images + return loadable.site.name() + File.separator + loadable.boardCode + File.separator + loadable.no + @@ -1120,11 +1137,8 @@ public static String getImagesSubDir(Loadable loadable) { } public static String getBoardSubDir(Loadable loadable) { - // saved_threads/4chan/g - - return SAVED_THREADS_DIR_NAME + - File.separator + - loadable.site.name() + + // 4chan/g + return loadable.site.name() + File.separator + loadable.boardCode; } @@ -1132,7 +1146,9 @@ public static String getBoardSubDir(Loadable loadable) { /** * The main difference between AdditionalThreadParameters and SaveThreadParameters is that * SaveThreadParameters is getting deleted after each thread download attempt while - * AdditionalThreadParameters stay until app restart. + * AdditionalThreadParameters stay until app restart. We use them to not download 404ed images + * on each attempt (because it may block the downloading process for up to + * OKHTTP_TIMEOUT_SECONDS seconds) * */ public static class AdditionalThreadParameters { private Loadable loadable; @@ -1194,43 +1210,54 @@ public void cancel() { } } - class ImageWasAlreadyDeletedException extends Exception { + static class ImageWasAlreadyDeletedException extends Exception { public ImageWasAlreadyDeletedException(String fileName) { super("Image " + fileName + " was already deleted"); } } - class NoNewPostsToSaveException extends Exception { + static class NoNewPostsToSaveException extends Exception { public NoNewPostsToSaveException() { super("No new posts left to save after filtering"); } } - class CouldNotCreateThreadDirectoryException extends Exception { - public CouldNotCreateThreadDirectoryException(ExternalFile threadSaveDir) { + static class CouldNotCreateThreadDirectoryException extends Exception { + public CouldNotCreateThreadDirectoryException(@Nullable AbstractFile threadSaveDir) { super("Could not create a directory to save the thread " + - "to (full path: " + threadSaveDir.getFullPath() + ")"); + "to (full path: " + getPathOrNull(threadSaveDir) + ")"); + } + + @NonNull + private static String getPathOrNull(@Nullable AbstractFile boardSaveDir) { + return boardSaveDir == null ? "null" : boardSaveDir.getFullPath(); } } - class CouldNotCreateNoMediaFile extends Exception { - public CouldNotCreateNoMediaFile(ExternalFile threadSaveDirImages) { + static class CouldNotCreateNoMediaFile extends Exception { + public CouldNotCreateNoMediaFile(AbstractFile threadSaveDirImages) { super("Could not create .nomedia file in directory " + threadSaveDirImages.getFullPath()); } } - class CouldNotCreateImagesDirectoryException extends Exception { - public CouldNotCreateImagesDirectoryException(ExternalFile threadSaveDirImages) { + static class CouldNotCreateImagesDirectoryException extends Exception { + public CouldNotCreateImagesDirectoryException(AbstractFile threadSaveDirImages) { super("Could not create a directory to save the thread images" + "to (full path: " + threadSaveDirImages.getFullPath() + ")"); } } - class CouldNotCreateSpoilerImageDirectoryException extends Exception { - public CouldNotCreateSpoilerImageDirectoryException(ExternalFile boardSaveDir) { + static class CouldNotCreateSpoilerImageDirectoryException extends Exception { + public CouldNotCreateSpoilerImageDirectoryException(@Nullable AbstractFile boardSaveDir) { super("Could not create a directory to save the spoiler image " + - "to (full path: " + boardSaveDir.getFullPath() + ")"); + "to (full path: " + getPathOrNull(boardSaveDir) + ")"); + } + + @NonNull + private static String getPathOrNull(@Nullable AbstractFile boardSaveDir) { + return boardSaveDir == null ? "null" : boardSaveDir.getFullPath(); } + } public enum DownloadRequestState { diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/UpdateManager.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/UpdateManager.java index 1144c54e35..bb13dc9f9e 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/UpdateManager.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/UpdateManager.java @@ -39,6 +39,7 @@ import com.github.adamantcheese.chan.core.cache.FileCache; import com.github.adamantcheese.chan.core.cache.FileCacheListener; import com.github.adamantcheese.chan.core.net.UpdateApiRequest; +import com.github.adamantcheese.chan.core.saf.file.RawFile; import com.github.adamantcheese.chan.core.settings.ChanSettings; import com.github.adamantcheese.chan.ui.helper.RuntimePermissionsHelper; import com.github.adamantcheese.chan.utils.AndroidUtils; @@ -171,14 +172,14 @@ public void onProgress(long downloaded, long total) { } @Override - public void onSuccess(File file) { + public void onSuccess(RawFile file) { updateDownloadDialog.dismiss(); updateDownloadDialog = null; File copy = new File(Environment.getExternalStoragePublicDirectory( Environment.DIRECTORY_DOWNLOADS), getApplicationLabel() + ".apk"); try { - IOUtils.copyFile(file, copy); + IOUtils.copyFile(new File(file.getFullPath()), copy); } catch (IOException e) { Logger.e(TAG, "requestApkInstall", e); new AlertDialog.Builder(context) diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/WatchManager.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/WatchManager.java index c3a784423b..69e4058ce4 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/WatchManager.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/WatchManager.java @@ -229,14 +229,14 @@ public boolean createPin(Pin pin, boolean sendBroadcast) { return true; } - public void startSavingThread( + public boolean startSavingThread( Loadable loadable, List postsToSave) { if (!startSavingThread(loadable)) { - return; + return false; } - threadSaveManager.enqueueThreadToSave(loadable, postsToSave); + return threadSaveManager.enqueueThreadToSave(loadable, postsToSave); } public boolean startSavingThread(Loadable loadable) { diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/model/export/ExportedAppSettings.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/model/export/ExportedAppSettings.java index c3fbabc44f..723eb85760 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/model/export/ExportedAppSettings.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/model/export/ExportedAppSettings.java @@ -19,13 +19,13 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.github.adamantcheese.chan.core.repository.ImportExportRepository; import com.google.gson.annotations.SerializedName; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import static com.github.adamantcheese.chan.core.repository.ImportExportRepository.CURRENT_EXPORT_SETTINGS_VERSION; public class ExportedAppSettings { @SerializedName("exported_sites") @@ -75,7 +75,9 @@ public static ExportedAppSettings empty() { * (probably only settings) */ public boolean isEmpty() { - return exportedSites.isEmpty() && exportedBoards.isEmpty() && (settings == null || settings.isEmpty()); + return exportedSites.isEmpty() + && exportedBoards.isEmpty() + && (settings == null || settings.isEmpty()); } public List getExportedSites() { @@ -99,7 +101,7 @@ public List getExportedSavedThreads() { } public int getVersion() { - return CURRENT_EXPORT_SETTINGS_VERSION; + return ImportExportRepository.CURRENT_EXPORT_SETTINGS_VERSION; } @Nullable @@ -128,6 +130,7 @@ public void setExportedSavedThreads(List exportedSavedThrea } public void setSettings(String settings) { - throw new UnsupportedOperationException("Settings are only allowed to be set with the constructor, and must be from ChanSettings.serializeToString()."); + throw new UnsupportedOperationException("Settings are only allowed to be set with the " + + "constructor, and must be from ChanSettings.serializeToString()."); } } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/ReplyPresenter.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/ReplyPresenter.java index 20f593ec62..611942386d 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/ReplyPresenter.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/ReplyPresenter.java @@ -601,7 +601,12 @@ private void showPreview(String name, File file) { if (file.length() > maxSize && maxSize != -1) { String fileSize = getReadableFileSize(file.length(), false); String maxSizeString = getReadableFileSize(maxSize, false); - String text = getRes().getString(probablyWebm ? R.string.reply_webm_too_big : R.string.reply_file_too_big, fileSize, maxSizeString); + + int stringResId = probablyWebm + ? R.string.reply_webm_too_big + : R.string.reply_file_too_big; + + String text = getRes().getString(stringResId, fileSize, maxSizeString); callback.openPreviewMessage(true, text); } else { callback.openPreviewMessage(false, null); diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/ThreadPresenter.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/ThreadPresenter.java index f89bd072d6..c9efe36e42 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/ThreadPresenter.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/ThreadPresenter.java @@ -21,6 +21,7 @@ import android.content.Context; import android.text.TextUtils; import android.view.LayoutInflater; +import android.widget.Toast; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; @@ -46,6 +47,7 @@ import com.github.adamantcheese.chan.core.model.orm.SavedReply; import com.github.adamantcheese.chan.core.model.orm.SavedThread; import com.github.adamantcheese.chan.core.pool.ChanLoaderFactory; +import com.github.adamantcheese.chan.core.saf.FileManager; import com.github.adamantcheese.chan.core.settings.ChanSettings; import com.github.adamantcheese.chan.core.site.Site; import com.github.adamantcheese.chan.core.site.SiteActions; @@ -106,13 +108,14 @@ public class ThreadPresenter implements ChanThreadLoader.ChanLoaderCallback, private static final int POST_OPTION_EXTRA = 15; private static final int POST_OPTION_REMOVE = 16; - private ThreadPresenterCallback threadPresenterCallback; - private WatchManager watchManager; - private DatabaseManager databaseManager; - private ChanLoaderFactory chanLoaderFactory; - private PageRequestManager pageRequestManager; - private ThreadSaveManager threadSaveManager; + private final WatchManager watchManager; + private final DatabaseManager databaseManager; + private final ChanLoaderFactory chanLoaderFactory; + private final PageRequestManager pageRequestManager; + private final ThreadSaveManager threadSaveManager; + private final FileManager fileManager; + private ThreadPresenterCallback threadPresenterCallback; private Loadable loadable; private ChanThreadLoader chanLoader; private boolean searchOpen; @@ -128,12 +131,14 @@ public ThreadPresenter(WatchManager watchManager, DatabaseManager databaseManager, ChanLoaderFactory chanLoaderFactory, PageRequestManager pageRequestManager, - ThreadSaveManager threadSaveManager) { + ThreadSaveManager threadSaveManager, + FileManager fileManager) { this.watchManager = watchManager; this.databaseManager = databaseManager; this.chanLoaderFactory = chanLoaderFactory; this.pageRequestManager = pageRequestManager; this.threadSaveManager = threadSaveManager; + this.fileManager = fileManager; } public void create(ThreadPresenterCallback threadPresenterCallback) { @@ -270,6 +275,11 @@ private void startSavingThreadIfItIsNotBeingSaved(Loadable loadable) { return; } + if (!fileManager.baseLocalThreadsDirectoryExists()) { + // Base directory for local threads does not exist or was deleted + return; + } + watchManager.startSavingThread(loadable); Pin pin = watchManager.findPinByLoadableId(loadable.id); @@ -345,6 +355,11 @@ public boolean save() { return false; } + if (!fileManager.baseLocalThreadsDirectoryExists()) { + Toast.makeText(context, R.string.base_local_threads_dir_not_exists, Toast.LENGTH_LONG).show(); + return false; + } + Pin pin = watchManager.findPinByLoadableId(loadable.id); if (pin != null) { if (PinType.hasDownloadFlag(pin.pinType)) { @@ -356,19 +371,20 @@ public boolean save() { watchManager.updatePin(pin); watchManager.stopSavingThread(pin.loadable); } - } else { - saveInternal(); } - } else { - saveInternal(); + } + + if (!saveInternal()) { + watchManager.stopSavingThread(loadable); + return false; } return true; } - private void saveInternal() { + private boolean saveInternal() { if (chanLoader.getThread() == null) { - return; + return false; } Post op = chanLoader.getThread().op; @@ -384,9 +400,12 @@ private void saveInternal() { } oldPin.pinType = PinType.addDownloadNewPostsFlag(oldPin.pinType); - watchManager.updatePin(oldPin); - startSavingThreadInternal(loadable, postsToSave, oldPin); + + if (!startSavingThreadInternal(loadable, postsToSave, oldPin)) { + return false; + } + EventBus.getDefault().post(new WatchManager.PinMessages.PinChangedMessage(oldPin)); } else { // Save button is clicked and bookmark button is not yet pressed @@ -403,16 +422,21 @@ private void saveInternal() { throw new IllegalStateException("Could not find freshly created pin by loadable " + loadable); } - startSavingThreadInternal(loadable, postsToSave, newPin); + if (!startSavingThreadInternal(loadable, postsToSave, newPin)) { + return false; + } + EventBus.getDefault().post(new WatchManager.PinMessages.PinAddedMessage(newPin)); } if (!ChanSettings.watchEnabled.get() || !ChanSettings.watchBackground.get()) { threadPresenterCallback.shownBackgroundWatcherIsDisabledToast(); } + + return true; } - private void startSavingThreadInternal( + private boolean startSavingThreadInternal( Loadable loadable, List postsToSave, Pin newPin) { @@ -420,7 +444,7 @@ private void startSavingThreadInternal( throw new IllegalStateException("newPin does not have DownloadFlag: " + newPin.pinType); } - watchManager.startSavingThread(loadable, postsToSave); + return watchManager.startSavingThread(loadable, postsToSave); } public boolean isPinned() { @@ -605,7 +629,11 @@ private void storeNewPostsIfThreadIsBeingDownloaded(Loadable loadable, List. - */ -package com.github.adamantcheese.chan.core.repository; - -import android.annotation.SuppressLint; -import android.os.ParcelFileDescriptor; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.github.adamantcheese.chan.core.database.DatabaseHelper; -import com.github.adamantcheese.chan.core.database.DatabaseManager; -import com.github.adamantcheese.chan.core.model.export.ExportedAppSettings; -import com.github.adamantcheese.chan.core.model.export.ExportedBoard; -import com.github.adamantcheese.chan.core.model.export.ExportedFilter; -import com.github.adamantcheese.chan.core.model.export.ExportedLoadable; -import com.github.adamantcheese.chan.core.model.export.ExportedPin; -import com.github.adamantcheese.chan.core.model.export.ExportedPostHide; -import com.github.adamantcheese.chan.core.model.export.ExportedSavedThread; -import com.github.adamantcheese.chan.core.model.export.ExportedSite; -import com.github.adamantcheese.chan.core.model.orm.Board; -import com.github.adamantcheese.chan.core.model.orm.Filter; -import com.github.adamantcheese.chan.core.model.orm.Loadable; -import com.github.adamantcheese.chan.core.model.orm.Pin; -import com.github.adamantcheese.chan.core.model.orm.PostHide; -import com.github.adamantcheese.chan.core.model.orm.SavedThread; -import com.github.adamantcheese.chan.core.model.orm.SiteModel; -import com.github.adamantcheese.chan.core.saf.file.ExternalFile; -import com.github.adamantcheese.chan.core.settings.ChanSettings; -import com.github.adamantcheese.chan.utils.Logger; -import com.google.gson.Gson; -import com.j256.ormlite.support.ConnectionSource; - -import java.io.FileDescriptor; -import java.io.FileReader; -import java.io.FileWriter; -import java.io.IOException; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import javax.inject.Inject; - -public class ImportExportRepository { - private static final String TAG = "ImportExportRepository"; - - // Don't forget to change this when changing any of the Export models. - // Also, don't forget to handle the change in the onUpgrade or onDowngrade methods - public static final int CURRENT_EXPORT_SETTINGS_VERSION = 3; - - private final DatabaseManager databaseManager; - private final DatabaseHelper databaseHelper; - private final Gson gson; - - @Inject - public ImportExportRepository( - DatabaseManager databaseManager, - DatabaseHelper databaseHelper, - Gson gson - ) { - this.databaseManager = databaseManager; - this.databaseHelper = databaseHelper; - this.gson = gson; - } - - public void exportTo(ExternalFile settingsFile, boolean isNewFile, ImportExportCallbacks callbacks) { - databaseManager.runTask(() -> { - try { - ExportedAppSettings appSettings = readSettingsFromDatabase(); - if (appSettings.isEmpty()) { - callbacks.onNothingToImportExport(ImportExport.Export); - return null; - } - - String json = gson.toJson(appSettings); - - if (!settingsFile.exists() || !settingsFile.canWrite()) { - throw new IOException( - "Something wrong with export file (Can't write or it doesn't exist) " - + settingsFile.getFullPath() - ); - } - - // If the user has opened an old settings file we need to use WriteTruncate mode - // so that there no leftovers of the old file after writing the settings. - // Otherwise use Write mode - ExternalFile.FileDescriptorMode fdm = ExternalFile.FileDescriptorMode.WriteTruncate; - if (isNewFile) { - fdm = ExternalFile.FileDescriptorMode.Write; - } - - try (ParcelFileDescriptor parcelFileDescriptor = settingsFile.getParcelFileDescriptor(fdm)) { - - if (parcelFileDescriptor == null) { - IllegalStateException exception = new IllegalStateException( - "parcelFileDescriptor is null, path = " + settingsFile.getFullPath()); - - callbacks.onError( - exception, - ImportExport.Export); - return null; - } - - FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor(); - if (fileDescriptor == null) { - IllegalStateException exception = new IllegalStateException( - "fileDescriptor is null, path = " + settingsFile.getFullPath()); - - callbacks.onError( - exception, - ImportExport.Export); - return null; - } - - try (FileWriter writer = new FileWriter(fileDescriptor)) { - writer.write(json); - writer.flush(); - } - - Logger.d(TAG, "Exporting done!"); - callbacks.onSuccess(ImportExport.Export); - } - } catch (Throwable error) { - Logger.e(TAG, "Error while trying to export settings", error); - - callbacks.onError(error, ImportExport.Export); - } - - return null; - }); - } - - public void importFrom(ExternalFile settingsFile, ImportExportCallbacks callbacks) { - databaseManager.runTask(() -> { - try { - if (!settingsFile.exists()) { - Logger.i(TAG, "There is nothing to import, importFile does not exist " - + settingsFile.getFullPath()); - callbacks.onNothingToImportExport(ImportExport.Import); - return null; - } - - if (!settingsFile.canRead()) { - throw new IOException( - "Something wrong with import file (Can't read or it doesn't exist) " - + settingsFile.getFullPath() - ); - } - - try (ParcelFileDescriptor parcelFileDescriptor = settingsFile.getParcelFileDescriptor( - ExternalFile.FileDescriptorMode.Read)) { - - if (parcelFileDescriptor == null) { - IllegalStateException exception = new IllegalStateException( - "parcelFileDescriptor is null, path = " + settingsFile.getFullPath()); - - callbacks.onError( - exception, - ImportExport.Import); - return null; - } - - FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor(); - if (fileDescriptor == null) { - IllegalStateException exception = new IllegalStateException( - "fileDescriptor is null, path = " + settingsFile.getFullPath()); - - callbacks.onError( - exception, - ImportExport.Import); - return null; - } - - ExportedAppSettings appSettings; - - try (FileReader reader = new FileReader(fileDescriptor)) { - appSettings = gson.fromJson(reader, ExportedAppSettings.class); - } - - if (appSettings.isEmpty()) { - Logger.i(TAG, "There is nothing to import, appSettings is empty"); - callbacks.onNothingToImportExport(ImportExport.Import); - return null; - } - - writeSettingsToDatabase(appSettings); - - Logger.d(TAG, "Importing done!"); - callbacks.onSuccess(ImportExport.Import); - - } - } catch (Throwable error) { - Logger.e(TAG, "Error while trying to import settings", error); - callbacks.onError(error, ImportExport.Import); - } - - return null; - }); - } - - private void writeSettingsToDatabase(@NonNull ExportedAppSettings appSettingsParam) - throws SQLException, IOException, DowngradeNotSupportedException { - ExportedAppSettings appSettings = appSettingsParam; - - if (appSettings.getVersion() < CURRENT_EXPORT_SETTINGS_VERSION) { - appSettings = onUpgrade(appSettings.getVersion(), appSettings); - } else if (appSettings.getVersion() > CURRENT_EXPORT_SETTINGS_VERSION) { - // we don't support settings downgrade so just notify the user about it - throw new DowngradeNotSupportedException("You are attempting to import settings with " + - "version higher than the current app's settings version (downgrade). " + - "This is not supported so nothing will be imported." - ); - } - - // recreate tables from scratch, because we need to reset database IDs as well - try (ConnectionSource cs = databaseHelper.getConnectionSource()) { - databaseHelper.dropTables(cs); - databaseHelper.createTables(cs); - } - - for (ExportedBoard exportedBoard : appSettings.getExportedBoards()) { - assert exportedBoard.getDescription() != null; - databaseHelper.boardsDao.createIfNotExists(new Board( - exportedBoard.getSiteId(), - exportedBoard.isSaved(), - exportedBoard.getOrder(), - exportedBoard.getName(), - exportedBoard.getCode(), - exportedBoard.isWorkSafe(), - exportedBoard.getPerPage(), - exportedBoard.getPages(), - exportedBoard.getMaxFileSize(), - exportedBoard.getMaxWebmSize(), - exportedBoard.getMaxCommentChars(), - exportedBoard.getBumpLimit(), - exportedBoard.getImageLimit(), - exportedBoard.getCooldownThreads(), - exportedBoard.getCooldownReplies(), - exportedBoard.getCooldownImages(), - exportedBoard.isSpoilers(), - exportedBoard.getCustomSpoilers(), - exportedBoard.isUserIds(), - exportedBoard.isCodeTags(), - exportedBoard.isPreuploadCaptcha(), - exportedBoard.isCountryFlags(), - exportedBoard.isMathTags(), - exportedBoard.getDescription(), - exportedBoard.isArchive() - )); - } - - for (ExportedSite exportedSite : appSettings.getExportedSites()) { - SiteModel inserted = databaseHelper.siteDao.createIfNotExists(new SiteModel( - exportedSite.getSiteId(), - exportedSite.getConfiguration(), - exportedSite.getUserSettings(), - exportedSite.getOrder() - )); - - List exportedSavedThreads = appSettings.getExportedSavedThreads(); - - for (ExportedPin exportedPin : exportedSite.getExportedPins()) { - ExportedLoadable exportedLoadable = exportedPin.getExportedLoadable(); - if (exportedLoadable == null) { - continue; - } - - Loadable loadable = Loadable.importLoadable( - inserted.id, - exportedLoadable.getMode(), - exportedLoadable.getBoardCode(), - exportedLoadable.getNo(), - exportedLoadable.getTitle(), - exportedLoadable.getListViewIndex(), - exportedLoadable.getListViewTop(), - exportedLoadable.getLastViewed(), - exportedLoadable.getLastLoaded() - ); - - Loadable insertedLoadable = databaseHelper.loadableDao.createIfNotExists(loadable); - ExportedSavedThread exportedSavedThread = findSavedThreadByOldLoadableId( - exportedSavedThreads, - (int) exportedLoadable.getLoadableId()); - - // ExportedSavedThreads may have their loadable ids noncontiguous. Like 1,3,4,5,21,152. - // SQLite does not like it and will be returning to us contiguous ids ignoring our ids. - // This will create a situation where savedThread.loadableId may not have a loadable. - // So we need to fix this by finding a saved thread by old loadable id and updating - // it's loadable id with the newly inserted id. - if (exportedSavedThread != null) { - exportedSavedThread.loadableId = insertedLoadable.id; - - databaseHelper.savedThreadDao.createIfNotExists(new SavedThread( - exportedSavedThread.isFullyDownloaded, - exportedSavedThread.isStopped, - exportedSavedThread.lastSavedPostNo, - exportedSavedThread.loadableId - )); - } - - Pin pin = new Pin( - insertedLoadable, - exportedPin.isWatching(), - exportedPin.getWatchLastCount(), - exportedPin.getWatchNewCount(), - exportedPin.getQuoteLastCount(), - exportedPin.getQuoteNewCount(), - exportedPin.isError(), - exportedPin.getThumbnailUrl(), - exportedPin.getOrder(), - exportedPin.isArchived(), - exportedPin.getPinType() - ); - databaseHelper.pinDao.createIfNotExists(pin); - } - } - - for (ExportedFilter exportedFilter : appSettings.getExportedFilters()) { - databaseHelper.filterDao.createIfNotExists(new Filter( - exportedFilter.isEnabled(), - exportedFilter.getType(), - exportedFilter.getPattern(), - exportedFilter.isAllBoards(), - exportedFilter.getBoards(), - exportedFilter.getAction(), - exportedFilter.getColor(), - exportedFilter.getApplyToReplies(), - exportedFilter.getOrder(), - exportedFilter.getOnlyOnOP(), - exportedFilter.getApplyToSaved() - )); - } - - for (ExportedPostHide exportedPostHide : appSettings.getExportedPostHides()) { - databaseHelper.postHideDao.createIfNotExists(new PostHide( - exportedPostHide.getSite(), - exportedPostHide.getBoard(), - exportedPostHide.getNo() - )); - } - - ChanSettings.deserializeFromString(appSettingsParam.getSettings()); - } - - @Nullable - private ExportedSavedThread findSavedThreadByOldLoadableId( - List exportedSavedThreads, - int oldLoadableId) { - for (ExportedSavedThread exportedSavedThread : exportedSavedThreads) { - if (exportedSavedThread.loadableId == oldLoadableId) { - return exportedSavedThread; - } - } - - return null; - } - - private ExportedAppSettings onUpgrade(int version, ExportedAppSettings appSettings) { - if (version < 2) { - //clear the post hides for version 1, threadNo field was added - appSettings.setExportedPostHides(new ArrayList<>()); - } - - if (version < 3) { - //clear the site model usersettings to be an empty JSON map for version 2, as they won't parse correctly otherwise - for (ExportedSite site : appSettings.getExportedSites()) { - site.setUserSettings("{}"); - } - } - return appSettings; - } - - @NonNull - private ExportedAppSettings readSettingsFromDatabase() throws java.sql.SQLException, IOException { - @SuppressLint("UseSparseArrays") - Map sitesMap = new HashMap<>(); - { - List sites = databaseHelper.siteDao.queryForAll(); - - for (SiteModel site : sites) { - sitesMap.put(site.id, site); - } - } - - @SuppressLint("UseSparseArrays") - Map loadableMap = new HashMap<>(); - { - List loadables = databaseHelper.loadableDao.queryForAll(); - - for (Loadable loadable : loadables) { - loadableMap.put(loadable.id, loadable); - } - } - - Set pins = new HashSet<>(databaseHelper.pinDao.queryForAll()); - Map> toExportMap = new HashMap<>(); - - for (SiteModel siteModel : sitesMap.values()) { - toExportMap.put(siteModel, new ArrayList<>()); - } - - for (Pin pin : pins) { - Loadable loadable = loadableMap.get(pin.loadable.id); - if (loadable == null) { - throw new NullPointerException("Could not find Loadable by pin.loadable.id " + pin.loadable.id); - } - - SiteModel siteModel = sitesMap.get(loadable.siteId); - if (siteModel == null) { - throw new NullPointerException("Could not find siteModel by loadable.siteId " + loadable.siteId); - } - - ExportedLoadable exportedLoadable = new ExportedLoadable( - loadable.boardCode, - loadable.id, - loadable.lastLoaded, - loadable.lastViewed, - loadable.listViewIndex, - loadable.listViewTop, - loadable.mode, - loadable.no, - loadable.siteId, - loadable.title - ); - - ExportedPin exportedPin = new ExportedPin( - pin.archived, - pin.id, - pin.isError, - loadable.id, - pin.order, - pin.quoteLastCount, - pin.quoteNewCount, - pin.thumbnailUrl, - pin.watchLastCount, - pin.watchNewCount, - pin.watching, - exportedLoadable, - pin.pinType - ); - - toExportMap.get(siteModel).add(exportedPin); - } - - List exportedSites = new ArrayList<>(); - - for (Map.Entry> entry : toExportMap.entrySet()) { - ExportedSite exportedSite = new ExportedSite( - entry.getKey().id, - entry.getKey().configuration, - entry.getKey().order, - entry.getKey().userSettings, - entry.getValue() - ); - - exportedSites.add(exportedSite); - } - - List exportedBoards = new ArrayList<>(); - - for (Board board : databaseHelper.boardsDao.queryForAll()) { - exportedBoards.add(new ExportedBoard( - board.siteId, - board.saved, - board.order, - board.name, - board.code, - board.workSafe, - board.perPage, - board.pages, - board.maxFileSize, - board.maxWebmSize, - board.maxCommentChars, - board.bumpLimit, - board.imageLimit, - board.cooldownThreads, - board.cooldownReplies, - board.cooldownImages, - board.spoilers, - board.customSpoilers, - board.userIds, - board.codeTags, - board.preuploadCaptcha, - board.countryFlags, - board.mathTags, - board.description, - board.archive - )); - } - - List exportedFilters = new ArrayList<>(); - - for (Filter filter : databaseHelper.filterDao.queryForAll()) { - exportedFilters.add(new ExportedFilter( - filter.enabled, - filter.type, - filter.pattern, - filter.allBoards, - filter.boards, - filter.action, - filter.color, - filter.applyToReplies, - filter.order, - filter.onlyOnOP, - filter.applyToSaved - )); - } - - List exportedPostHides = new ArrayList<>(); - - for (PostHide threadHide : databaseHelper.postHideDao.queryForAll()) { - exportedPostHides.add(new ExportedPostHide( - threadHide.site, - threadHide.board, - threadHide.no, - threadHide.wholeThread, - threadHide.hide, - threadHide.hideRepliesToThisPost, - threadHide.threadNo - )); - } - - List exportedSavedThreads = new ArrayList<>(); - - for (SavedThread savedThread : databaseHelper.savedThreadDao.queryForAll()) { - exportedSavedThreads.add(new ExportedSavedThread( - savedThread.loadableId, - savedThread.lastSavedPostNo, - savedThread.isFullyDownloaded, - savedThread.isStopped - )); - } - - String settings = ChanSettings.serializeToString(); - - return new ExportedAppSettings( - exportedSites, - exportedBoards, - exportedFilters, - exportedPostHides, - exportedSavedThreads, - settings - ); - } - - public enum ImportExport { - Import, - Export - } - - public interface ImportExportCallbacks { - void onSuccess(ImportExport importExport); - - void onNothingToImportExport(ImportExport importExport); - - void onError(Throwable error, ImportExport importExport); - } - - public static class DowngradeNotSupportedException extends Exception { - public DowngradeNotSupportedException(String message) { - super(message); - } - } -} diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/ImportExportRepository.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/ImportExportRepository.kt new file mode 100644 index 0000000000..79ed0aa534 --- /dev/null +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/ImportExportRepository.kt @@ -0,0 +1,510 @@ +/* + * Kuroba - *chan browser https://github.com/Adamantcheese/Kuroba/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.github.adamantcheese.chan.core.repository + +import android.annotation.SuppressLint +import com.github.adamantcheese.chan.core.database.DatabaseHelper +import com.github.adamantcheese.chan.core.database.DatabaseManager +import com.github.adamantcheese.chan.core.model.export.* +import com.github.adamantcheese.chan.core.model.orm.* +import com.github.adamantcheese.chan.core.saf.file.ExternalFile +import com.github.adamantcheese.chan.core.saf.file.FileDescriptorMode +import com.github.adamantcheese.chan.core.settings.ChanSettings +import com.github.adamantcheese.chan.utils.Logger +import com.google.gson.Gson +import java.io.FileReader +import java.io.FileWriter +import java.io.IOException +import java.sql.SQLException +import java.util.* +import javax.inject.Inject + +class ImportExportRepository @Inject +constructor( + private val databaseManager: DatabaseManager, + private val databaseHelper: DatabaseHelper, + private val gson: Gson +) { + + fun exportTo(settingsFile: ExternalFile, isNewFile: Boolean, callbacks: ImportExportCallbacks) { + databaseManager.runTask { + try { + val appSettings = readSettingsFromDatabase() + if (appSettings.isEmpty) { + callbacks.onNothingToImportExport(ImportExport.Export) + return@runTask + } + + val json = gson.toJson(appSettings) + + if (!settingsFile.exists() || !settingsFile.canWrite()) { + throw IOException( + "Something wrong with export file (Can't write or it doesn't exist) " + + settingsFile.getFullPath() + ) + } + + // If the user has opened an old settings file we need to use WriteTruncate mode + // so that there no leftovers of the old file after writing the settings. + // Otherwise use Write mode + var fdm = FileDescriptorMode.WriteTruncate + if (isNewFile) { + fdm = FileDescriptorMode.Write + } + + val result = settingsFile.withFileDescriptor(fdm) { fileDescriptor -> + FileWriter(fileDescriptor).use { writer -> + writer.write(json) + writer.flush() + } + + Logger.d(TAG, "Exporting done!") + callbacks.onSuccess(ImportExport.Export) + } + + if (result.isFailure) { + throw result.exceptionOrNull()!! + } + + } catch (error: Throwable) { + Logger.e(TAG, "Error while trying to export settings", error) + + callbacks.onError(error, ImportExport.Export) + } + } + } + + fun importFrom(settingsFile: ExternalFile, callbacks: ImportExportCallbacks) { + databaseManager.runTask { + try { + if (!settingsFile.exists()) { + Logger.i(TAG, "There is nothing to import, importFile does not exist " + + settingsFile.getFullPath()) + callbacks.onNothingToImportExport(ImportExport.Import) + return@runTask + } + + if (!settingsFile.canRead()) { + throw IOException( + "Something wrong with import file (Can't read or it doesn't exist) " + + settingsFile.getFullPath() + ) + } + + val result = settingsFile.withFileDescriptor(FileDescriptorMode.Read) { fileDescriptor -> + FileReader(fileDescriptor).use { reader -> + val appSettings = gson.fromJson(reader, ExportedAppSettings::class.java) + + if (appSettings.isEmpty) { + Logger.i(TAG, "There is nothing to import, appSettings is empty") + callbacks.onNothingToImportExport(ImportExport.Import) + return@use + } + + writeSettingsToDatabase(appSettings) + + Logger.d(TAG, "Importing done!") + callbacks.onSuccess(ImportExport.Import) + } + } + + if (result.isFailure) { + throw result.exceptionOrNull()!! + } + + } catch (error: Throwable) { + Logger.e(TAG, "Error while trying to import settings", error) + callbacks.onError(error, ImportExport.Import) + } + } + } + + @Throws(SQLException::class, IOException::class, DowngradeNotSupportedException::class) + private fun writeSettingsToDatabase(appSettingsParam: ExportedAppSettings) { + var appSettings = appSettingsParam + + if (appSettings.version < CURRENT_EXPORT_SETTINGS_VERSION) { + appSettings = onUpgrade(appSettings.version, appSettings) + } else if (appSettings.version > CURRENT_EXPORT_SETTINGS_VERSION) { + // we don't support settings downgrade so just notify the user about it + throw DowngradeNotSupportedException("You are attempting to import settings with " + + "version higher than the current app's settings version (downgrade). " + + "This is not supported so nothing will be imported." + ) + } + + // recreate tables from scratch, because we need to reset database IDs as well + databaseHelper.connectionSource.use { cs -> + databaseHelper.dropTables(cs) + databaseHelper.createTables(cs) + } + + for (exportedBoard in appSettings.exportedBoards) { + assert(exportedBoard.description != null) + databaseHelper.boardsDao.createIfNotExists(Board( + exportedBoard.siteId, + exportedBoard.isSaved, + exportedBoard.order, + exportedBoard.name, + exportedBoard.code, + exportedBoard.isWorkSafe, + exportedBoard.perPage, + exportedBoard.pages, + exportedBoard.maxFileSize, + exportedBoard.maxWebmSize, + exportedBoard.maxCommentChars, + exportedBoard.bumpLimit, + exportedBoard.imageLimit, + exportedBoard.cooldownThreads, + exportedBoard.cooldownReplies, + exportedBoard.cooldownImages, + exportedBoard.isSpoilers, + exportedBoard.customSpoilers, + exportedBoard.isUserIds, + exportedBoard.isCodeTags, + exportedBoard.isPreuploadCaptcha, + exportedBoard.isCountryFlags, + exportedBoard.isMathTags, + exportedBoard.description ?: "", + exportedBoard.isArchive + )) + } + + for (exportedSite in appSettings.exportedSites) { + val inserted = databaseHelper.siteDao.createIfNotExists(SiteModel( + exportedSite.siteId, + exportedSite.configuration, + exportedSite.userSettings, + exportedSite.order + )) + + val exportedSavedThreads = appSettings.exportedSavedThreads + + for (exportedPin in exportedSite.exportedPins) { + val exportedLoadable = exportedPin.exportedLoadable + if (exportedLoadable == null) { + continue + } + + val loadable = Loadable.importLoadable( + inserted.id, + exportedLoadable.mode, + exportedLoadable.boardCode, + exportedLoadable.no, + exportedLoadable.title, + exportedLoadable.listViewIndex, + exportedLoadable.listViewTop, + exportedLoadable.lastViewed, + exportedLoadable.lastLoaded + ) + + val insertedLoadable = databaseHelper.loadableDao.createIfNotExists(loadable) + val exportedSavedThread = findSavedThreadByOldLoadableId( + exportedSavedThreads, + exportedLoadable.loadableId.toInt()) + + // ExportedSavedThreads may have their loadable ids noncontiguous. Like 1,3,4,5,21,152. + // SQLite does not like it and will be returning to us contiguous ids ignoring our ids. + // This will create a situation where savedThread.loadableId may not have a loadable. + // So we need to fix this by finding a saved thread by old loadable id and updating + // it's loadable id with the newly inserted id. + if (exportedSavedThread != null) { + exportedSavedThread.loadableId = insertedLoadable.id + + databaseHelper.savedThreadDao.createIfNotExists(SavedThread( + exportedSavedThread.isFullyDownloaded, + exportedSavedThread.isStopped, + exportedSavedThread.lastSavedPostNo, + exportedSavedThread.loadableId + )) + } + + val pin = Pin( + insertedLoadable, + exportedPin.isWatching, + exportedPin.watchLastCount, + exportedPin.watchNewCount, + exportedPin.quoteLastCount, + exportedPin.quoteNewCount, + exportedPin.isError, + exportedPin.thumbnailUrl, + exportedPin.order, + exportedPin.isArchived, + exportedPin.pinType + ) + databaseHelper.pinDao.createIfNotExists(pin) + } + } + + for (exportedFilter in appSettings.exportedFilters) { + databaseHelper.filterDao.createIfNotExists(Filter( + exportedFilter.isEnabled, + exportedFilter.type, + exportedFilter.pattern, + exportedFilter.isAllBoards, + exportedFilter.boards, + exportedFilter.action, + exportedFilter.color, + exportedFilter.applyToReplies, + exportedFilter.order, + exportedFilter.onlyOnOP, + exportedFilter.applyToSaved + )) + } + + for (exportedPostHide in appSettings.exportedPostHides) { + databaseHelper.postHideDao.createIfNotExists(PostHide( + exportedPostHide.site, + exportedPostHide.board, + exportedPostHide.no)) + } + + ChanSettings.deserializeFromString(appSettingsParam.settings) + } + + private fun findSavedThreadByOldLoadableId( + exportedSavedThreads: List, + oldLoadableId: Int): ExportedSavedThread? { + for (exportedSavedThread in exportedSavedThreads) { + if (exportedSavedThread.loadableId == oldLoadableId) { + return exportedSavedThread + } + } + + return null + } + + private fun onUpgrade(version: Int, appSettings: ExportedAppSettings): ExportedAppSettings { + if (version < 2) { + //clear the post hides for version 1, threadNo field was added + appSettings.exportedPostHides = ArrayList() + } + + if (version < 3) { + //clear the site model usersettings to be an empty JSON map for version 2, + // as they won't parse correctly otherwise + for (site in appSettings.exportedSites) { + site.userSettings = "{}" + } + } + return appSettings + } + + @Throws(java.sql.SQLException::class, IOException::class) + private fun readSettingsFromDatabase(): ExportedAppSettings { + @SuppressLint("UseSparseArrays") + val sitesMap = fillSitesMap() + + @SuppressLint("UseSparseArrays") + val loadableMap = fillLoadablesMap() + + val pins = HashSet(databaseHelper.pinDao.queryForAll()) + val toExportMap = HashMap>() + + for (siteModel in sitesMap.values) { + toExportMap[siteModel] = ArrayList() + } + + for (pin in pins) { + val loadable = loadableMap[pin.loadable.id] + if (loadable == null) { + throw NullPointerException("Could not find Loadable by pin.loadable.id " + + pin.loadable.id) + } + + val siteModel = sitesMap[loadable.siteId] + if (siteModel == null) { + throw NullPointerException("Could not find siteModel by loadable.siteId " + + loadable.siteId) + } + + val exportedLoadable = ExportedLoadable( + loadable.boardCode, + loadable.id.toLong(), + loadable.lastLoaded, + loadable.lastViewed, + loadable.listViewIndex, + loadable.listViewTop, + loadable.mode, + loadable.no, + loadable.siteId, + loadable.title + ) + + val exportedPin = ExportedPin( + pin.archived, + pin.id, + pin.isError, + loadable.id, + pin.order, + pin.quoteLastCount, + pin.quoteNewCount, + pin.thumbnailUrl, + pin.watchLastCount, + pin.watchNewCount, + pin.watching, + exportedLoadable, + pin.pinType + ) + + toExportMap[siteModel]!!.add(exportedPin) + } + + val exportedSites = ArrayList() + + for ((key, value) in toExportMap) { + val exportedSite = ExportedSite( + key.id, + key.configuration, + key.order, + key.userSettings, + value + ) + + exportedSites.add(exportedSite) + } + + val exportedBoards = ArrayList() + + for (board in databaseHelper.boardsDao.queryForAll()) { + exportedBoards.add(ExportedBoard( + board.siteId, + board.saved, + board.order, + board.name, + board.code, + board.workSafe, + board.perPage, + board.pages, + board.maxFileSize, + board.maxWebmSize, + board.maxCommentChars, + board.bumpLimit, + board.imageLimit, + board.cooldownThreads, + board.cooldownReplies, + board.cooldownImages, + board.spoilers, + board.customSpoilers, + board.userIds, + board.codeTags, + board.preuploadCaptcha, + board.countryFlags, + board.mathTags, + board.description, + board.archive + )) + } + + val exportedFilters = ArrayList() + + for (filter in databaseHelper.filterDao.queryForAll()) { + exportedFilters.add(ExportedFilter( + filter.enabled, + filter.type, + filter.pattern, + filter.allBoards, + filter.boards, + filter.action, + filter.color, + filter.applyToReplies, + filter.order, + filter.onlyOnOP, + filter.applyToSaved + )) + } + + val exportedPostHides = ArrayList() + + for (threadHide in databaseHelper.postHideDao.queryForAll()) { + exportedPostHides.add(ExportedPostHide( + threadHide.site, + threadHide.board, + threadHide.no, + threadHide.wholeThread, + threadHide.hide, + threadHide.hideRepliesToThisPost, + threadHide.threadNo + )) + } + + val exportedSavedThreads = ArrayList() + + for (savedThread in databaseHelper.savedThreadDao.queryForAll()) { + exportedSavedThreads.add(ExportedSavedThread( + savedThread.loadableId, + savedThread.lastSavedPostNo, + savedThread.isFullyDownloaded, + savedThread.isStopped + )) + } + + val settings = ChanSettings.serializeToString() + + return ExportedAppSettings( + exportedSites, + exportedBoards, + exportedFilters, + exportedPostHides, + exportedSavedThreads, + settings + ) + } + + private fun fillLoadablesMap(): Map { + val map = hashMapOf() + val loadables = databaseHelper.loadableDao.queryForAll() + + for (loadable in loadables) { + map[loadable.id] = loadable + } + + return map + } + + private fun fillSitesMap(): Map { + val map = hashMapOf() + val sites = databaseHelper.siteDao.queryForAll() + + for (site in sites) { + map[site.id] = site + } + + return map + } + + enum class ImportExport { + Import, + Export + } + + interface ImportExportCallbacks { + fun onSuccess(importExport: ImportExport) + fun onNothingToImportExport(importExport: ImportExport) + fun onError(error: Throwable, importExport: ImportExport) + } + + class DowngradeNotSupportedException(message: String) : Exception(message) + + companion object { + private const val TAG = "ImportExportRepository" + + // Don't forget to change this when changing any of the Export models. + // Also, don't forget to handle the change in the onUpgrade or onDowngrade methods + const val CURRENT_EXPORT_SETTINGS_VERSION = 3 + } +} diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/SavedThreadLoaderRepository.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/SavedThreadLoaderRepository.java deleted file mode 100644 index 062a9ba9a2..0000000000 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/SavedThreadLoaderRepository.java +++ /dev/null @@ -1,167 +0,0 @@ -package com.github.adamantcheese.chan.core.repository; - -import android.os.ParcelFileDescriptor; - -import androidx.annotation.Nullable; - -import com.github.adamantcheese.chan.core.mapper.ThreadMapper; -import com.github.adamantcheese.chan.core.model.Post; -import com.github.adamantcheese.chan.core.model.save.SerializableThread; -import com.github.adamantcheese.chan.core.saf.file.ExternalFile; -import com.github.adamantcheese.chan.utils.BackgroundUtils; -import com.github.adamantcheese.chan.utils.Logger; -import com.google.gson.Gson; - -import java.io.FileReader; -import java.io.FileWriter; -import java.io.IOException; -import java.util.List; - -import javax.inject.Inject; - -public class SavedThreadLoaderRepository { - private static final String TAG = "SavedThreadLoaderRepository"; - private static final int MAX_THREAD_SIZE_BYTES = 50 * 1024 * 1024; // 50mb - private static final int THREAD_FILE_HEADER_SIZE = 4; - public static final String THREAD_FILE_NAME = "thread.json"; - - private Gson gson; - - /** - * It would probably be a better idea to save posts in the database but then users won't be - * able to backup them and they would be deleted after every app uninstall. This implementation - * is slower than the DB one, but at least users will have their threads even after app - * uninstall/app data clearing. - * */ - @Inject - public SavedThreadLoaderRepository(Gson gson) { - this.gson = gson; - } - - @Nullable - public SerializableThread loadOldThreadFromJsonFile( - ExternalFile threadSaveDir) throws IOException, OldThreadTakesTooMuchSpace { - if (BackgroundUtils.isMainThread()) { - throw new RuntimeException("Cannot be executed on the main thread!"); - } - - ExternalFile threadFile = threadSaveDir - .clone() - .appendFileNameSegment(THREAD_FILE_NAME); - if (!threadFile.exists()) { - Logger.d(TAG, "threadFile does not exist, threadFilePath = " + threadFile.getFullPath()); - return null; - } - - try (ParcelFileDescriptor parcelFileDescriptor = threadFile.getParcelFileDescriptor( - ExternalFile.FileDescriptorMode.Read)) { - if (parcelFileDescriptor == null) { - Logger.d(TAG, "getParcelFileDescriptor() returned null, threadFilePath = " - + threadFile.getFullPath()); - return null; - } - - try (FileReader fileReader = new FileReader(parcelFileDescriptor.getFileDescriptor())) { - int fileLength = getThreadFileLength(fileReader); - if (fileLength <= 0 || fileLength > MAX_THREAD_SIZE_BYTES) { - throw new OldThreadTakesTooMuchSpace(fileLength); - } - - long skipped = fileReader.skip(THREAD_FILE_HEADER_SIZE); - if (skipped != THREAD_FILE_HEADER_SIZE) { - throw new IOException("Could not skip " + THREAD_FILE_HEADER_SIZE + " bytes"); - } - - return gson.fromJson(fileReader, SerializableThread.class); - } - } - } - - public void savePostsToJsonFile( - @Nullable SerializableThread oldSerializableThread, - List posts, - ExternalFile threadSaveDir - ) throws IOException, CouldNotCreateThreadFile, CouldNotGetParcelFileDescriptor { - if (BackgroundUtils.isMainThread()) { - throw new RuntimeException("Cannot be executed on the main thread!"); - } - - ExternalFile threadFile = threadSaveDir - .clone() - .appendFileNameSegment(THREAD_FILE_NAME); - - if (!threadFile.exists() && !threadFile.create()) { - throw new CouldNotCreateThreadFile(threadFile); - } - - try (ParcelFileDescriptor parcelFileDescriptor = threadFile.getParcelFileDescriptor( - ExternalFile.FileDescriptorMode.WriteTruncate)) { - if (parcelFileDescriptor == null) { - throw new CouldNotGetParcelFileDescriptor(threadFile); - } - - - // Update the thread file - try (FileWriter fileWriter = new FileWriter(parcelFileDescriptor.getFileDescriptor())) { - SerializableThread serializableThread; - - if (oldSerializableThread != null) { - // Merge with old posts if there are any - serializableThread = oldSerializableThread.merge(posts); - } else { - // Use only the new posts - serializableThread = ThreadMapper.toSerializableThread(posts); - } - - char[] threadJsonBytes = gson.toJson(serializableThread).toCharArray(); - char[] lengthChars = String.valueOf(threadJsonBytes.length).toCharArray(); - - // TODO: may not work! - fileWriter.write(lengthChars); - fileWriter.write(threadJsonBytes); - fileWriter.flush(); - } - } - } - - private int getThreadFileLength(FileReader fileReader) throws IOException { - char[] sizeBytes = new char[THREAD_FILE_HEADER_SIZE]; - int readCount = fileReader.read(sizeBytes); - - if (readCount != THREAD_FILE_HEADER_SIZE) { - throw new IOException("Could not read the length of the thread from the thread file header"); - } - - String sizeBytesString = String.valueOf(sizeBytes); - - try { - return Integer.parseInt(sizeBytesString); - } catch (NumberFormatException nfe) { - // Convert the NumberFormatException into an IOException - throw new IOException("Couldn't convert file size string into an int, sizeBytesString = " - + sizeBytesString); - } - } - - public class CouldNotGetParcelFileDescriptor extends Exception { - public CouldNotGetParcelFileDescriptor(ExternalFile threadFile) { - super("getParcelFileDescriptor() returned null, threadFilePath = " - + threadFile.getFullPath()); - } - } - - public class OldThreadTakesTooMuchSpace extends Exception { - public OldThreadTakesTooMuchSpace(int size) { - super("Old serialized thread takes way too much space: " + size + - " bytes. You are not trying to save an infinite or sticky thread, right? " + - "It's not supported."); - } - } - - public class CouldNotCreateThreadFile extends Exception { - public CouldNotCreateThreadFile(ExternalFile threadFile) { - super("Could not create the thread file " + - "(path: " + threadFile.getFullPath() + ")"); - } - } -} diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/SavedThreadLoaderRepository.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/SavedThreadLoaderRepository.kt new file mode 100644 index 0000000000..e4d6f15ad7 --- /dev/null +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/SavedThreadLoaderRepository.kt @@ -0,0 +1,107 @@ +package com.github.adamantcheese.chan.core.repository + +import com.github.adamantcheese.chan.core.mapper.ThreadMapper +import com.github.adamantcheese.chan.core.model.Post +import com.github.adamantcheese.chan.core.model.save.SerializableThread +import com.github.adamantcheese.chan.core.saf.file.AbstractFile +import com.github.adamantcheese.chan.core.saf.file.ExternalFile +import com.github.adamantcheese.chan.utils.BackgroundUtils +import com.github.adamantcheese.chan.utils.Logger +import com.google.gson.Gson +import java.io.DataInputStream +import java.io.DataOutputStream +import java.io.IOException +import java.nio.charset.StandardCharsets +import javax.inject.Inject + +class SavedThreadLoaderRepository +/** + * It would probably be a better idea to save posts in the database but then users won't be + * able to backup them and they would be deleted after every app uninstall. This implementation + * is slower than the DB one, but at least users will have their threads even after app + * uninstall/app data clearing. + */ +@Inject +constructor(private val gson: Gson) { + + @Throws(IOException::class) + fun loadOldThreadFromJsonFile( + threadSaveDir: AbstractFile): SerializableThread? { + if (BackgroundUtils.isMainThread()) { + throw RuntimeException("Cannot be executed on the main thread!") + } + + val threadFile = threadSaveDir + .clone() + .appendFileNameSegment(THREAD_FILE_NAME) + + if (!threadFile.exists()) { + Logger.d(TAG, "threadFile does not exist, threadFilePath = " + threadFile.getFullPath()) + return null + } + + return threadFile.getInputStream()?.use { inputStream -> + return@use DataInputStream(inputStream).use { dis -> + val json = String(dis.readBytes(), StandardCharsets.UTF_8) + + return@use gson.fromJson( + json, + SerializableThread::class.java) + } + } + } + + @Throws(IOException::class, + CouldNotCreateThreadFile::class, + CouldNotGetParcelFileDescriptor::class) + fun savePostsToJsonFile( + oldSerializableThread: SerializableThread?, + posts: List, + threadSaveDir: AbstractFile) { + if (BackgroundUtils.isMainThread()) { + throw RuntimeException("Cannot be executed on the main thread!") + } + + val threadFile = threadSaveDir + .clone() + .appendFileNameSegment(THREAD_FILE_NAME) + + if (!threadFile.exists() && !threadFile.create()) { + throw CouldNotCreateThreadFile(threadFile) + } + + threadFile.getOutputStream()?.use { outputStream -> + // Update the thread file + return@use DataOutputStream(outputStream).use { dos -> + val serializableThread = if (oldSerializableThread != null) { + // Merge with old posts if there are any + oldSerializableThread.merge(posts) + } else { + // Use only the new posts + ThreadMapper.toSerializableThread(posts) + } + + val bytes = gson.toJson(serializableThread) + .toByteArray(StandardCharsets.UTF_8) + + dos.write(bytes) + dos.flush() + + return@use Unit + } + } ?: throw IOException("threadFile.getOutputStream() returned null, threadFile = " + + threadFile.getFullPath()) + } + + inner class CouldNotGetParcelFileDescriptor(threadFile: ExternalFile) + : Exception("getParcelFileDescriptor() returned null, threadFilePath = " + + threadFile.getFullPath()) + + inner class CouldNotCreateThreadFile(threadFile: AbstractFile) + : Exception("Could not create the thread file (path: " + threadFile.getFullPath() + ")") + + companion object { + private const val TAG = "SavedThreadLoaderRepository" + const val THREAD_FILE_NAME = "thread.json" + } +} diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt index 299dc320ae..cc84140978 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt @@ -60,9 +60,36 @@ class FileManager( // Api to convert native file/documentFile classes into our own abstractions //======================================================= + fun baseSaveLocalDirectoryExists(): Boolean { + val baseDirFile = newSaveLocationFile() + if (baseDirFile == null) { + return false + } + + if (!baseDirFile.exists()) { + return false + } + + return true + } + + fun baseLocalThreadsDirectoryExists(): Boolean { + val baseDirFile = newLocalThreadFile() + if (baseDirFile == null) { + return false + } + + if (!baseDirFile.exists()) { + return false + } + + return true + } + /** * Create a raw file from a path. * Use this method to convert a java File by this path into an AbstractFile. + * Does not create file on the disk automatically! * */ fun fromPath(path: String): RawFile { return fromRawFile(File(path)) @@ -71,6 +98,7 @@ class FileManager( /** * Create RawFile from Java File. * Use this method to convert a java File into an AbstractFile. + * Does not create file on the disk automatically! * */ fun fromRawFile(file: File): RawFile { if (file.isFile) { @@ -83,12 +111,13 @@ class FileManager( /** * Create an external file from Uri. * Use this method to convert external file uri (file that may be located at sd card) into an - * AbstractFile. + * AbstractFile. If a file does not exist null is returned. + * Does not create file on the disk automatically! * */ - fun fromUri(uri: Uri): ExternalFile { + fun fromUri(uri: Uri): ExternalFile? { val documentFile = toDocumentFile(uri) if (documentFile == null) { - throw IllegalStateException("fromUri() toDocumentFile() returned null") + return null } return if (documentFile.isFile) { @@ -103,12 +132,44 @@ class FileManager( } } + /** + * Instantiates a new AbstractFile with the root being in the local threads directory. + * Does not create file on the disk automatically! + * */ + fun newLocalThreadFile(): AbstractFile? { + if (ChanSettings.localThreadLocation.get().isEmpty() + && ChanSettings.localThreadsLocationUri.get().isEmpty()) { + // wtf? + throw RuntimeException("Both local thread save locations are empty! " + + "Something went terribly wrong.") + } + + val uri = ChanSettings.localThreadsLocationUri.get() + if (uri.isNotEmpty()) { + // When we change localThreadsLocation we also set localThreadsLocationUri to an + // empty string, so we need to check whether the localThreadsLocationUri is empty or not, + // because saveLocation is never empty + val rootDirectory = DocumentFile.fromTreeUri(appContext, Uri.parse(uri)) + if (rootDirectory == null) { + return null + } + + return ExternalFile( + appContext, + AbstractFile.Root.DirRoot(rootDirectory)) + } + + val path = ChanSettings.localThreadLocation.get() + return RawFile(AbstractFile.Root.DirRoot(File(path))) + } + /** * Instantiates a new AbstractFile with the root being in the app's base directory (either the Kuroba * directory in case of using raw file api or the user's selected directory in case of using SAF). + * Does not create file on the disk automatically! * */ - fun newFile(): AbstractFile { - if (ChanSettings.saveLocationUri.get().isEmpty() && ChanSettings.saveLocation.get().isEmpty()) { + fun newSaveLocationFile(): AbstractFile? { + if (ChanSettings.saveLocation.get().isEmpty() && ChanSettings.saveLocationUri.get().isEmpty()) { // wtf? throw RuntimeException("Both save locations are empty! Something went terribly wrong.") } @@ -120,7 +181,7 @@ class FileManager( // empty val rootDirectory = DocumentFile.fromTreeUri(appContext, Uri.parse(uri)) if (rootDirectory == null) { - throw IllegalStateException("Root directory cannot be null!") + return null } return ExternalFile( diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt index 9c6efdaec2..e081877b2c 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt @@ -3,10 +3,7 @@ package com.github.adamantcheese.chan.core.saf.file import com.github.adamantcheese.chan.core.extension import com.github.adamantcheese.chan.core.saf.annotation.ImmutableMethod import com.github.adamantcheese.chan.core.saf.annotation.MutableMethod -import java.io.File -import java.io.FileDescriptor -import java.io.InputStream -import java.io.OutputStream +import java.io.* /** * An abstraction class over both the Java File and the new Storage Access Framework DocumentFile. @@ -88,10 +85,16 @@ import java.io.OutputStream * Sometimes you don't know which external directory to choose to store a new file (the SAF or the * old raw Java File external directory). In this case you can use: * - * AbstractFile baseDir = fileManager.newFile(); + * AbstractFile baseDir = fileManager.newSaveLocationFile(); * * Method which will create an [AbstractFile] with root pointing to either Kuroba SAF base directory - * (if user has set it) or if he didn't then to the default external directory (Backed by raw Java File). + * (if user has set it) or if he didn't then to the default external directory (Backed by raw + * Java File) or + * + * AbstractFile baseDir = fileManager.newLocalThreadFile(); + * + * Method which will create an [AbstractFile] with root pointing to either user's selected local + * threads directory or to the default external local threads directory * * */ abstract class AbstractFile( @@ -101,27 +104,29 @@ abstract class AbstractFile( protected val segments: MutableList ) { /** - * Appends a new subdirectory to the root directory + * Appends a new subdirectory (or couple of subdirectories, e.g. "dir1/dir2/dir3") + * to the root directory * */ @MutableMethod - abstract fun appendSubDirSegment(name: String): T + abstract fun appendSubDirSegment(name: String): AbstractFile /** - * Appends a file name to the root directory + * Appends a file name to the root directory (or couple subdirectories with filename at the end, + * e.g. "dir1/dir2/dir3/test.txt" * */ @MutableMethod - abstract fun appendFileNameSegment(name: String): T + abstract fun appendFileNameSegment(name: String): AbstractFile /** * Creates a new file that consists of the root directory and segments (sub dirs or the file name) * Behave similarly to Java's mkdirs() method but work not only with directories but files as well. * */ @ImmutableMethod - abstract fun createNew(): T? + abstract fun createNew(): AbstractFile? @ImmutableMethod - fun create(): Boolean { - return createNew() != null + fun create(): Boolean { + return createNew() != null } /** @@ -131,7 +136,7 @@ abstract class AbstractFile( * a couple of files in the same directory you would want to clone the directory * [AbstractFile] and then append the filename to those copies) * */ - abstract fun clone(): T + abstract fun clone(): AbstractFile @ImmutableMethod abstract fun exists(): Boolean @@ -149,7 +154,7 @@ abstract class AbstractFile( abstract fun canWrite(): Boolean @MutableMethod - abstract fun getParent(): T? + abstract fun getParent(): AbstractFile? @ImmutableMethod abstract fun getFullPath(): String @@ -167,15 +172,21 @@ abstract class AbstractFile( abstract fun getName(): String @ImmutableMethod - abstract fun findFile(fileName: String): T? + abstract fun findFile(fileName: String): AbstractFile? @ImmutableMethod abstract fun getLength(): Long @ImmutableMethod - abstract fun withFileDescriptor( + abstract fun withFileDescriptor( fileDescriptorMode: FileDescriptorMode, - func: (FileDescriptor) -> Unit) + func: (FileDescriptor) -> T): Result + + @ImmutableMethod + abstract fun listFiles(): List + + @ImmutableMethod + abstract fun lastModified(): Long /** * Removes the last appended segment if there are any @@ -191,12 +202,13 @@ abstract class AbstractFile( return true } - @Suppress("UNCHECKED_CAST") - protected fun appendSubDirSegmentInner(name: String): T { + + protected fun appendSubDirSegmentInner(name: String): AbstractFile { check(!isFilenameAppended()) { "Cannot append anything after file name has been appended" } require(!name.isBlank()) { "Bad name: $name" } - require(name.extension() == null) { "Directory name must not contain extension, " + - "extension = ${name.extension()}" } + require(name.extension() == null) { + "Directory name must not contain extension, extension = ${name.extension()}" + } val nameList = if (name.contains(File.separatorChar)) { name.split(File.separatorChar) @@ -207,17 +219,17 @@ abstract class AbstractFile( nameList .onEach { splitName -> require(splitName.extension() == null) { - "appendSubDirSegment does not allow segments with extensions! bad name = $splitName" + "appendSubDirSegment does not allow segments with extensions! " + + "bad name = $splitName" } } .map { splitName -> Segment(splitName) } .forEach { segment -> segments += segment } - return this as T + return this } - @Suppress("UNCHECKED_CAST") - protected fun appendFileNameSegmentInner(name: String): T { + protected fun appendFileNameSegmentInner(name: String): AbstractFile { check(!isFilenameAppended()) { "Cannot append anything after file name has been appended" } require(!name.isBlank()) { "Bad name: $name" } @@ -230,6 +242,8 @@ abstract class AbstractFile( listOf(name) } + require(nameList.last().extension() != null) { "Last segment must be a filename" } + for ((index, splitName) in nameList.withIndex()) { require(!(splitName.extension() != null && index != nameList.lastIndex)) { "Only the last split segment may have a file name, " + @@ -240,7 +254,7 @@ abstract class AbstractFile( segments += Segment(splitName, isFileName) } - return this as T + return this } private fun isFilenameAppended(): Boolean = segments.lastOrNull()?.isFileName ?: false @@ -298,16 +312,4 @@ abstract class AbstractFile( val name: String, val isFileName: Boolean = false ) - - enum class FileDescriptorMode(val mode: String) { - Read("r"), - Write("w"), - // When overwriting an existing file it is a really good ide to use truncate mode, - // because otherwise if a new file's length is less than the old one's then there will be - // old file's data left at the end of the file. Truncate flags will make sure that the file - // is truncated at the end to fit the new length. - WriteTruncate("wt") - - // ReadWrite and ReadWriteTruncate are not supported! - } } \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt index f1dc3d062c..ace2136864 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt @@ -19,18 +19,17 @@ class ExternalFile( ) : AbstractFile(segments) { private val mimeTypeMap = MimeTypeMap.getSingleton() - override fun appendSubDirSegment(name: String): T { + override fun appendSubDirSegment(name: String): ExternalFile { check(root !is Root.FileRoot) { "root is already FileRoot, cannot append anything anymore" } - return super.appendSubDirSegmentInner(name) + return super.appendSubDirSegmentInner(name) as ExternalFile } - override fun appendFileNameSegment(name: String): T { + override fun appendFileNameSegment(name: String): ExternalFile { check(root !is Root.FileRoot) { "root is already FileRoot, cannot append anything anymore" } - return super.appendFileNameSegmentInner(name) + return super.appendFileNameSegmentInner(name) as ExternalFile } - @Suppress("UNCHECKED_CAST") - override fun createNew(): T? { + override fun createNew(): ExternalFile? { check(root !is Root.FileRoot) { // TODO: do we need this check? "root is already FileRoot, cannot append anything anymore" @@ -39,7 +38,6 @@ class ExternalFile( if (segments.isEmpty()) { // Root is probably already exists and there is no point in creating it again so just // return null here - Logger.e(TAG, "No segments") return null } @@ -72,7 +70,7 @@ class ExternalFile( // Ignore any left segments (which we shouldn't have) after encountering fileName // segment - return ExternalFile(appContext, Root.FileRoot(newFile, segment.name)) as T + return ExternalFile(appContext, Root.FileRoot(newFile, segment.name)) } } @@ -95,26 +93,24 @@ class ExternalFile( Root.DirRoot(newFile) } - return ExternalFile(appContext, root) as T + return ExternalFile(appContext, root) } - @Suppress("UNCHECKED_CAST") - override fun clone(): T = ExternalFile( + override fun clone(): ExternalFile = ExternalFile( appContext, root.clone(), - segments.toMutableList()) as T + segments.toMutableList()) - override fun exists(): Boolean = clone().toDocumentFile()?.exists() ?: false - override fun isFile(): Boolean = clone().toDocumentFile()?.isFile ?: false - override fun isDirectory(): Boolean = clone().toDocumentFile()?.isDirectory ?: false - override fun canRead(): Boolean = clone().toDocumentFile()?.canRead() ?: false - override fun canWrite(): Boolean = clone().toDocumentFile()?.canWrite() ?: false + override fun exists(): Boolean = clone().toDocumentFile()?.exists() ?: false + override fun isFile(): Boolean = clone().toDocumentFile()?.isFile ?: false + override fun isDirectory(): Boolean = clone().toDocumentFile()?.isDirectory ?: false + override fun canRead(): Boolean = clone().toDocumentFile()?.canRead() ?: false + override fun canWrite(): Boolean = clone().toDocumentFile()?.canWrite() ?: false - @Suppress("UNCHECKED_CAST") - override fun getParent(): T? { + override fun getParent(): ExternalFile? { if (segments.isNotEmpty()) { removeLastSegment() - return this as T + return this } val parent = when (root) { @@ -127,7 +123,7 @@ class ExternalFile( return null } - return ExternalFile(appContext, Root.DirRoot(parent)) as T + return ExternalFile(appContext, Root.DirRoot(parent)) } override fun getFullPath(): String { @@ -138,12 +134,12 @@ class ExternalFile( } override fun delete(): Boolean { - return clone().toDocumentFile()?.delete() ?: false + return clone().toDocumentFile()?.delete() ?: false } override fun getInputStream(): InputStream? { val contentResolver = appContext.contentResolver - val documentFile = clone().toDocumentFile() + val documentFile = clone().toDocumentFile() if (documentFile == null) { Logger.e(TAG, "getInputStream() toDocumentFile() returned null") @@ -170,7 +166,7 @@ class ExternalFile( override fun getOutputStream(): OutputStream? { val contentResolver = appContext.contentResolver - val documentFile = clone().toDocumentFile() + val documentFile = clone().toDocumentFile() if (documentFile == null) { Logger.e(TAG, "getOutputStream() toDocumentFile() returned null") @@ -200,7 +196,7 @@ class ExternalFile( return segments.last().name } - val documentFile = clone().toDocumentFile() + val documentFile = clone().toDocumentFile() if (documentFile == null) { throw IllegalStateException("getName() toDocumentFile() returned null") } @@ -209,8 +205,7 @@ class ExternalFile( ?: throw IllegalStateException("Could not extract file name from document file") } - @Suppress("UNCHECKED_CAST") - override fun findFile(fileName: String): T? { + override fun findFile(fileName: String): ExternalFile? { check(root !is Root.FileRoot) { "Cannot use FileRoot as directory" } val filteredSegments = segments @@ -239,7 +234,7 @@ class ExternalFile( return ExternalFile( appContext, - root) as T + root) } } @@ -252,25 +247,41 @@ class ExternalFile( return ExternalFile( appContext, - root) as T + root) } // Not found return null } - override fun getLength(): Long = clone().toDocumentFile()?.length() ?: -1L + override fun getLength(): Long = clone().toDocumentFile()?.length() ?: -1L - override fun withFileDescriptor( + override fun withFileDescriptor( fileDescriptorMode: FileDescriptorMode, - func: (FileDescriptor) -> Unit) { - getParcelFileDescriptor(fileDescriptorMode)?.use { pfd -> - func(pfd.fileDescriptor) - } ?: throw IllegalStateException("Could not get ParcelFileDescriptor " + - "from root with uri = ${root.holder.uri}") + func: (FileDescriptor) -> T): Result { + return runCatching { + getParcelFileDescriptor(fileDescriptorMode)?.use { pfd -> + func(pfd.fileDescriptor) + } ?: throw IllegalStateException("Could not get ParcelFileDescriptor " + + "from root with uri = ${root.holder.uri}") + } + } + + override fun listFiles(): List { + check(root !is Root.FileRoot) { "Cannot use listFiles with FileRoot" } + + return clone() + .toDocumentFile() + ?.listFiles() + ?.map { documentFile -> ExternalFile(appContext, Root.DirRoot(documentFile)) } + ?: emptyList() + } + + override fun lastModified(): Long { + return clone().toDocumentFile()?.lastModified() ?: 0L } - fun getParcelFileDescriptor(fileDescriptorMode: FileDescriptorMode): ParcelFileDescriptor? { + private fun getParcelFileDescriptor(fileDescriptorMode: FileDescriptorMode): ParcelFileDescriptor? { return appContext.contentResolver.openFileDescriptor( root.holder.uri, fileDescriptorMode.mode) diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/FileDescriptorMode.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/FileDescriptorMode.kt new file mode 100644 index 0000000000..4259624ee8 --- /dev/null +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/FileDescriptorMode.kt @@ -0,0 +1,13 @@ +package com.github.adamantcheese.chan.core.saf.file + +enum class FileDescriptorMode(val mode: String) { + Read("r"), + Write("w"), + // When overwriting an existing file it is a really good ide to use truncate mode, + // because otherwise if a new file's length is less than the old one's then there will be + // old file's data left at the end of the file. Truncate flags will make sure that the file + // is truncated at the end to fit the new length. + WriteTruncate("wt") + + // ReadWrite and ReadWriteTruncate are not supported! +} \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt index 9a22628a58..d1ae4f63fe 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt @@ -9,18 +9,17 @@ class RawFile( segments: MutableList = mutableListOf() ) : AbstractFile(segments) { - override fun appendSubDirSegment(name: String): T { + override fun appendSubDirSegment(name: String): RawFile { check(root !is Root.FileRoot) { "root is already FileRoot, cannot append anything anymore" } - return super.appendSubDirSegmentInner(name) + return super.appendSubDirSegmentInner(name) as RawFile } - override fun appendFileNameSegment(name: String): T { + override fun appendFileNameSegment(name: String): RawFile { check(root !is Root.FileRoot) { "root is already FileRoot, cannot append anything anymore" } - return super.appendFileNameSegmentInner(name) + return super.appendFileNameSegmentInner(name) as RawFile } - @Suppress("UNCHECKED_CAST") - override fun createNew(): T? { + override fun createNew(): RawFile? { check(root !is Root.FileRoot) { // TODO: do we need this check? "root is already FileRoot, cannot append anything anymore" @@ -29,7 +28,6 @@ class RawFile( if (segments.isEmpty()) { // Root is probably already existing and there is no point in creating it again so just // return null here - Logger.e(TAG, "No segments") return null } @@ -51,32 +49,30 @@ class RawFile( } if (segment.isFileName) { - return RawFile(Root.FileRoot(newFile, segment.name)) as T + return RawFile(Root.FileRoot(newFile, segment.name)) } } - return RawFile(Root.DirRoot(newFile)) as T + return RawFile(Root.DirRoot(newFile)) } - @Suppress("UNCHECKED_CAST") - override fun clone(): T = RawFile( + override fun clone(): RawFile = RawFile( root.clone(), - segments.toMutableList()) as T + segments.toMutableList()) - override fun exists(): Boolean = clone().toFile().exists() - override fun isFile(): Boolean = clone().toFile().isFile - override fun isDirectory(): Boolean = clone().toFile().isDirectory - override fun canRead(): Boolean = clone().toFile().canRead() - override fun canWrite(): Boolean = clone().toFile().canWrite() + override fun exists(): Boolean = clone().toFile().exists() + override fun isFile(): Boolean = clone().toFile().isFile + override fun isDirectory(): Boolean = clone().toFile().isDirectory + override fun canRead(): Boolean = clone().toFile().canRead() + override fun canWrite(): Boolean = clone().toFile().canWrite() - @Suppress("UNCHECKED_CAST") - override fun getParent(): T? { + override fun getParent(): RawFile? { if (segments.isNotEmpty()) { removeLastSegment() - return this as T + return this } - return RawFile(Root.DirRoot(root.holder.parentFile)) as T + return RawFile(Root.DirRoot(root.holder.parentFile)) } override fun getFullPath(): String { @@ -86,11 +82,11 @@ class RawFile( } override fun delete(): Boolean { - return clone().toFile().delete() + return clone().toFile().delete() } override fun getInputStream(): InputStream? { - val file = clone().toFile() + val file = clone().toFile() if (!file.exists()) { Logger.e(TAG, "getInputStream() file does not exist, path = ${file.absolutePath}") @@ -111,7 +107,7 @@ class RawFile( } override fun getOutputStream(): OutputStream? { - val file = clone().toFile() + val file = clone().toFile() if (!file.exists()) { Logger.e(TAG, "getOutputStream() file does not exist, path = ${file.absolutePath}") @@ -132,11 +128,10 @@ class RawFile( } override fun getName(): String { - return clone().toFile().name + return clone().toFile().name } - @Suppress("UNCHECKED_CAST") - override fun findFile(fileName: String): T? { + override fun findFile(fileName: String): RawFile? { check(root !is Root.FileRoot) { "Cannot use FileRoot as directory" } val copy = File(root.holder.absolutePath) @@ -155,32 +150,49 @@ class RawFile( Root.DirRoot(resultFile) } - return RawFile(newRoot) as T + return RawFile(newRoot) } - override fun getLength(): Long = clone().toFile().length() - - override fun withFileDescriptor(fileDescriptorMode: FileDescriptorMode, func: (FileDescriptor) -> Unit) { - val fileCopy = clone().toFile() - - when (fileDescriptorMode) { - FileDescriptorMode.Read -> FileInputStream(fileCopy).use { fis -> func(fis.fd) } - FileDescriptorMode.Write, - FileDescriptorMode.WriteTruncate -> { - val fileOutputStream = when (fileDescriptorMode) { - FileDescriptorMode.Write -> FileOutputStream(fileCopy, false) - FileDescriptorMode.WriteTruncate -> FileOutputStream(fileCopy, true) - else -> throw NotImplementedError("Not implemented for " + - "fileDescriptorMode = ${fileDescriptorMode.name}") + override fun getLength(): Long = clone().toFile().length() + + override fun withFileDescriptor( + fileDescriptorMode: FileDescriptorMode, + func: (FileDescriptor) -> T): Result { + return runCatching { + val fileCopy = clone().toFile() + + when (fileDescriptorMode) { + FileDescriptorMode.Read -> FileInputStream(fileCopy).use { fis -> func(fis.fd) } + FileDescriptorMode.Write, + FileDescriptorMode.WriteTruncate -> { + val fileOutputStream = when (fileDescriptorMode) { + FileDescriptorMode.Write -> FileOutputStream(fileCopy, false) + FileDescriptorMode.WriteTruncate -> FileOutputStream(fileCopy, true) + else -> throw NotImplementedError("Not implemented for " + + "fileDescriptorMode = ${fileDescriptorMode.name}") + } + + fileOutputStream.use { fos -> func(fos.fd) } } - - fileOutputStream.use { fos -> func(fos.fd) } + else -> throw NotImplementedError("Not implemented for " + + "fileDescriptorMode = ${fileDescriptorMode.name}") } - else -> throw NotImplementedError("Not implemented for " + - "fileDescriptorMode = ${fileDescriptorMode.name}") } } + override fun lastModified(): Long { + return clone().toFile().lastModified() + } + + override fun listFiles(): List { + check(root !is Root.FileRoot) { "Cannot use listFiles with FileRoot" } + + return clone() + .toFile() + .listFiles() + .map { file -> RawFile(Root.DirRoot(file)) } + } + private fun toFile(): File { return if (segments.isEmpty()) { root.holder diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaveTask.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaveTask.java index dc879ed02c..7b9a82c4e1 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaveTask.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaveTask.java @@ -109,8 +109,8 @@ public void run() { } @Override - public void onSuccess(File file) { - if (copyToDestination(file)) { + public void onSuccess(RawFile file) { + if (copyToDestination(new File(file.getFullPath()))) { onDestination(); } else { deleteDestination(); diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaver.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaver.java index cf68ca9c22..da3fbf94b2 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaver.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaver.java @@ -22,6 +22,8 @@ import android.os.SystemClock; import android.widget.Toast; +import androidx.annotation.Nullable; + import com.github.adamantcheese.chan.R; import com.github.adamantcheese.chan.StartActivity; import com.github.adamantcheese.chan.core.model.PostImage; @@ -30,6 +32,7 @@ import com.github.adamantcheese.chan.core.settings.ChanSettings; import com.github.adamantcheese.chan.ui.helper.RuntimePermissionsHelper; import com.github.adamantcheese.chan.ui.service.SavingNotification; +import com.github.adamantcheese.chan.utils.Logger; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; @@ -59,12 +62,18 @@ public ImageSaver(FileManager fileManager) { EventBus.getDefault().register(this); } - public void startDownloadTask(Context context, final ImageSaveTask task) { + public boolean startDownloadTask(Context context, final ImageSaveTask task) { + AbstractFile saveLocation = getSaveLocation(task); + if (saveLocation == null) { + return false; + } + PostImage postImage = task.getPostImage(); - String name = ChanSettings.saveServerFilename.get() ? postImage.originalName : postImage.filename; + String name = ChanSettings.saveServerFilename.get() + ? postImage.originalName + : postImage.filename; String fileName = filterName(name + "." + postImage.extension); - AbstractFile saveLocation = getSaveLocation(task); AbstractFile saveFile = saveLocation .clone() .appendFileNameSegment(fileName); @@ -97,24 +106,29 @@ public void startDownloadTask(Context context, final ImageSaveTask task) { } }); } + + return true; } public boolean startBundledTask(Context context, final String subFolder, final List tasks) { if (hasPermission(context)) { - startBundledTaskInternal(subFolder, tasks); - return true; - } else { - // This does not request the permission when another request is pending. - // This is ok and will drop the tasks. - requestPermission(context, granted -> { - if (granted) { - startBundledTaskInternal(subFolder, tasks); - } else { - showToast(null, false, false); - } - }); - return false; + return startBundledTaskInternal(subFolder, tasks); } + + // This does not request the permission when another request is pending. + // This is ok and will drop the tasks. + requestPermission(context, granted -> { + if (granted) { + if (startBundledTaskInternal(subFolder, tasks)) { + return; + } + } + + showToast(null, false, false); + }); + + // TODO: uhh not sure about this one + return true; } public String getSubFolder(String name) { @@ -123,10 +137,20 @@ public String getSubFolder(String name) { return filtered; } + @Nullable public AbstractFile getSaveLocation(ImageSaveTask task) { - String subFolder = task.getSubFolder(); - AbstractFile destination = fileManager.newFile(); + if (!fileManager.baseSaveLocalDirectoryExists()) { + Logger.e(TAG, "Base save local directory does not exist"); + return null; + } + + AbstractFile destination = fileManager.newSaveLocationFile(); + if (destination == null) { + Logger.e(TAG, "getSaveLocation() fileManager.newSaveLocationFile() returned null"); + return null; + } + String subFolder = task.getSubFolder(); if (subFolder != null) { destination.appendSubDirSegment(subFolder); } @@ -159,20 +183,30 @@ private void startTask(ImageSaveTask task) { executor.execute(task); } - private void startBundledTaskInternal(String subFolder, List tasks) { + private boolean startBundledTaskInternal(String subFolder, List tasks) { + boolean allSuccess = true; + for (ImageSaveTask task : tasks) { PostImage postImage = task.getPostImage(); String fileName = filterName(postImage.originalName + "." + postImage.extension); - AbstractFile saveLocation = getSaveLocation(task) + AbstractFile saveLocation = getSaveLocation(task); + if (saveLocation == null) { + Logger.e(TAG, "getSaveLocation() returned null"); + allSuccess = false; + continue; + } + + AbstractFile destinationFile = saveLocation .appendSubDirSegment(subFolder) .appendFileNameSegment(fileName); - task.setDestination(saveLocation); + task.setDestination(destinationFile); startTask(task); } updateNotification(); + return allSuccess; } private void cancelAll() { @@ -215,9 +249,18 @@ private String getText(ImageSaveTask task, boolean success, boolean wasAlbumSave String text; if (success) { if (wasAlbumSave) { + String location; + AbstractFile locationFile = getSaveLocation(task); + + if (locationFile == null) { + location = "Unknown location"; + } else { + location = locationFile.getFullPath(); + } + text = getAppContext().getString( R.string.album_download_success, - getSaveLocation(task).getFullPath()); + location); } else { text = getAppContext().getString( R.string.image_save_as, diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/ChanSettings.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/ChanSettings.java index feb09e62c8..977fbe99c5 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/ChanSettings.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/ChanSettings.java @@ -20,7 +20,10 @@ import android.os.Environment; import android.text.TextUtils; +import androidx.annotation.NonNull; + import com.github.adamantcheese.chan.R; +import com.github.adamantcheese.chan.core.manager.ThreadSaveManager; import com.github.adamantcheese.chan.ui.adapter.PostsFilter; import com.github.adamantcheese.chan.utils.AndroidUtils; @@ -121,9 +124,9 @@ public String getKey() { public static final BooleanSetting postPinThread; public static final BooleanSetting shortPinInfo; - @Deprecated public static final StringSetting saveLocation; public static final StringSetting saveLocationUri; + public static final StringSetting localThreadLocation; public static final StringSetting localThreadsLocationUri; public static final BooleanSetting saveServerFilename; public static final BooleanSetting shareUrl; @@ -184,7 +187,6 @@ public String getKey() { public static final BooleanSetting padThumbs; public static final BooleanSetting incrementalThreadDownloadingEnabled; - public static final BooleanSetting fullUserRotationEnable; static { @@ -213,8 +215,7 @@ public String getKey() { postPinThread = new BooleanSetting(p, "preference_pin_on_post", false); shortPinInfo = new BooleanSetting(p, "preference_short_pin_info", true); - saveLocation = new StringSetting(p, "preference_image_save_location", - Environment.getExternalStorageDirectory() + File.separator + getApplicationLabel()); + saveLocation = new StringSetting(p, "preference_image_save_location", getDefaultSaveLocationDir()); saveLocation.addCallback((setting, value) -> { EventBus.getDefault().post(new SettingChanged<>(saveLocation)); }); @@ -224,6 +225,11 @@ public String getKey() { EventBus.getDefault().post(new SettingChanged<>(saveLocationUri)); })); + localThreadLocation = new StringSetting(p, "local_threads_location", getDefaultLocalThreadsLocation()); + localThreadLocation.addCallback(((setting, value) -> { + EventBus.getDefault().post(new SettingChanged<>(localThreadLocation)); + })); + localThreadsLocationUri = new StringSetting(p, "local_threads_location_uri", ""); localThreadsLocationUri.addCallback((settings, value) -> { EventBus.getDefault().post(new SettingChanged<>(localThreadsLocationUri)); @@ -302,6 +308,22 @@ public String getKey() { fullUserRotationEnable = new BooleanSetting(p, "full_user_rotation_enable", true); } + @NonNull + public static String getDefaultLocalThreadsLocation() { + return Environment.getExternalStorageDirectory() + + File.separator + + getApplicationLabel() + + File.separator + + ThreadSaveManager.SAVED_THREADS_DIR_NAME; + } + + @NonNull + public static String getDefaultSaveLocationDir() { + return Environment.getExternalStorageDirectory() + + File.separator + + getApplicationLabel(); + } + public static ThemeColor getThemeAndColor() { String themeRaw = ChanSettings.theme.get(); @@ -394,6 +416,16 @@ public static void deserializeFromString(String settings) throws IOException { } } + public static boolean isLocalThreadsDirUsesSAF() { + if (ChanSettings.localThreadsLocationUri.get().isEmpty() + && ChanSettings.localThreadLocation.get().isEmpty()) { + throw new IllegalStateException("Both localThreadsLocationUri and " + + "localThreadLocation are empty!"); + } + + return !ChanSettings.localThreadsLocationUri.get().isEmpty(); + } + public static class ThemeColor { public String theme; public String color; diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/AlbumDownloadController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/AlbumDownloadController.java index 59339312f8..7eb126639b 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/AlbumDownloadController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/AlbumDownloadController.java @@ -23,6 +23,7 @@ import android.view.ViewGroup; import android.view.animation.DecelerateInterpolator; import android.widget.ImageView; +import android.widget.Toast; import androidx.appcompat.app.AlertDialog; import androidx.core.graphics.drawable.DrawableCompat; @@ -46,6 +47,9 @@ import java.util.ArrayList; import java.util.List; +import javax.inject.Inject; + +import static com.github.adamantcheese.chan.Chan.inject; import static com.github.adamantcheese.chan.utils.AndroidUtils.dp; public class AlbumDownloadController extends Controller implements View.OnClickListener { @@ -55,10 +59,15 @@ public class AlbumDownloadController extends Controller implements View.OnClickL private List items = new ArrayList<>(); private Loadable loadable; + @Inject + ImageSaver imageSaver; + private boolean allChecked = true; public AlbumDownloadController(Context context) { super(context); + + inject(this); } @Override @@ -96,7 +105,7 @@ public void onClick(View v) { .setPositiveButton(R.string.ok, null) .show(); } else { - final String folderForAlbum = Chan.injector().instance(ImageSaver.class).getSubFolder(loadable.title); + final String folderForAlbum = imageSaver.getSubFolder(loadable.title); String message = context.getString(R.string.album_download_confirm, context.getResources().getQuantityString(R.plurals.image, checkCount, checkCount), @@ -113,9 +122,15 @@ public void onClick(View v) { } } - if (Chan.injector().instance(ImageSaver.class).startBundledTask(context, folderForAlbum, tasks)) { + if (imageSaver.startBundledTask(context, folderForAlbum, tasks)) { navigationController.popController(); + return; } + + Toast.makeText( + context, + R.string.album_download_could_not_save_one_or_more_images, + Toast.LENGTH_SHORT).show(); }) .show(); } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ExperimentalSettingsController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ExperimentalSettingsController.java index 5258e3632a..a2f6718a2c 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ExperimentalSettingsController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ExperimentalSettingsController.java @@ -12,9 +12,7 @@ import com.github.adamantcheese.chan.ui.settings.SettingView; import com.github.adamantcheese.chan.ui.settings.SettingsController; import com.github.adamantcheese.chan.ui.settings.SettingsGroup; -import com.github.adamantcheese.chan.utils.IOUtils; -import java.io.File; import java.util.ArrayList; import java.util.List; @@ -23,6 +21,8 @@ import static com.github.adamantcheese.chan.Chan.inject; public class ExperimentalSettingsController extends SettingsController { + private static final String TAG = "ExperimentalSettingsController"; + public ExperimentalSettingsController(Context context) { super(context); } @@ -92,16 +92,12 @@ private void cancelAllDownloads() { } databaseManager.getDatabasePinManager().updatePins(downloadPins).call(); + boolean usesSAF = ChanSettings.isLocalThreadsDirUsesSAF(); for (Pin pin : downloadPins) { - String threadSubDir = ThreadSaveManager.getThreadSubDir(pin.loadable); - File threadSaveDir = new File(ChanSettings.saveLocation.get(), threadSubDir); - - if (!threadSaveDir.exists() || !threadSaveDir.isDirectory()) { - continue; - } - - IOUtils.deleteDirWithContents(threadSaveDir); + databaseManager.getDatabaseSavedThreadManager().deleteThreadFromDisk( + pin.loadable, + usesSAF); } return null; diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ImageViewerController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ImageViewerController.java index a3c4c975e9..c4bf297dce 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ImageViewerController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ImageViewerController.java @@ -37,6 +37,7 @@ import android.view.animation.DecelerateInterpolator; import android.widget.ArrayAdapter; import android.widget.ListView; +import android.widget.Toast; import androidx.appcompat.app.AlertDialog; @@ -96,6 +97,8 @@ public class ImageViewerController extends Controller implements ImageViewerPres @Inject ImageLoaderV2 imageLoaderV2; + @Inject + ImageSaver imageSaver; private int statusBarColorPrevious; private AnimatorSet startAnimation; @@ -295,7 +298,10 @@ private void saveShare(boolean share, PostImage postImage) { } task.setSubFolder(subFolderName); } - Chan.injector().instance(ImageSaver.class).startDownloadTask(context, task); + + if (!imageSaver.startDownloadTask(context, task)) { + Toast.makeText(context, "Couldn't start download task", Toast.LENGTH_LONG).show(); + } } } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ImportExportSettingsController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ImportExportSettingsController.java index d46b0241e4..257734f304 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ImportExportSettingsController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ImportExportSettingsController.java @@ -36,6 +36,7 @@ import com.github.adamantcheese.chan.ui.settings.SettingsController; import com.github.adamantcheese.chan.ui.settings.SettingsGroup; import com.github.adamantcheese.chan.utils.AndroidUtils; +import com.github.adamantcheese.chan.utils.Logger; import org.jetbrains.annotations.NotNull; @@ -46,6 +47,7 @@ public class ImportExportSettingsController extends SettingsController implements ImportExportSettingsPresenter.ImportExportSettingsCallbacks { + private static final String TAG = "ImportExportSettingsController"; public static final String EXPORT_FILE_NAME = getApplicationLabel() + "_exported_settings.json"; @Inject @@ -175,6 +177,13 @@ private void onFileChosen(Uri uri, boolean isNewFile) { // We use SAF here by default because settings importing/exporting does not depend on the // Kuroba default directory location. There is just no need to use old java files. ExternalFile externalFile = fileManager.fromUri(uri); + if (externalFile == null) { + String message = "onFileChosen() fileManager.fromUri() returned null, uri = " + uri; + + Logger.d(TAG, message); + showMessage(message); + return; + } navigationController.presentController(loadingViewController); presenter.doExport(externalFile, isNewFile); @@ -185,6 +194,13 @@ private void onImportClicked() { @Override public void onResult(@NotNull Uri uri) { ExternalFile externalFile = fileManager.fromUri(uri); + if (externalFile == null) { + String message = "onImportClicked() fileManager.fromUri() returned null, uri = " + uri; + + Logger.d(TAG, message); + showMessage(message); + return; + } navigationController.presentController(loadingViewController); presenter.doImport(externalFile); diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java index 2225386df9..98f774a5f3 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java @@ -19,14 +19,16 @@ import android.app.AlertDialog; import android.content.Context; import android.net.Uri; -import android.os.Environment; import android.widget.Toast; import com.github.adamantcheese.chan.R; +import com.github.adamantcheese.chan.core.database.DatabaseManager; +import com.github.adamantcheese.chan.core.model.orm.SavedThread; import com.github.adamantcheese.chan.core.saf.FileManager; import com.github.adamantcheese.chan.core.saf.callback.DirectoryChooserCallback; import com.github.adamantcheese.chan.core.saf.file.AbstractFile; import com.github.adamantcheese.chan.core.saf.file.ExternalFile; +import com.github.adamantcheese.chan.core.saf.file.FileDescriptorMode; import com.github.adamantcheese.chan.core.settings.ChanSettings; import com.github.adamantcheese.chan.ui.settings.BooleanSettingView; import com.github.adamantcheese.chan.ui.settings.LinkSettingView; @@ -40,7 +42,6 @@ import org.greenrobot.eventbus.Subscribe; import org.jetbrains.annotations.NotNull; -import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; @@ -54,7 +55,6 @@ import kotlin.Unit; import static com.github.adamantcheese.chan.Chan.inject; -import static com.github.adamantcheese.chan.utils.AndroidUtils.getApplicationLabel; import static com.github.adamantcheese.chan.utils.AndroidUtils.getString; public class MediaSettingsController extends SettingsController { @@ -71,6 +71,9 @@ public class MediaSettingsController extends SettingsController { @Inject FileManager fileManager; + @Inject + DatabaseManager databaseManager; + public MediaSettingsController(Context context) { super(context); } @@ -114,15 +117,25 @@ public void onPreferenceChange(SettingView item) { @Subscribe public void onEvent(ChanSettings.SettingChanged setting) { if (setting.setting == ChanSettings.saveLocationUri) { - String defaultDir = Environment.getExternalStorageDirectory() + - File.separator + - getApplicationLabel(); + // Image save location (SAF) was chosen + String defaultDir = ChanSettings.getDefaultSaveLocationDir(); ChanSettings.saveLocation.setNoUpdate(defaultDir); saveLocation.setDescription(ChanSettings.saveLocationUri.get()); + } else if (setting.setting == ChanSettings.localThreadsLocationUri) { + // Local threads location (SAF) was chosen + String defaultDir = ChanSettings.getDefaultLocalThreadsLocation(); + + ChanSettings.localThreadLocation.setNoUpdate(defaultDir); + localThreadsLocation.setDescription(ChanSettings.localThreadsLocationUri.get()); } else if (setting.setting == ChanSettings.saveLocation) { + // Image save location (Java File API) was chosen ChanSettings.saveLocationUri.setNoUpdate(""); saveLocation.setDescription(ChanSettings.saveLocation.get()); + } else if (setting.setting == ChanSettings.localThreadLocation) { + // Local threads location (Java File API) was chosen + ChanSettings.localThreadsLocationUri.setNoUpdate(""); + localThreadsLocation.setDescription(ChanSettings.localThreadLocation.get()); } } @@ -203,29 +216,26 @@ private void setupLocalThreadLocationSetting(SettingsGroup media) { LinkSettingView localThreadsLocationSetting = new LinkSettingView(this, R.string.media_settings_local_threads_location_title, 0, - v -> onLocalThreadsLocationSettingClicked()); + v -> showUseSAFOrOldAPIForLocalThreadsLocationDialog()); - String localThreadsLocationString; - - if (ChanSettings.localThreadsLocationUri.get().isEmpty()) { - localThreadsLocationString = context.getString(R.string.media_settings_local_threads_setting_not_set); - } else { - localThreadsLocationString = ChanSettings.localThreadsLocationUri.get(); - } localThreadsLocation = (LinkSettingView) media.add(localThreadsLocationSetting); - localThreadsLocation.setDescription(localThreadsLocationString); + localThreadsLocation.setDescription(getLocalThreadsLocation()); } - private void onLocalThreadsLocationSettingClicked() { - // TODO + private String getLocalThreadsLocation() { + if (!ChanSettings.localThreadsLocationUri.get().isEmpty()) { + return ChanSettings.localThreadsLocationUri.get(); + } + + return ChanSettings.localThreadLocation.get(); } private void setupSaveLocationSetting(SettingsGroup media) { LinkSettingView chooseSaveLocationSetting = new LinkSettingView(this, R.string.save_location_screen, 0, - v -> showDialog()); + v -> showUseSAFOrOldAPIForSaveLocationDialog()); saveLocation = (LinkSettingView) media.add(chooseSaveLocationSetting); saveLocation.setDescription(getSaveLocation()); @@ -239,10 +249,89 @@ private String getSaveLocation() { return ChanSettings.saveLocation.get(); } - private void showDialog() { + private void showUseSAFOrOldAPIForLocalThreadsLocationDialog() { + if (hasActiveDownloadingThreads()) { + // I don't even want to imagine what's going to happen if we allow this + Toast.makeText( + context, + R.string.media_settings_cannot_switch_local_threads_base_dir_message, + Toast.LENGTH_LONG).show(); + return; + } + + AlertDialog alertDialog = new AlertDialog.Builder(context) + .setTitle(R.string.use_saf_for_local_threads_location_dialog_title) + .setMessage(R.string.use_saf_for_local_threads_location_dialog_message) + .setPositiveButton(R.string.use_saf_dialog_positive_button_text, (dialog, which) -> { + onLocalThreadsLocationUseSAFClicked(); + }) + .setNegativeButton(R.string.use_saf_dialog_negative_button_text, (dialog, which) -> { + onLocalThreadsLocationUseOldApiClicked(); + }) + .create(); + + alertDialog.show(); + } + + private boolean hasActiveDownloadingThreads() { + List savedThreads = databaseManager.runTask( + databaseManager.getDatabaseSavedThreadManager().getSavedThreads()); + + for (SavedThread savedThread : savedThreads) { + if (savedThread.isRunning()) { + return true; + } + } + + return false; + } + + /** + * Select a directory where local threads will be stored via the old Java File API + */ + private void onLocalThreadsLocationUseOldApiClicked() { + SaveLocationController saveLocationController = new SaveLocationController( + context, + SaveLocationController.SaveLocationControllerMode.LocalThreadsSaveLocation, + dirPath -> { + Logger.d(TAG, "SaveLocationController with LocalThreadsSaveLocation mode returned dir " + + dirPath); + + // Supa hack to get the callback called + ChanSettings.localThreadLocation.setSync(""); + ChanSettings.localThreadLocation.setSync(dirPath); + }); + + navigationController.pushController(saveLocationController); + } + + /** + * Select a directory where local threads will be stored via the SAF + */ + private void onLocalThreadsLocationUseSAFClicked() { + fileManager.openChooseDirectoryDialog(new DirectoryChooserCallback() { + @Override + public void onResult(@NotNull Uri uri) { + ChanSettings.localThreadsLocationUri.set(uri.toString()); + + String defaultDir = ChanSettings.getDefaultLocalThreadsLocation(); + ChanSettings.localThreadLocation.setNoUpdate(defaultDir); + localThreadsLocation.setDescription(uri.toString()); + + testMethod(uri); + } + + @Override + public void onCancel(@NotNull String reason) { + Toast.makeText(context, reason, Toast.LENGTH_LONG).show(); + } + }); + } + + private void showUseSAFOrOldAPIForSaveLocationDialog() { AlertDialog alertDialog = new AlertDialog.Builder(context) - .setTitle(R.string.use_saf_dialog_title) - .setMessage(R.string.use_saf_dialog_message) + .setTitle(R.string.use_saf_for_save_location_dialog_title) + .setMessage(R.string.use_saf_for_save_location_dialog_message) .setPositiveButton(R.string.use_saf_dialog_positive_button_text, (dialog, which) -> { onSaveLocationUseSAFClicked(); }) @@ -254,20 +343,35 @@ private void showDialog() { alertDialog.show(); } + /** + * Select a directory where saved images will be stored via the old Java File API + */ private void onSaveLocationUseOldApiClicked() { - navigationController.pushController(new SaveLocationController(context)); + SaveLocationController saveLocationController = new SaveLocationController( + context, + SaveLocationController.SaveLocationControllerMode.ImageSaveLocation, + dirPath -> { + Logger.d(TAG, "SaveLocationController with ImageSaveLocation mode returned dir " + + dirPath); + + // Supa hack to get the callback called + ChanSettings.saveLocation.setSync(""); + ChanSettings.saveLocation.setSync(dirPath); + }); + + navigationController.pushController(saveLocationController); } + /** + * Select a directory where saved images will be stored via the SAF + */ private void onSaveLocationUseSAFClicked() { fileManager.openChooseDirectoryDialog(new DirectoryChooserCallback() { @Override public void onResult(@NotNull Uri uri) { ChanSettings.saveLocationUri.set(uri.toString()); - String defaultDir = Environment.getExternalStorageDirectory() + - File.separator + - getApplicationLabel(); - + String defaultDir = ChanSettings.getDefaultSaveLocationDir(); ChanSettings.saveLocation.setNoUpdate(defaultDir); saveLocation.setDescription(uri.toString()); @@ -357,7 +461,7 @@ private void testMethod(@NotNull Uri uri) { } { - AbstractFile externalFile = fileManager.newFile() + AbstractFile externalFile = fileManager.newSaveLocationFile() .appendSubDirSegment("1234") .appendSubDirSegment("4566") .appendFileNameSegment("filename.json") @@ -379,7 +483,7 @@ private void testMethod(@NotNull Uri uri) { throw new RuntimeException("externalFile1 name != filename.json"); } - AbstractFile dir = fileManager.newFile() + AbstractFile dir = fileManager.newSaveLocationFile() .appendSubDirSegment("1234") .appendSubDirSegment("4566"); @@ -392,9 +496,10 @@ private void testMethod(@NotNull Uri uri) { throw new RuntimeException("Couldn't find filename.json"); } + // Write string to the file String testString = "Hello world"; - foundFile.withFileDescriptor(AbstractFile.FileDescriptorMode.WriteTruncate, (fd) -> { + foundFile.withFileDescriptor(FileDescriptorMode.WriteTruncate, (fd) -> { try (OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(fd))) { osw.write(testString); osw.flush(); @@ -410,7 +515,7 @@ private void testMethod(@NotNull Uri uri) { + foundFile.getLength()); } - foundFile.withFileDescriptor(AbstractFile.FileDescriptorMode.Read, (fd) -> { + foundFile.withFileDescriptor(FileDescriptorMode.Read, (fd) -> { try (InputStreamReader isr = new InputStreamReader(new FileInputStream(fd))) { char[] stringBytes = new char[testString.length()]; int read = isr.read(stringBytes); @@ -420,7 +525,7 @@ private void testMethod(@NotNull Uri uri) { } String resultString = new String(stringBytes); - if (!resultString.equals(testString)){ + if (!resultString.equals(testString)) { throw new RuntimeException("resultString != testString, resultString = " + resultString); } @@ -432,6 +537,47 @@ private void testMethod(@NotNull Uri uri) { return Unit.INSTANCE; }); + // Write another string that is shorter than the previous string + String testString2 = "Hello"; + + foundFile.withFileDescriptor(FileDescriptorMode.WriteTruncate, (fd) -> { + try (OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(fd))) { + osw.write(testString2); + osw.flush(); + } catch (IOException e) { + e.printStackTrace(); + } + + return Unit.INSTANCE; + }); + + if (foundFile.getLength() != testString2.length()) { + throw new RuntimeException("file length != testString.length(), file length = " + + foundFile.getLength()); + } + + foundFile.withFileDescriptor(FileDescriptorMode.Read, (fd) -> { + try (InputStreamReader isr = new InputStreamReader(new FileInputStream(fd))) { + char[] stringBytes = new char[testString2.length()]; + int read = isr.read(stringBytes); + + if (read != testString2.length()) { + throw new RuntimeException("read bytes != testString2.length(), read = " + read); + } + + String resultString = new String(stringBytes); + if (!resultString.equals(testString2)) { + throw new RuntimeException("resultString != testString2, resultString = " + + resultString); + } + + } catch (IOException e) { + e.printStackTrace(); + } + + return Unit.INSTANCE; + }); + AbstractFile parent = externalFile.getParent().getParent(); if (!parent.getName().equals("1234")) { throw new RuntimeException("dir.name != 1234, name = " + parent.getName()); diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ViewThreadController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ViewThreadController.java index 7748263182..59d6e9e86e 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ViewThreadController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ViewThreadController.java @@ -156,16 +156,6 @@ private void pinClicked(ToolbarMenuItem item) { } private void saveClicked(ToolbarMenuItem item) { - if (ChanSettings.localThreadsLocationUri.get().isEmpty()) { - // TODO: show the SAF directory chooser right here instead of just showing a toast? Or - // open up the media settings controller? - Toast.makeText( - context, - R.string.view_thread_controller_local_threads_location_is_not_set, - Toast.LENGTH_LONG).show(); - return; - } - RuntimePermissionsHelper runtimePermissionsHelper = ((StartActivity) context).getRuntimePermissionsHelper(); if (runtimePermissionsHelper.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { saveClickedInternal(); diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/helper/ImagePickDelegate.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/helper/ImagePickDelegate.java index 048dc067fc..3abe934cea 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/helper/ImagePickDelegate.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/helper/ImagePickDelegate.java @@ -31,12 +31,13 @@ import com.github.adamantcheese.chan.core.cache.FileCache; import com.github.adamantcheese.chan.core.cache.FileCacheListener; import com.github.adamantcheese.chan.core.manager.ReplyManager; +import com.github.adamantcheese.chan.core.saf.FileManager; +import com.github.adamantcheese.chan.core.saf.file.RawFile; import com.github.adamantcheese.chan.utils.IOUtils; import com.github.adamantcheese.chan.utils.Logger; import java.io.File; import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -58,6 +59,8 @@ public class ImagePickDelegate implements Runnable { @Inject ReplyManager replyManager; + @Inject + FileManager fileManager; private Activity activity; @@ -65,7 +68,7 @@ public class ImagePickDelegate implements Runnable { private Uri uri; private String fileName; private boolean success = false; - private File cacheFile; + private RawFile cacheFile; public ImagePickDelegate(Activity activity) { this.activity = activity; @@ -91,10 +94,10 @@ public void pick(ImagePickCallback callback, boolean longPressed) { HttpUrl finalClipboardURL = clipboardURL; Chan.injector().instance(FileCache.class).downloadFile(clipboardURL.toString(), new FileCacheListener() { @Override - public void onSuccess(File file) { + public void onSuccess(RawFile file) { Toast.makeText(activity, activity.getString(R.string.image_url_get_success), Toast.LENGTH_SHORT).show(); Uri imageURL = Uri.parse(finalClipboardURL.toString()); - callback.onFilePicked(imageURL.getLastPathSegment(), file); + callback.onFilePicked(imageURL.getLastPathSegment(), new File(file.getFullPath())); reset(); } @@ -174,13 +177,23 @@ public boolean onActivityResult(int requestCode, int resultCode, Intent data) { @Override public void run() { - cacheFile = replyManager.getPickFile(); + cacheFile = fileManager.fromRawFile(replyManager.getPickFile()); InputStream is = null; OutputStream os = null; try (ParcelFileDescriptor fileDescriptor = activity.getContentResolver().openFileDescriptor(uri, "r")) { + if (fileDescriptor == null) { + throw new IOException("Couldn't open file descriptor for uri = " + uri); + } + is = new FileInputStream(fileDescriptor.getFileDescriptor()); - os = new FileOutputStream(cacheFile); + os = cacheFile.getOutputStream(); + + if (os == null) { + throw new IOException("Could not get OutputStream from the cacheFile, " + + "cacheFile = " + cacheFile.getFullPath()); + } + boolean fullyCopied = IOUtils.copy(is, os, MAX_FILE_SIZE); if (fullyCopied) { success = true; @@ -200,7 +213,7 @@ public void run() { runOnUiThread(() -> { if (success) { - callback.onFilePicked(fileName, cacheFile); + callback.onFilePicked(fileName, new File(cacheFile.getFullPath())); } else { callback.onFilePickError(false); } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/service/WatchNotification.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/service/WatchNotification.java index 58301b293c..544e87b1df 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/service/WatchNotification.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/service/WatchNotification.java @@ -237,7 +237,9 @@ private void updateSavedThreads(HashMap>> Loadable loadable = entry.getValue().first; List posts = entry.getValue().second; - threadSaveManager.enqueueThreadToSave(loadable, posts); + if (!threadSaveManager.enqueueThreadToSave(loadable, posts)) { + watchManager.stopSavingThread(loadable); + } } } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/view/MultiImageView.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/view/MultiImageView.java index 9f0101db59..1ef8961cce 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/view/MultiImageView.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/view/MultiImageView.java @@ -29,6 +29,7 @@ import android.widget.ImageView; import android.widget.Toast; +import androidx.annotation.NonNull; import androidx.core.content.FileProvider; import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleObserver; @@ -48,6 +49,8 @@ import com.github.adamantcheese.chan.core.image.ImageLoaderV2; import com.github.adamantcheese.chan.core.model.PostImage; import com.github.adamantcheese.chan.core.model.orm.Loadable; +import com.github.adamantcheese.chan.core.saf.file.AbstractFile; +import com.github.adamantcheese.chan.core.saf.file.RawFile; import com.github.adamantcheese.chan.core.settings.ChanSettings; import com.github.adamantcheese.chan.utils.AndroidUtils; import com.github.adamantcheese.chan.utils.Logger; @@ -64,6 +67,7 @@ import java.io.File; import java.io.IOException; +import java.io.InputStream; import javax.inject.Inject; @@ -292,8 +296,8 @@ public void onProgress(long downloaded, long total) { } @Override - public void onSuccess(File file) { - setBigImageFile(file); + public void onSuccess(RawFile file) { + setBigImageFile(new File(file.getFullPath())); } @Override @@ -335,9 +339,9 @@ public void onProgress(long downloaded, long total) { } @Override - public void onSuccess(File file) { + public void onSuccess(RawFile file) { if (!hasContent || mode == Mode.GIF) { - setGifFile(file); + setGifFile(new File(file.getFullPath())); } } @@ -408,9 +412,9 @@ public void onProgress(long downloaded, long total) { } @Override - public void onSuccess(File file) { + public void onSuccess(RawFile file) { if (!hasContent || mode == Mode.MOVIE) { - setVideoFile(file); + setVideoFile(new File(file.getFullPath())); } } @@ -436,7 +440,6 @@ private void setVideoFile(final File file) { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setDataAndType(FileProvider.getUriForFile(getAppContext(), getAppContext().getPackageName() + ".fileprovider", file), "video/*"); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - AndroidUtils.openIntent(intent); onModeLoaded(Mode.MOVIE, null); diff --git a/Kuroba/app/src/main/res/values/strings.xml b/Kuroba/app/src/main/res/values/strings.xml index 8456c47a93..5a0be6974d 100644 --- a/Kuroba/app/src/main/res/values/strings.xml +++ b/Kuroba/app/src/main/res/values/strings.xml @@ -687,14 +687,18 @@ Don't have a 4chan Pass?
Can\'t share this thread, it\'s already been deleted 99+ Allow full sensor rotation - Use the new Storage Access Framework API to choose the base directory? - If you choose to use the SAF you will be able to choose the sd-card (or even something like Google Drive) as a place to store images/files/downloaded thread etc + Use the new Storage Access Framework API to choose images download directory? + Use the new Storage Access Framework API to choose local threads download directory? + If you choose to use the SAF you will be able to choose the sd-card (or even something like Google Drive) as a place to store downloaded images + If you choose to use the SAF you will be able to choose the sd-card (or even something like Google Drive) as a place to store local threads images Use SAF API Use old API Overwrite existing file or create a new one? Overwrite Create new Local threads location - Not set Local threads location]]> + Cannot switch local threads base directory when there is at least one thread being downloaded. Stop all downloadings (or delete threads) and then try again. + Base local threads directory does not exist (or it was deleted) + Could not save one or more images diff --git a/Kuroba/app/src/test/java/com/github/adamantcheese/chan/core/ExtensionsKtTest.kt b/Kuroba/app/src/test/java/com/github/adamantcheese/chan/core/ExtensionsKtTest.kt new file mode 100644 index 0000000000..45328fc49f --- /dev/null +++ b/Kuroba/app/src/test/java/com/github/adamantcheese/chan/core/ExtensionsKtTest.kt @@ -0,0 +1,29 @@ +package com.github.adamantcheese.chan.core + +import org.junit.Assert.* +import org.junit.Test + +class ExtensionsKtTest { + + @Test + fun testConvertIntToCharArray() { + val int = 0x11223344 + val charArray = int.toCharArray() + + assertEquals(0x11, charArray[0].toInt()) + assertEquals(0x22, charArray[1].toInt()) + assertEquals(0x33, charArray[2].toInt()) + assertEquals(0x44, charArray[3].toInt()) + } + + @Test + fun testCharArrayToInt() { + val charArray = CharArray(4) + charArray[0] = 0x11.toChar() + charArray[1] = 0x22.toChar() + charArray[2] = 0x33.toChar() + charArray[3] = 0x44.toChar() + + assertEquals(0x11223344, charArray.toInt()) + } +} \ No newline at end of file From 941a3d2a6d94b97f5969d7ad4771e0df97fef1a4 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 25 Aug 2019 14:18:08 +0300 Subject: [PATCH 030/184] (#172) Fix FileCacheDownloader bug where the output file wouldn't be created because it has no extension --- .../chan/core/cache/CacheHandler.java | 9 ++++++++- .../chan/core/cache/FileCacheDownloader.java | 7 +++++-- .../chan/core/saver/ImageSaveTask.java | 15 ++++++++------- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/CacheHandler.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/CacheHandler.java index d1dd2848e5..e554fcd389 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/CacheHandler.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/CacheHandler.java @@ -41,6 +41,7 @@ public class CacheHandler { private static final String TAG = "CacheHandler"; //1GB for prefetching, so that entire threads can be loaded at once more easily, otherwise 100MB is plenty private static final long FILE_CACHE_DISK_SIZE = (ChanSettings.autoLoadThreadImages.get() ? 1000 : 100) * 1024 * 1024; + private static final String CACHE_EXTENSION = "cache"; private final ExecutorService pool = Executors.newSingleThreadExecutor(); private final RawFile cacheDirFile; @@ -68,8 +69,14 @@ public boolean exists(String key) { public RawFile get(String key) { createDirectories(); + String fileName = String.format( + "%s.%s", + // We need extension here because AbstractFile expects all file names to have + // extensions + String.valueOf(key.hashCode()), CACHE_EXTENSION); + return cacheDirFile.clone() - .appendSubDirSegment(String.valueOf(key.hashCode())); + .appendFileNameSegment(fileName); } public File randomCacheFile() throws IOException { diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/FileCacheDownloader.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/FileCacheDownloader.java index 02adf71007..ec8b717958 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/FileCacheDownloader.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/FileCacheDownloader.java @@ -134,6 +134,11 @@ private void execute() { Source source = body.source(); sourceCloseable = source; + if (!output.exists() && !output.create()) { + throw new IOException("Couldn't create output file, output = " + + output.getFullPath()); + } + outputFileOutputStream = output.getOutputStream(); if (outputFileOutputStream == null) { throw new IOException("Couldn't get output file's OutputStream"); @@ -145,9 +150,7 @@ private void execute() { checkCancel(); log("got input stream"); - pipeBody(source, sink); - log("done"); long fileLen = output.getLength(); diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaveTask.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaveTask.java index 7b9a82c4e1..c2e3f82116 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaveTask.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaveTask.java @@ -132,13 +132,14 @@ private void deleteDestination() { private void onDestination() { success = true; - String[] paths = {destination.getFullPath()}; - - // TODO: may not work - MediaScannerConnection.scanFile(getAppContext(), paths, null, (path, uri) -> { - // Runs on a binder thread - AndroidUtils.runOnUiThread(() -> afterScan(uri)); - }); +// String[] paths = {destination.getFullPath()}; + + // FIXME: does not work. Who in their right mind even wants their downloaded images + // to be scanned by the google botnet +// MediaScannerConnection.scanFile(getAppContext(), paths, null, (path, uri) -> { +// // Runs on a binder thread +// AndroidUtils.runOnUiThread(() -> afterScan(uri)); +// }); } private boolean copyToDestination(File source) { From b2878bef5bd177cc3b0b7897ed597d058607095d Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 25 Aug 2019 15:06:44 +0300 Subject: [PATCH 031/184] (#172) Return back lost SaveLocationController --- .../ui/controller/SaveLocationController.java | 199 ++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100644 Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/SaveLocationController.java diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/SaveLocationController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/SaveLocationController.java new file mode 100644 index 0000000000..e0f3654e7a --- /dev/null +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/SaveLocationController.java @@ -0,0 +1,199 @@ +/* + * Kuroba - *chan browser https://github.com/Adamantcheese/Kuroba/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.github.adamantcheese.chan.ui.controller; + +import android.Manifest; +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.DialogInterface; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.Toast; + +import androidx.appcompat.app.AlertDialog; + +import com.github.adamantcheese.chan.R; +import com.github.adamantcheese.chan.StartActivity; +import com.github.adamantcheese.chan.controller.Controller; +import com.github.adamantcheese.chan.core.saver.FileWatcher; +import com.github.adamantcheese.chan.core.settings.ChanSettings; +import com.github.adamantcheese.chan.ui.adapter.FilesAdapter; +import com.github.adamantcheese.chan.ui.helper.RuntimePermissionsHelper; +import com.github.adamantcheese.chan.ui.layout.FilesLayout; +import com.github.adamantcheese.chan.ui.layout.NewFolderLayout; +import com.google.android.material.floatingactionbutton.FloatingActionButton; + +import java.io.File; + +public class SaveLocationController extends Controller implements FileWatcher.FileWatcherCallback, FilesAdapter.Callback, FilesLayout.Callback, View.OnClickListener { + private FilesLayout filesLayout; + private FloatingActionButton setButton; + private FloatingActionButton addButton; + private RuntimePermissionsHelper runtimePermissionsHelper; + private FileWatcher fileWatcher; + private SaveLocationControllerMode mode; + private SaveLocationControllerCallback callback; + + public SaveLocationController( + Context context, + SaveLocationControllerMode mode, + SaveLocationControllerCallback callback) { + super(context); + + this.callback = callback; + this.mode = mode; + } + + @Override + public void onCreate() { + super.onCreate(); + + navigation.setTitle(R.string.save_location_screen); + + view = inflateRes(R.layout.controller_save_location); + filesLayout = view.findViewById(R.id.files_layout); + filesLayout.setCallback(this); + setButton = view.findViewById(R.id.set_button); + setButton.setOnClickListener(this); + addButton = view.findViewById(R.id.add_button); + addButton.setOnClickListener(this); + + File saveLocation; + + if (mode == SaveLocationControllerMode.ImageSaveLocation) { + if (ChanSettings.saveLocation.get().isEmpty()) { + throw new IllegalStateException("saveLocation is empty!"); + } + + saveLocation = new File(ChanSettings.saveLocation.get()); + } else { + if (ChanSettings.localThreadLocation.get().isEmpty()) { + throw new IllegalStateException("localThreadLocation is empty!"); + } + + saveLocation = new File(ChanSettings.localThreadLocation.get()); + } + + fileWatcher = new FileWatcher(this, saveLocation); + + runtimePermissionsHelper = ((StartActivity) context).getRuntimePermissionsHelper(); + if (runtimePermissionsHelper.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + initialize(); + } else { + requestPermission(); + } + } + + @Override + public void onClick(View v) { + if (v == setButton) { + onDirectoryChosen(); + navigationController.popController(); + } else if (v == addButton) { + @SuppressLint("InflateParams") final NewFolderLayout dialogView = + (NewFolderLayout) LayoutInflater.from(context) + .inflate(R.layout.layout_folder_add, null); + + new AlertDialog.Builder(context) + .setView(dialogView) + .setTitle(R.string.save_new_folder) + .setPositiveButton(R.string.add, (dialog, which) -> { + onPositionButtonClick(dialogView, dialog); + }) + .setNegativeButton(R.string.cancel, null) + .create() + .show(); + } + } + + private void onPositionButtonClick(NewFolderLayout dialogView, DialogInterface dialog) { + if (!dialogView.getFolderName().matches("\\A\\w+\\z")) { + Toast.makeText( + context, + "Folder must be a word, no spaces", + Toast.LENGTH_SHORT).show(); + } else { + File newDir = new File( + fileWatcher.getCurrentPath().getAbsolutePath() + + File.separator + + dialogView.getFolderName()); + + if (!newDir.mkdir()) { + throw new IllegalStateException("Could not create directory " + + newDir.getAbsolutePath()); + } + + fileWatcher.navigateTo(newDir); + + onDirectoryChosen(); + navigationController.popController(); + } + + dialog.dismiss(); + } + + private void onDirectoryChosen() { + callback.onDirectorySelected(fileWatcher.getCurrentPath().getAbsolutePath()); + } + + @Override + public void onFiles(FileWatcher.FileItems fileItems) { + filesLayout.setFiles(fileItems); + } + + @Override + public void onBackClicked() { + fileWatcher.navigateUp(); + } + + @Override + public void onFileItemClicked(FileWatcher.FileItem fileItem) { + if (fileItem.canNavigate()) { + fileWatcher.navigateTo(fileItem.file); + } + // Else ignore, we only do folder selection here + } + + private void requestPermission() { + runtimePermissionsHelper.requestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, granted -> { + if (runtimePermissionsHelper.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + initialize(); + } else { + runtimePermissionsHelper.showPermissionRequiredDialog( + context, + context.getString(R.string.save_location_storage_permission_required_title), + context.getString(R.string.save_location_storage_permission_required), + this::requestPermission + ); + } + }); + } + + private void initialize() { + filesLayout.initialize(); + fileWatcher.initialize(); + } + + public interface SaveLocationControllerCallback { + void onDirectorySelected(String dirPath); + } + + public enum SaveLocationControllerMode { + ImageSaveLocation, + LocalThreadsSaveLocation + } +} From 478e3a7f3177bbb365699d7286b4daa113336c41 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 25 Aug 2019 15:16:03 +0300 Subject: [PATCH 032/184] (#172) Remove empty files --- .../adamantcheese/chan/core/manager/SavedThreadLoaderManager.java | 0 .../chan/core/repository/SavedThreadLoaderRepository.java | 0 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/SavedThreadLoaderManager.java delete mode 100644 Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/SavedThreadLoaderRepository.java diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/SavedThreadLoaderManager.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/SavedThreadLoaderManager.java deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/SavedThreadLoaderRepository.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/SavedThreadLoaderRepository.java deleted file mode 100644 index e69de29bb2..0000000000 From 6772d78df805c1b691d9cae21ae5acd30249d81f Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 25 Aug 2019 15:18:53 +0300 Subject: [PATCH 033/184] (#172) Fix grammar mistakes --- .../github/adamantcheese/chan/core/saf/file/AbstractFile.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt index e081877b2c..675a31ddd6 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt @@ -8,7 +8,7 @@ import java.io.* /** * An abstraction class over both the Java File and the new Storage Access Framework DocumentFile. * - * Some methods are marked with [MutableMethod] annotation. This means that such method are gonna + * Some methods are marked with [MutableMethod] annotation. This means that such methods are gonna * mutate the inner data of the [AbstractFile] (such as root or segments). Sometimes this behavior is * not desirable. For example, when you have an AbstractFile representing some directory that may * not even exists on the disk and you want to check whether it exists and if it does check some @@ -17,7 +17,7 @@ import java.io.* * method on the file that represents the directory. It will create a copy of the file that you can * safely work without worry that the original file may change. * - * Other methods are marked with [ImmutableMethod] annotation. This means that those files create a + * Other methods are marked with [ImmutableMethod] annotation. This means that those methods create a * copy of the [AbstractFile] internally and are safe to use without calling [clone] * * Examples. @@ -265,7 +265,7 @@ abstract class AbstractFile( * If it's a file we can't do that so usually when attempting to append something to the FileRoot * an exception will be thrown * - * @param holder either Uri or File. Represents either just a path or a path with file name + * @param holder either DocumentFile or File. * */ sealed class Root(val holder: T) { From a38b337a52a6c416aeb740976db573a3bbcd7683 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 25 Aug 2019 17:28:27 +0300 Subject: [PATCH 034/184] (#172) Fix media scanner scanning file with disabled setting Fix Uri segments not being encoded when appended --- .../adamantcheese/chan/core/Extensions.kt | 2 +- .../chan/core/manager/ThreadSaveManager.java | 2 +- .../chan/core/presenter/ThreadPresenter.java | 19 +++++---------- .../chan/core/saf/file/ExternalFile.kt | 2 +- .../ui/controller/ViewThreadController.java | 23 +++++++++++++++++-- Kuroba/app/src/main/res/values/strings.xml | 2 +- 6 files changed, 31 insertions(+), 19 deletions(-) diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/Extensions.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/Extensions.kt index 5052ca3f93..056f1edd95 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/Extensions.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/Extensions.kt @@ -22,7 +22,7 @@ fun String.extension(): String? { fun Uri.Builder.appendManyEncoded(segments: List): Uri.Builder { for (segment in segments) { - this.appendEncodedPath(segment) + this.appendPath(segment) } return this diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/ThreadSaveManager.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/ThreadSaveManager.java index 20e07928be..6a86d8f7cd 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/ThreadSaveManager.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/ThreadSaveManager.java @@ -505,7 +505,7 @@ private void dealWithMediaScanner(AbstractFile threadSaveDirImages) throws Could .clone() .appendFileNameSegment(NO_MEDIA_FILE_NAME); - if (ChanSettings.allowMediaScannerToScanLocalThreads.get()) { + if (!ChanSettings.allowMediaScannerToScanLocalThreads.get()) { // .nomedia file being in the images directory "should" prevent media scanner from // scanning this directory if (!noMediaFile.exists() && !noMediaFile.create()) { diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/ThreadPresenter.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/ThreadPresenter.java index d78fb6f521..d5729afc3e 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/ThreadPresenter.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/ThreadPresenter.java @@ -327,15 +327,12 @@ public boolean pin() { public boolean save() { Pin pin = watchManager.findPinByLoadableId(loadable.id); if (pin == null || !PinType.hasDownloadFlag(pin.pinType)) { - return saveInternal(); - } + boolean startedSaving = saveInternal(); + if (!startedSaving) { + watchManager.stopSavingThread(loadable); + } - if (!fileManager.baseLocalThreadsDirectoryExists()) { - Toast.makeText( - context, - R.string.base_local_threads_dir_not_exists, - Toast.LENGTH_LONG).show(); - return false; + return startedSaving; } pin.pinType = PinType.removeDownloadNewPostsFlag(pin.pinType); @@ -346,17 +343,13 @@ public boolean save() { watchManager.stopSavingThread(pin.loadable); } - if (!saveInternal()) { - watchManager.stopSavingThread(loadable); - return false; - } - loadable.loadableDownloadingState = Loadable.LoadableDownloadingState.NotDownloading; return true; } private boolean saveInternal() { if (chanLoader.getThread() == null) { + Logger.e(TAG, "chanLoader.getThread() == null"); return false; } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt index ace2136864..aa9008d71f 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt @@ -320,7 +320,7 @@ class ExternalFile( val builder = uri.buildUpon() for (i in index until segments.size) { - builder.appendEncodedPath(segments[i].name) + builder.appendPath(segments[i].name) } return DocumentFile.fromSingleUri(appContext, builder.build()) diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ViewThreadController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ViewThreadController.java index f58a8115b7..32a9aad30a 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ViewThreadController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ViewThreadController.java @@ -44,6 +44,7 @@ import com.github.adamantcheese.chan.core.model.orm.PinType; import com.github.adamantcheese.chan.core.model.orm.SavedThread; import com.github.adamantcheese.chan.core.presenter.ThreadPresenter; +import com.github.adamantcheese.chan.core.saf.FileManager; import com.github.adamantcheese.chan.core.settings.ChanSettings; import com.github.adamantcheese.chan.ui.helper.HintPopup; import com.github.adamantcheese.chan.ui.helper.RuntimePermissionsHelper; @@ -56,6 +57,7 @@ import com.github.adamantcheese.chan.ui.toolbar.ToolbarMenuSubItem; import com.github.adamantcheese.chan.utils.AndroidUtils; import com.github.adamantcheese.chan.utils.AnimationUtils; +import com.github.adamantcheese.chan.utils.Logger; import org.greenrobot.eventbus.Subscribe; @@ -69,6 +71,8 @@ import static com.github.adamantcheese.chan.utils.AndroidUtils.getAttrColor; public class ViewThreadController extends ThreadController implements ThreadLayout.ThreadLayoutCallback, ArchivesLayout.Callback { + private static final String TAG = "ViewThreadController"; + private static final int PIN_ID = 1; private static final int SAVE_THREAD_ID = 2; @@ -77,6 +81,8 @@ public class ViewThreadController extends ThreadController implements ThreadLayo @Inject WatchManager watchManager; + @Inject + FileManager fileManager; private boolean pinItemPinned = false; private DownloadThreadState prevState = DownloadThreadState.Default; @@ -130,11 +136,16 @@ public void onCreate() { } protected void buildMenu() { + prevState = DownloadThreadState.Default; + NavigationItem.MenuBuilder menuBuilder = navigation.buildMenu() .withItem(R.drawable.ic_image_white_24dp, this::albumClicked) .withItem(PIN_ID, R.drawable.ic_bookmark_outline_white_24dp, this::pinClicked); if (ChanSettings.incrementalThreadDownloadingEnabled.get()) { + // This method recreates the menu (and if there was the download animation running it + // will be reset to the default icon). We need to reset the prev state as well so that + // we can start animation again menuBuilder.withItem(SAVE_THREAD_ID, downloadIconOutline, this::saveClicked); } @@ -196,6 +207,12 @@ private void saveClicked(ToolbarMenuItem item) { return; } + if (!fileManager.baseLocalThreadsDirectoryExists()) { + Logger.e(TAG, "Base local threads directory does not exist"); + Toast.makeText(context, R.string.base_local_threads_dir_not_exists, Toast.LENGTH_LONG).show(); + return; + } + RuntimePermissionsHelper runtimePermissionsHelper = ((StartActivity) context).getRuntimePermissionsHelper(); if (runtimePermissionsHelper.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { saveClickedInternal(); @@ -216,10 +233,11 @@ private void saveClicked(ToolbarMenuItem item) { private void saveClickedInternal() { if (threadLayout.getPresenter().save()) { - setSaveIconState(true); updateDrawerHighlighting(loadable); - populateLocalOrLiveVersionMenu(); + + // Update icon at the very end, otherwise it won't start animating at all + setSaveIconState(true); } } @@ -662,6 +680,7 @@ private void setSaveIconStateDrawable( menuItem.setImage(downloadIconOutline, animated); break; case DownloadInProgress: + // FIXME: shit is broken menuItem.setImage(downloadAnimation, animated); downloadAnimation.start(); diff --git a/Kuroba/app/src/main/res/values/strings.xml b/Kuroba/app/src/main/res/values/strings.xml index 3ec684a0f2..bda2708fa9 100644 --- a/Kuroba/app/src/main/res/values/strings.xml +++ b/Kuroba/app/src/main/res/values/strings.xml @@ -702,6 +702,6 @@ Don't have a 4chan Pass?
Local threads location Local threads location]]> Cannot switch local threads base directory when there is at least one thread being downloaded. Stop all downloadings (or delete threads) and then try again. - Base local threads directory does not exist (or it was deleted) + Base local threads directory does not exist (or it was deleted). You need to manually set it again in Media settings. Could not save one or more images From 3e792918d8210ce570fb6b2f21f970a6fb59a4fb Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 25 Aug 2019 17:38:35 +0300 Subject: [PATCH 035/184] (#172) Remove testMethod when choosing local threads location (since it breaks the app and don't test anything useful) --- .../chan/ui/controller/MediaSettingsController.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java index 38cb4c8fa7..39e8a66bda 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java @@ -317,8 +317,6 @@ public void onResult(@NotNull Uri uri) { String defaultDir = ChanSettings.getDefaultLocalThreadsLocation(); ChanSettings.localThreadLocation.setNoUpdate(defaultDir); localThreadsLocation.setDescription(uri.toString()); - - testMethod(uri); } @Override From c0492b7bf09f170ea27f32482fee80e9daf68be9 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 25 Aug 2019 17:40:14 +0300 Subject: [PATCH 036/184] (#172) Revert strict mode back --- .../com/github/adamantcheese/chan/Chan.java | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/Chan.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/Chan.java index 3bb1f665b4..cfc52dd77e 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/Chan.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/Chan.java @@ -103,19 +103,19 @@ public void onCreate() { // Start watching for slow disk reads and writes after the heavy initializing is done if (BuildConfig.DEBUG) { -// StrictMode.setThreadPolicy( -// new StrictMode.ThreadPolicy.Builder() -// .detectCustomSlowCalls() -// .detectNetwork() -// .detectDiskReads() -// .detectDiskWrites() -// .penaltyLog() -// .build()); -// StrictMode.setVmPolicy( -// new StrictMode.VmPolicy.Builder() -// .detectAll() -// .penaltyLog() -// .build()); + StrictMode.setThreadPolicy( + new StrictMode.ThreadPolicy.Builder() + .detectCustomSlowCalls() + .detectNetwork() + .detectDiskReads() + .detectDiskWrites() + .penaltyLog() + .build()); + StrictMode.setVmPolicy( + new StrictMode.VmPolicy.Builder() + .detectAll() + .penaltyLog() + .build()); } RxJavaPlugins.setErrorHandler(e -> { From 98f45e7579885e31b4cf6d362f1a105f97b3b5e6 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 1 Sep 2019 16:29:01 +0300 Subject: [PATCH 037/184] (#172) Attempt to implement migration from one local threads base directory to another Basically, when a user want to change a local threads directory to another one (let's say they had it on sd-card and then bought another one and want to use it from now on). This is kinda working but it's fucking hacky and some parts of it are really slow. Fixed a bug with save thread icon being set to the default state when it should be set to stopped state --- .../chan/core/cache/CacheHandler.java | 6 +- .../chan/core/cache/FileCache.java | 2 +- .../database/DatabaseSavedThreadManager.java | 10 ++ .../chan/core/image/ImageLoaderV2.java | 8 +- .../core/manager/SavedThreadLoaderManager.kt | 2 +- .../chan/core/saf/FileChooser.kt | 11 +- .../chan/core/saf/FileManager.kt | 120 +++++++++++++ .../chan/core/saf/file/AbstractFile.kt | 53 +++--- .../chan/core/saf/file/ExternalFile.kt | 75 +++++--- .../chan/core/saf/file/RawFile.kt | 28 +-- .../chan/core/saver/ImageSaveTask.java | 17 +- .../chan/core/saver/ImageSaver.java | 71 +++++--- .../ui/controller/ImageViewerController.java | 13 +- .../controller/MediaSettingsController.java | 165 ++++++++++-------- .../ui/controller/ViewThreadController.java | 39 ++++- Kuroba/app/src/main/res/values/strings.xml | 2 +- 16 files changed, 417 insertions(+), 205 deletions(-) diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/CacheHandler.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/CacheHandler.java index e554fcd389..cfee9aaf78 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/CacheHandler.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/CacheHandler.java @@ -137,10 +137,8 @@ public void clearCache() { @MainThread public void createDirectories() { - if (!cacheDirFile.exists()) { - if (!cacheDirFile.create()) { - Logger.e(TAG, "Unable to create file cache dir " + cacheDirFile.getFullPath()); - } + if (!cacheDirFile.exists() && !cacheDirFile.create()) { + throw new RuntimeException("Unable to create file cache dir " + cacheDirFile.getFullPath()); } } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/FileCache.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/FileCache.java index b23bfcdc75..bfce3b23f9 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/FileCache.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/FileCache.java @@ -78,7 +78,7 @@ public FileCacheDownloader downloadFile( AbstractFile baseDirFile = fileManager.newLocalThreadFile(); if (baseDirFile == null) { - Logger.e(TAG, "fileManager.newLocalThreadFile() returned null"); + Logger.e(TAG, "downloadFile() fileManager.newLocalThreadFile() returned null"); return null; } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/database/DatabaseSavedThreadManager.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/database/DatabaseSavedThreadManager.java index 13110d99b9..6301849633 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/database/DatabaseSavedThreadManager.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/database/DatabaseSavedThreadManager.java @@ -42,6 +42,16 @@ public Callable> getSavedThreads() { }; } + public Callable hasSavedThreads() { + return () -> { + SavedThread savedThread = helper.savedThreadDao + .queryBuilder() + .queryForFirst(); + + return savedThread != null; + }; + } + public Callable startSavingThread(final SavedThread savedThread) { return () -> { SavedThread prevSavedThread = getSavedThreadByLoadableId(savedThread.loadableId).call(); diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/image/ImageLoaderV2.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/image/ImageLoaderV2.java index d4447926a0..54af87cf51 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/image/ImageLoaderV2.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/image/ImageLoaderV2.java @@ -120,7 +120,7 @@ public ImageContainer getFromDisk( AbstractFile baseDirFile = fileManager.newLocalThreadFile(); if (baseDirFile == null) { - throw new IOException("fileManager.newLocalThreadFile() returned null"); + throw new IOException("getFromDisk() fileManager.newLocalThreadFile() returned null"); } String imageDir; @@ -168,9 +168,11 @@ public ImageContainer getFromDisk( mainThreadHandler.post(() -> { container.setBitmap(bitmap); container.setRequestUrl(imageDir); + if (container.getListener() != null) { - container.getListener().onResponse(container, true); - }}); + container.getListener().onResponse(container, true); + } + }); } } catch (Exception e) { String message = "Could not get an image from the disk, error message = " diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/SavedThreadLoaderManager.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/SavedThreadLoaderManager.kt index cba5bc377f..420d7a5b90 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/SavedThreadLoaderManager.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/SavedThreadLoaderManager.kt @@ -23,7 +23,7 @@ constructor( val threadSubDir = ThreadSaveManager.getThreadSubDir(loadable) val baseDir = fileManager.newLocalThreadFile() if (baseDir == null) { - Logger.e(TAG, "fileManager.newLocalThreadFile() returned null") + Logger.e(TAG, "loadSavedThread() fileManager.newLocalThreadFile() returned null") return null } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileChooser.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileChooser.kt index 38153b86cf..4b9d7e191e 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileChooser.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileChooser.kt @@ -31,11 +31,12 @@ internal class FileChooser( internal fun openChooseDirectoryDialog(directoryChooserCallback: DirectoryChooserCallback) { startActivityCallbacks?.let { callbacks -> val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + intent.putExtra("android.content.extra.SHOW_ADVANCED", true) + intent.addFlags( Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION or - Intent.FLAG_GRANT_WRITE_URI_PERMISSION or - Intent.FLAG_GRANT_PREFIX_URI_PERMISSION + Intent.FLAG_GRANT_WRITE_URI_PERMISSION ) val nextRequestCode = ++requestCode @@ -45,7 +46,8 @@ internal class FileChooser( callbacks.myStartActivityForResult(intent, nextRequestCode) } catch (e: Exception) { callbacksMap.remove(nextRequestCode) - directoryChooserCallback.onCancel(e.message ?: "openChooseDirectoryDialog() Unknown error") + directoryChooserCallback.onCancel(e.message + ?: "openChooseDirectoryDialog() Unknown error") } } } @@ -80,7 +82,8 @@ internal class FileChooser( startActivityCallbacks?.let { callbacks -> val intent = Intent(Intent.ACTION_CREATE_DOCUMENT) intent.addFlags( - Intent.FLAG_GRANT_READ_URI_PERMISSION or + Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or + Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION ) diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt index cc84140978..800669460b 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.Intent import android.net.Uri import android.provider.DocumentsContract +import android.util.Log import androidx.documentfile.provider.DocumentFile import com.github.adamantcheese.chan.core.saf.callback.DirectoryChooserCallback import com.github.adamantcheese.chan.core.saf.callback.FileChooserCallback @@ -19,6 +20,7 @@ import java.io.File import java.io.IOException import java.lang.IllegalStateException import java.lang.RuntimeException +import java.util.* class FileManager( private val appContext: Context @@ -210,6 +212,124 @@ class FileManager( } } + // TODO: maybe it is a better idea to not copy old local threads when changing base directory + // at all? + /** + * VERY SLOW!!! DO NOT EVEN THINK RUNNING THIS ON THE MAIN THREAD!!! + * */ + fun copyDirectoryWithContent(sourceDir: AbstractFile, destDir: AbstractFile): Boolean { + if (!sourceDir.exists()) { + Logger.e(TAG, "Source directory does not exists, path = ${sourceDir.getFullPath()}") + return false + } + + if (sourceDir.listFiles().isEmpty()) { + Logger.d(TAG, "Source directory is empty, nothing to copy") + return true + } + + if (!destDir.exists()) { + Logger.e(TAG, "Destination directory does not exists, path = ${sourceDir.getFullPath()}") + return false + } + + if (!sourceDir.isDirectory()) { + Logger.e(TAG, "Source directory is not a directory, path = ${sourceDir.getFullPath()}") + return false + } + + if (!destDir.isDirectory()) { + Logger.e(TAG, "Destination directory is not a directory, path = ${destDir.getFullPath()}") + return false + } + + val queue = LinkedList() + val files = mutableListOf() + queue.offer(sourceDir) + + // Collect all of the inner files in the source directory + while (queue.isNotEmpty()) { + val file = queue.poll() + if (file.isDirectory()) { + file.listFiles().forEach { queue.offer(it) } + } else { + files.add(file) + } + } + + val prefix = sourceDir.getFullPath() + + for (file in files) { + // Holy shit this hack is so fucking disgusting and may break literally any minute. + // If this shit breaks then blame google for providing such a retarded fucking API. + + // Basically we have a directory, let's say /123 and we want to copy all + // of it's files into /456. So we collect every file in /123 then we iterate every + // collected file, remove base directory prefix (/123 in this case) and recreate this + // file with the same directory structure in another base directory (/456 in this case). + // Let's was we have the following files: + // + // /123/1.txt + // /123/111/2.txt + // /123/222/3.txt + // + // After calling this method we will have these files copied into /456: + // + // /456/1.txt + // /456/111/2.txt + // /456/222/3.txt + // + val fileInNewDirectory = newLocalThreadFile() + ?.appendFileNameSegment(file.getFullPath().removePrefix(prefix)) + ?.createNew() + + if (fileInNewDirectory == null) { + Logger.e(TAG, "Couldn't create inner file with name ${file.getName()}") + return false + } + + if (!copyFileContents(file, fileInNewDirectory)) { + Logger.e(TAG, "Couldn't copy one file into another") + return false + } + } + + return true + } + + fun forgetSAFTree(directory: AbstractFile): Boolean { + if (directory !is ExternalFile) { + // Only ExternalFile is being used with SAF + return true + } + + val uri = Uri.parse(directory.getFullPath()) + + if (!directory.exists()) { + Logger.e(TAG, "Couldn't revoke permissions from directory because it does not exist, path = $uri") + return false + } + + if (!directory.isDirectory()) { + Logger.e(TAG, "Couldn't revoke permissions from directory it is not a directory, path = $uri") + return false + } + + return try { + val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or + Intent.FLAG_GRANT_WRITE_URI_PERMISSION + + appContext.contentResolver.releasePersistableUriPermission(uri, flags) + appContext.revokeUriPermission(uri, flags) + + Logger.d(TAG, "Revoke old path permissions success on $uri") + true + } catch (err: Exception) { + Logger.e(TAG, "Error revoking old path permissions on $uri", err) + false + } + } + private fun toDocumentFile(uri: Uri): DocumentFile? { if (!DocumentFile.isDocumentUri(appContext, uri)) { Logger.e(TAG, "Not a DocumentFile, uri = $uri") diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt index 675a31ddd6..6e2f6a9f5c 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt @@ -153,9 +153,6 @@ abstract class AbstractFile( @ImmutableMethod abstract fun canWrite(): Boolean - @MutableMethod - abstract fun getParent(): AbstractFile? - @ImmutableMethod abstract fun getFullPath(): String @@ -188,41 +185,21 @@ abstract class AbstractFile( @ImmutableMethod abstract fun lastModified(): Long - /** - * Removes the last appended segment if there are any - * e.g: /test/123/test2 -> /test/123 -> /test - * */ - @MutableMethod - fun removeLastSegment(): Boolean { - if (segments.isEmpty()) { - return false - } - - segments.removeAt(segments.lastIndex) - return true - } - - protected fun appendSubDirSegmentInner(name: String): AbstractFile { check(!isFilenameAppended()) { "Cannot append anything after file name has been appended" } require(!name.isBlank()) { "Bad name: $name" } - require(name.extension() == null) { - "Directory name must not contain extension, extension = ${name.extension()}" - } - val nameList = if (name.contains(File.separatorChar)) { - name.split(File.separatorChar) + val nameList = if (name.contains(File.separatorChar) || name.contains(ENCODED_SEPARATOR)) { + name + // First of all split by the "/" symbol + .split(File.separatorChar) + // Then try to split every part again by this time by the "%2F" symbol + .flatMap { names -> names.split(ENCODED_SEPARATOR) } } else { listOf(name) } nameList - .onEach { splitName -> - require(splitName.extension() == null) { - "appendSubDirSegment does not allow segments with extensions! " + - "bad name = $splitName" - } - } .map { splitName -> Segment(splitName) } .forEach { segment -> segments += segment } @@ -233,8 +210,12 @@ abstract class AbstractFile( check(!isFilenameAppended()) { "Cannot append anything after file name has been appended" } require(!name.isBlank()) { "Bad name: $name" } - val nameList = if (name.contains(File.separatorChar)) { - val split = name.split(File.separatorChar) + val nameList = if (name.contains(File.separatorChar) || name.contains(ENCODED_SEPARATOR)) { + val split = name + // First of all split by the "/" symbol + .split(File.separatorChar) + // Then try to split every part again by this time by the "%2F" symbol + .flatMap { names -> names.split(ENCODED_SEPARATOR) } check(split.size >= 2) { "Should have at least two entries, name = $name" } split @@ -259,6 +240,10 @@ abstract class AbstractFile( private fun isFilenameAppended(): Boolean = segments.lastOrNull()?.isFileName ?: false + override fun toString(): String { + return getFullPath() + } + /** * We can have the root to be a directory or a file. * If it's a directory, that means that we can append sub directories to it. @@ -288,8 +273,6 @@ abstract class AbstractFile( * /test/123/test2 * or * /test/123/test2/5/6/7/8/112233 - * - * Cannot have an extension! * */ class DirRoot(holder: T) : Root(holder) @@ -312,4 +295,8 @@ abstract class AbstractFile( val name: String, val isFileName: Boolean = false ) + + companion object { + const val ENCODED_SEPARATOR = "%2F" + } } \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt index aa9008d71f..5b4da7d8b9 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt @@ -3,6 +3,9 @@ package com.github.adamantcheese.chan.core.saf.file import android.content.Context import android.net.Uri import android.os.ParcelFileDescriptor +import android.provider.DocumentsContract +import android.provider.DocumentsContract.Document.COLUMN_DISPLAY_NAME +import android.provider.DocumentsContract.Document.COLUMN_DOCUMENT_ID import android.webkit.MimeTypeMap import androidx.documentfile.provider.DocumentFile import com.github.adamantcheese.chan.core.appendManyEncoded @@ -11,6 +14,7 @@ import com.github.adamantcheese.chan.utils.Logger import java.io.FileDescriptor import java.io.InputStream import java.io.OutputStream +import java.util.* class ExternalFile( private val appContext: Context, @@ -31,7 +35,6 @@ class ExternalFile( override fun createNew(): ExternalFile? { check(root !is Root.FileRoot) { - // TODO: do we need this check? "root is already FileRoot, cannot append anything anymore" } @@ -56,14 +59,14 @@ class ExternalFile( if (!segment.isFileName) { newFile = file.createDirectory(segment.name) if (newFile == null) { - Logger.e(TAG, "file.createDirectory returned null, file.uri = ${file.uri}, " + + Logger.e(TAG, "createNew() file.createDirectory() returned null, file.uri = ${file.uri}, " + "segment.name = ${segment.name}") return null } } else { newFile = file.createFile(mimeTypeMap.getMimeFromFilename(segment.name), segment.name) if (newFile == null) { - Logger.e(TAG, "file.createFile returned null, file.uri = ${file.uri}, " + + Logger.e(TAG, "createNew() file.createFile returned null, file.uri = ${file.uri}, " + "segment.name = ${segment.name}") return null } @@ -107,27 +110,8 @@ class ExternalFile( override fun canRead(): Boolean = clone().toDocumentFile()?.canRead() ?: false override fun canWrite(): Boolean = clone().toDocumentFile()?.canWrite() ?: false - override fun getParent(): ExternalFile? { - if (segments.isNotEmpty()) { - removeLastSegment() - return this - } - - val parent = when (root) { - is Root.DirRoot -> root.holder.parentFile - is Root.FileRoot -> root.holder.parentFile - } - - if (parent == null) { - Logger.e(TAG, "getParent() parentUri == null") - return null - } - - return ExternalFile(appContext, Root.DirRoot(parent)) - } - override fun getFullPath(): String { - return Uri.parse(root.holder.toString()).buildUpon() + return Uri.parse(root.holder.uri.toString()).buildUpon() .appendManyEncoded(segments.map { segment -> segment.name }) .build() .toString() @@ -298,9 +282,7 @@ class ExternalFile( for (i in 0 until segments.size) { val segment = segments[i] - val file = documentFile.listFiles() - .firstOrNull { file -> file.name == segment.name } - + val file = fastFindFile(documentFile, segment) if (file == null) { break } @@ -316,6 +298,47 @@ class ExternalFile( return documentFile } + private fun fastFindFile(root: DocumentFile, segment: Segment): DocumentFile? { + val name = 0 + val documentId = 1 + val selection = "$COLUMN_DISPLAY_NAME = ?" + val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree( + root.uri, + DocumentsContract.getDocumentId(root.uri)) + val projection = arrayOf(COLUMN_DISPLAY_NAME, COLUMN_DOCUMENT_ID) + val contentResolver = appContext.contentResolver + val lowerCaseFilename = segment.name.toLowerCase(Locale.US) + + return contentResolver.query( + childrenUri, + projection, + selection, + arrayOf(lowerCaseFilename), + null + )?.use { cursor -> + while (cursor.moveToNext()) { + if (cursor.isNull(name)) { + continue + } + + val foundFileName = cursor.getString(name) + ?: continue + + if (!foundFileName.toLowerCase(Locale.US).startsWith(lowerCaseFilename)) { + continue + } + + val uri = DocumentsContract.buildDocumentUriUsingTree( + root.uri, + cursor.getString(documentId)) + + return@use DocumentFile.fromSingleUri(appContext, uri) + } + + return@use null + } + } + private fun createDocumentFileFromUri(uri: Uri, index: Int): DocumentFile? { val builder = uri.buildUpon() diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt index d1ae4f63fe..3b704aa5f7 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt @@ -21,14 +21,25 @@ class RawFile( override fun createNew(): RawFile? { check(root !is Root.FileRoot) { - // TODO: do we need this check? "root is already FileRoot, cannot append anything anymore" } if (segments.isEmpty()) { - // Root is probably already existing and there is no point in creating it again so just - // return null here - return null + if (!root.holder.exists()) { + if (root.holder.isFile) { + if (!root.holder.createNewFile()) { + Logger.e(TAG, "Couldn't create file, path = ${root.holder.absolutePath}") + return null + } + } else { + if (!root.holder.mkdirs()) { + Logger.e(TAG, "Couldn't create directory, path = ${root.holder.absolutePath}") + return null + } + } + } + + return this } var newFile = root.holder @@ -66,15 +77,6 @@ class RawFile( override fun canRead(): Boolean = clone().toFile().canRead() override fun canWrite(): Boolean = clone().toFile().canWrite() - override fun getParent(): RawFile? { - if (segments.isNotEmpty()) { - removeLastSegment() - return this - } - - return RawFile(Root.DirRoot(root.holder.parentFile)) - } - override fun getFullPath(): String { return File(root.holder.absolutePath) .appendMany(segments.map { segment -> segment.name }) diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaveTask.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaveTask.java index c2e3f82116..c524904264 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaveTask.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaveTask.java @@ -151,16 +151,17 @@ private boolean copyToDestination(File source) { throw new IOException("Could not create destination file, path = " + destination.getFullPath()); } + // TODO: check whether this change broke something // If destination is a raw file then we need to check whether the parent directory exists. // Otherwise we don't - if (createdDestinationFile instanceof RawFile) { - AbstractFile parent = createdDestinationFile - .clone() // TODO: do we need to clone this file? - .getParent(); - if (parent == null || (!parent.create() && !parent.isDirectory())) { - throw new IOException("Could not create parent directory"); - } - } +// if (createdDestinationFile instanceof RawFile) { +// AbstractFile parent = createdDestinationFile +// .clone() // TODO: do we need to clone this file? +// .getParent(); +// if (parent == null || (!parent.create() && !parent.isDirectory())) { +// throw new IOException("Could not create parent directory"); +// } +// } if (createdDestinationFile.isDirectory()) { throw new IOException("Destination file is already a directory"); diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaver.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaver.java index da3fbf94b2..c135a971db 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaver.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaver.java @@ -62,10 +62,32 @@ public ImageSaver(FileManager fileManager) { EventBus.getDefault().register(this); } - public boolean startDownloadTask(Context context, final ImageSaveTask task) { + public void startDownloadTask( + Context context, + final ImageSaveTask task, + DownloadTaskCallbacks callbacks) { + if (hasPermission(context)) { + startDownloadTaskInternal(task, callbacks); + return; + } + + requestPermission(context, granted -> { + if (!granted) { + callbacks.onError("Cannot start saving images without WRITE permission"); + return; + } + + startDownloadTaskInternal(task, callbacks); + }); + } + + private void startDownloadTaskInternal( + ImageSaveTask task, + DownloadTaskCallbacks callbacks) { AbstractFile saveLocation = getSaveLocation(task); if (saveLocation == null) { - return false; + callbacks.onError("Couldn't figure out save location"); + return; } PostImage postImage = task.getPostImage(); @@ -91,23 +113,9 @@ public boolean startDownloadTask(Context context, final ImageSaveTask task) { task.setDestination(saveFile); - if (hasPermission(context)) { - startTask(task); - updateNotification(); - } else { - // This does not request the permission when another request is pending. - // This is ok and will drop the task. - requestPermission(context, granted -> { - if (granted) { - startTask(task); - updateNotification(); - } else { - showToast(null, false, false); - } - }); - } - - return true; + // At this point we already have disk permissions + startTask(task); + updateNotification(); } public boolean startBundledTask(Context context, final String subFolder, final List tasks) { @@ -139,23 +147,28 @@ public String getSubFolder(String name) { @Nullable public AbstractFile getSaveLocation(ImageSaveTask task) { - if (!fileManager.baseSaveLocalDirectoryExists()) { - Logger.e(TAG, "Base save local directory does not exist"); + AbstractFile baseSaveDir = fileManager.newSaveLocationFile(); + if (baseSaveDir == null) { + Logger.e(TAG, "getSaveLocation() fileManager.newSaveLocationFile() returned null"); return null; } - AbstractFile destination = fileManager.newSaveLocationFile(); - if (destination == null) { - Logger.e(TAG, "getSaveLocation() fileManager.newSaveLocationFile() returned null"); + if (!baseSaveDir.exists() && !baseSaveDir.create()) { + Logger.e(TAG, "Couldn't create base image save directory"); + return null; + } + + if (!fileManager.baseSaveLocalDirectoryExists()) { + Logger.e(TAG, "Base save local directory does not exist"); return null; } String subFolder = task.getSubFolder(); if (subFolder != null) { - destination.appendSubDirSegment(subFolder); + baseSaveDir.appendSubDirSegment(subFolder); } - return destination; + return baseSaveDir; } @Override @@ -192,7 +205,7 @@ private boolean startBundledTaskInternal(String subFolder, List t AbstractFile saveLocation = getSaveLocation(task); if (saveLocation == null) { - Logger.e(TAG, "getSaveLocation() returned null"); + Logger.e(TAG, "startBundledTaskInternal() getSaveLocation() returned null"); allSuccess = false; continue; } @@ -288,4 +301,8 @@ private boolean hasPermission(Context context) { private void requestPermission(Context context, RuntimePermissionsHelper.Callback callback) { ((StartActivity) context).getRuntimePermissionsHelper().requestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, callback); } + + public interface DownloadTaskCallbacks { + void onError(String message); + } } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ImageViewerController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ImageViewerController.java index 25e01b0a08..c153c5dbe0 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ImageViewerController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ImageViewerController.java @@ -78,6 +78,7 @@ import java.io.File; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import javax.inject.Inject; @@ -308,9 +309,15 @@ private void saveShare(boolean share, PostImage postImage) { task.setSubFolder(subFolderName); } - if (!imageSaver.startDownloadTask(context, task)) { - Toast.makeText(context, "Couldn't start download task", Toast.LENGTH_LONG).show(); - } + imageSaver.startDownloadTask(context, task, message -> { + String errorMessage = String.format( + Locale.US, + "%s, error message = %s", + "Couldn't start download task", + message); + + Toast.makeText(context, errorMessage, Toast.LENGTH_LONG).show(); + }); } } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java index 39e8a66bda..6a785084dd 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java @@ -21,9 +21,10 @@ import android.net.Uri; import android.widget.Toast; +import androidx.documentfile.provider.DocumentFile; + import com.github.adamantcheese.chan.R; import com.github.adamantcheese.chan.core.database.DatabaseManager; -import com.github.adamantcheese.chan.core.model.orm.SavedThread; import com.github.adamantcheese.chan.core.saf.FileManager; import com.github.adamantcheese.chan.core.saf.callback.DirectoryChooserCallback; import com.github.adamantcheese.chan.core.saf.file.AbstractFile; @@ -41,7 +42,9 @@ import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; @@ -49,6 +52,8 @@ import java.io.OutputStreamWriter; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; import javax.inject.Inject; @@ -56,6 +61,7 @@ import static com.github.adamantcheese.chan.Chan.inject; import static com.github.adamantcheese.chan.utils.AndroidUtils.getString; +import static com.github.adamantcheese.chan.utils.AndroidUtils.runOnUiThread; public class MediaSettingsController extends SettingsController { private static final String TAG = "MediaSettingsController"; @@ -68,6 +74,8 @@ public class MediaSettingsController extends SettingsController { private ListSettingView imageAutoLoadView; private ListSettingView videoAutoLoadView; + private Executor fileCopyingExecutor = Executors.newSingleThreadExecutor(); + @Inject FileManager fileManager; @@ -250,15 +258,6 @@ private String getSaveLocation() { } private void showUseSAFOrOldAPIForLocalThreadsLocationDialog() { - if (hasActiveDownloadingThreads()) { - // I don't even want to imagine what's going to happen if we allow this - Toast.makeText( - context, - R.string.media_settings_cannot_switch_local_threads_base_dir_message, - Toast.LENGTH_LONG).show(); - return; - } - AlertDialog alertDialog = new AlertDialog.Builder(context) .setTitle(R.string.use_saf_for_local_threads_location_dialog_title) .setMessage(R.string.use_saf_for_local_threads_location_dialog_message) @@ -273,19 +272,6 @@ private void showUseSAFOrOldAPIForLocalThreadsLocationDialog() { alertDialog.show(); } - private boolean hasActiveDownloadingThreads() { - List savedThreads = databaseManager.runTask( - databaseManager.getDatabaseSavedThreadManager().getSavedThreads()); - - for (SavedThread savedThread : savedThreads) { - if (savedThread.isRunning()) { - return true; - } - } - - return false; - } - /** * Select a directory where local threads will be stored via the old Java File API */ @@ -294,12 +280,19 @@ private void onLocalThreadsLocationUseOldApiClicked() { context, SaveLocationController.SaveLocationControllerMode.LocalThreadsSaveLocation, dirPath -> { + AbstractFile oldLocalThreadsDirectory = fileManager.newLocalThreadFile(); + Logger.d(TAG, "SaveLocationController with LocalThreadsSaveLocation mode returned dir " + dirPath); // Supa hack to get the callback called ChanSettings.localThreadLocation.setSync(""); ChanSettings.localThreadLocation.setSync(dirPath); + + AbstractFile newLocalThreadsDirectory = fileManager.newLocalThreadFile(); + askUserIfTheyWantToMoveOldThreadsToTheNewDirectory( + oldLocalThreadsDirectory, + newLocalThreadsDirectory); }); navigationController.pushController(saveLocationController); @@ -312,11 +305,18 @@ private void onLocalThreadsLocationUseSAFClicked() { fileManager.openChooseDirectoryDialog(new DirectoryChooserCallback() { @Override public void onResult(@NotNull Uri uri) { - ChanSettings.localThreadsLocationUri.set(uri.toString()); + AbstractFile oldLocalThreadsDirectory = fileManager.newLocalThreadFile(); + ChanSettings.localThreadsLocationUri.set(uri.toString()); String defaultDir = ChanSettings.getDefaultLocalThreadsLocation(); + ChanSettings.localThreadLocation.setNoUpdate(defaultDir); localThreadsLocation.setDescription(uri.toString()); + + AbstractFile newLocalThreadsDirectory = fileManager.newLocalThreadFile(); + askUserIfTheyWantToMoveOldThreadsToTheNewDirectory( + oldLocalThreadsDirectory, + newLocalThreadsDirectory); } @Override @@ -326,6 +326,74 @@ public void onCancel(@NotNull String reason) { }); } + private void askUserIfTheyWantToMoveOldThreadsToTheNewDirectory( + AbstractFile oldLocalThreadsDirectory, + AbstractFile newLocalThreadsDirectory) { + + // TODO: strings + AlertDialog alertDialog = new AlertDialog.Builder(context) + .setTitle("Move old local threads to the new directory?") + .setMessage("This operation may take quite some time. Once started this operation shouldn't be canceled, otherwise something may break") + .setPositiveButton("Move", (dialog, which) -> { + moveOldFilesToTheNewDirectory(oldLocalThreadsDirectory, newLocalThreadsDirectory); + }) + .setNegativeButton("Do not move", (dialog, which) -> {}) + .create(); + + alertDialog.show(); + } + + private void moveOldFilesToTheNewDirectory( + @Nullable AbstractFile oldLocalThreadsDirectory, + @Nullable AbstractFile newLocalThreadsDirectory) { + if (oldLocalThreadsDirectory == null || newLocalThreadsDirectory == null) { + Logger.e(TAG, "One of the directories is null, cannot copy " + + "(oldLocalThreadsDirectory is null == " + (oldLocalThreadsDirectory == null) + ")" + + ", newLocalThreadsDirectory is null == " + (newLocalThreadsDirectory == null) + ")"); + return; + } + + Logger.d(TAG, "oldLocalThreadsDirectory = " + oldLocalThreadsDirectory.getFullPath() + + ", newLocalThreadsDirectory = " + newLocalThreadsDirectory.getFullPath()); + + navigationController.pushController(new LoadingViewController(context, true)); + + fileCopyingExecutor.execute(() -> { + boolean result = fileManager.copyDirectoryWithContent( + oldLocalThreadsDirectory, + newLocalThreadsDirectory); + + runOnUiThread(() -> { + navigationController.popController(); + + if (!result) { + // TODO: strings + Toast.makeText( + context, + "Could not copy one directory's file into another one", + Toast.LENGTH_LONG + ).show(); + } else { + if (!fileManager.forgetSAFTree(oldLocalThreadsDirectory)) { + // TODO: strings + Toast.makeText( + context, + "Files were copied but couldn't remove SAF permissions from the old directory", + Toast.LENGTH_SHORT + ).show(); + } else { + // TODO: strings + Toast.makeText( + context, + "Successfully copied files", + Toast.LENGTH_LONG + ).show(); + } + } + }); + }); + } + private void showUseSAFOrOldAPIForSaveLocationDialog() { AlertDialog alertDialog = new AlertDialog.Builder(context) .setTitle(R.string.use_saf_for_save_location_dialog_title) @@ -373,7 +441,6 @@ public void onResult(@NotNull Uri uri) { ChanSettings.saveLocation.setNoUpdate(defaultDir); saveLocation.setDescription(uri.toString()); - testMethod(uri); } @Override @@ -421,41 +488,6 @@ private void testMethod(@NotNull Uri uri) { if (!externalFile.delete() && externalFile.exists()) { throw new RuntimeException("Couldn't delete test123.txt"); } - - AbstractFile parent1 = externalFile.getParent(); - if (!parent1.getName().equals("789")) { - throw new RuntimeException("Parent1.name != 789, name = " + parent1.getName()); - } - - if (parent1.isFile()) { - throw new RuntimeException("789 is a file"); - } - - if (!parent1.isDirectory()) { - throw new RuntimeException("789 is not a directory"); - } - - if (!parent1.delete() && parent1.exists()) { - throw new RuntimeException("Couldn't delete 789"); - } - - AbstractFile parent2 = parent1.getParent(); - if (!parent2.getName().equals("456")) { - throw new RuntimeException("Parent1.name != 456, name = " + parent2.getName()); - } - - if (!parent2.delete() && parent2.exists()) { - throw new RuntimeException("Couldn't delete 456"); - } - - AbstractFile parent3 = parent2.getParent(); - if (!parent3.getName().equals("123")) { - throw new RuntimeException("Parent1.name != 123, name = " + parent3.getName()); - } - - if (!parent3.delete() && parent3.exists()) { - throw new RuntimeException("Couldn't delete 123"); - } } { @@ -575,15 +607,6 @@ private void testMethod(@NotNull Uri uri) { return Unit.INSTANCE; }); - - AbstractFile parent = externalFile.getParent().getParent(); - if (!parent.getName().equals("1234")) { - throw new RuntimeException("dir.name != 1234, name = " + parent.getName()); - } - - if (!parent.delete() && parent.exists()) { - throw new RuntimeException("Couldn't delete /1234/4566/filename.json"); - } } { @@ -591,10 +614,6 @@ private void testMethod(@NotNull Uri uri) { if (!externalFile.getName().equals("Test")) { throw new RuntimeException("externalFile.name != Test, name = " + externalFile.getName()); } - - if (externalFile.getParent() != null) { - throw new RuntimeException("Root directory parent is not null!"); - } } System.out.println("All tests passed!"); diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ViewThreadController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ViewThreadController.java index 967760199a..4c719aa5f7 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ViewThreadController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ViewThreadController.java @@ -45,6 +45,7 @@ import com.github.adamantcheese.chan.core.model.orm.SavedThread; import com.github.adamantcheese.chan.core.presenter.ThreadPresenter; import com.github.adamantcheese.chan.core.saf.FileManager; +import com.github.adamantcheese.chan.core.saf.file.AbstractFile; import com.github.adamantcheese.chan.core.settings.ChanSettings; import com.github.adamantcheese.chan.ui.helper.HintPopup; import com.github.adamantcheese.chan.ui.helper.RuntimePermissionsHelper; @@ -206,12 +207,6 @@ private void saveClicked(ToolbarMenuItem item) { return; } - if (!fileManager.baseLocalThreadsDirectoryExists()) { - Logger.e(TAG, "Base local threads directory does not exist"); - Toast.makeText(context, R.string.base_local_threads_dir_not_exists, Toast.LENGTH_LONG).show(); - return; - } - RuntimePermissionsHelper runtimePermissionsHelper = ((StartActivity) context).getRuntimePermissionsHelper(); if (runtimePermissionsHelper.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { saveClickedInternal(); @@ -231,6 +226,34 @@ private void saveClicked(ToolbarMenuItem item) { } private void saveClickedInternal() { + AbstractFile baseLocalThreadsDir = fileManager.newLocalThreadFile(); + if (baseLocalThreadsDir == null) { + Logger.e(TAG, "saveClickedInternal() fileManager.newLocalThreadFile() returned null"); + Toast.makeText( + context, + R.string.base_local_threads_dir_not_exists, + Toast.LENGTH_LONG).show(); + return; + } + + if (!baseLocalThreadsDir.exists() && !baseLocalThreadsDir.create()) { + Logger.e(TAG, "saveClickedInternal() Couldn't create baseLocalThreadsDir"); + Toast.makeText( + context, + R.string.could_not_create_base_local_threads_dir, + Toast.LENGTH_LONG).show(); + return; + } + + if (!fileManager.baseLocalThreadsDirectoryExists()) { + Logger.e(TAG, "Base local threads directory does not exist"); + Toast.makeText( + context, + R.string.base_local_threads_dir_not_exists, + Toast.LENGTH_LONG).show(); + return; + } + if (threadLayout.getPresenter().save()) { updateDrawerHighlighting(loadable); populateLocalOrLiveVersionMenu(); @@ -648,11 +671,11 @@ private DownloadThreadState getThreadDownloadState() { } SavedThread savedThread = watchManager.findSavedThreadByLoadableId(pin.loadable.id); - if (savedThread == null || savedThread.isStopped) { + if (savedThread == null) { return DownloadThreadState.Default; } - if (savedThread.isFullyDownloaded) { + if (savedThread.isFullyDownloaded || savedThread.isStopped) { return DownloadThreadState.FullyDownloaded; } diff --git a/Kuroba/app/src/main/res/values/strings.xml b/Kuroba/app/src/main/res/values/strings.xml index c47984ea32..ae66c11fc5 100644 --- a/Kuroba/app/src/main/res/values/strings.xml +++ b/Kuroba/app/src/main/res/values/strings.xml @@ -704,7 +704,7 @@ Don't have a 4chan Pass?
Create new Local threads location Local threads location]]> - Cannot switch local threads base directory when there is at least one thread being downloaded. Stop all downloadings (or delete threads) and then try again. Base local threads directory does not exist (or it was deleted). You need to manually set it again in Media settings. Could not save one or more images + Could not create base local threads directory From 177412e24384dad838b6637c49273b3a2cdc8088 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 8 Sep 2019 19:27:51 +0300 Subject: [PATCH 038/184] Update travis.yml Add test python script for uploading built apk to a server --- .travis.yml | 40 ++++++++----- Kuroba/app/build.gradle | 13 +++++ Kuroba/main.py | 108 +++++++++++++++++++++++++++++++++++ Kuroba/scripts/update-apk.sh | 19 ------ 4 files changed, 146 insertions(+), 34 deletions(-) create mode 100644 Kuroba/main.py delete mode 100644 Kuroba/scripts/update-apk.sh diff --git a/.travis.yml b/.travis.yml index 03d5ca78c3..583f65d71c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,18 +1,28 @@ sudo: false -language: android -jdk: - - oraclejdk8 -before_install: - - cd Kuroba && chmod +x gradlew -android: - components: - - platform-tools - - tools - - extra-android-m2repository - - build-tools-28.0.3 - - android-28 -script: ./gradlew build --console plain -x lint +matrix: + include: + - language: android + jdk: + - oraclejdk8 + before_install: + - cd Kuroba && chmod +x gradlew + android: + components: + - platform-tools + - tools + - extra-android-m2repository + - build-tools-28.0.3 + - android-28 + script: + - ./gradlew build --console plain -x lint -after_success: - - bash scripts/update-apk.sh \ No newline at end of file + include: + - language: python + python: 3.6 + install: + - pip install requests + script: + - ls + - cd Kuroba + - python main.py 1234567890 401010 Kuroba\\app\\build\\outputs\\apk\\debug\\Kuroba.apk diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index e75e9c7536..1aabf18dae 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -1,3 +1,5 @@ +import java.util.concurrent.TimeUnit + apply plugin: 'com.android.application' android { @@ -184,3 +186,14 @@ dependencies { implementation "io.reactivex.rxjava2:rxjava:2.2.9" implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' } + +task getCheckedOutGitCommitHash { + doFirst { + def command = "git log ${from}^..HEAD --pretty=format:\"%H - %an, %ar : %s\" --first-parent" + println command + + def task = command.execute() + task.waitFor(15, TimeUnit.SECONDS) + println task.text + } +} diff --git a/Kuroba/main.py b/Kuroba/main.py new file mode 100644 index 0000000000..e4ded7b8c3 --- /dev/null +++ b/Kuroba/main.py @@ -0,0 +1,108 @@ +import os +import sys +import requests +import subprocess + +testCommits = """6019ab13ed8dc814ffabbdc76902eb07e2332f30 - Dmitry, 6 weeks ago : Update README.md +a03149610f28aa241c8e06fe2614645a1a11724d - Dmitry, 6 weeks ago : Merge pull request #21 from K1rakishou/dev +abbf9ade1fad68373ab91638ac1e8c598327bda5 - k1rakishou, 7 weeks ago : Trigger CI build +725f752a1f902926c29b504ad0306411ef6bf20c - k1rakishou, 7 weeks ago : Trigger CI build +568197ca26f4916b9213df41d95232dc38bd8719 - k1rakishou, 7 weeks ago : Merge remote-tracking branch 'origin/multi-feature' into multi-feature +c12c6863d3f0ff01b5b7ea971157a54ce1c9e24e - k1rakishou, 7 weeks ago : CI apk uploading +ce880e86b32244c2396b30cf96218c012b82a625 - k1rakishou, 7 weeks ago : Introduce travis CI +1604d021b04f1e428fd2ced9d1b4d61bf27086db - Dmitry, 7 weeks ago : Update README.md +48ca131cfe189c3701b2581d8d4a9ba6ef9cf2a4 - Dmitry, 7 weeks ago : Update README.md +72270df91cd47932a0003c477060036c09e9da36 - Dmitry, 7 weeks ago : Update README.md +3fb60638f572a5062ca1c092e652287ee0abda72 - Dmitry, 7 weeks ago : Update README.md +a59c48553af80a4517827e9a730d8e946ad5b3c0 - Dmitry, 7 weeks ago : Update README.md + +""" + + +def getLatestCommitHash(): + response = requests.get('http://127.0.0.1:8080/latest_commit_hash') + if response.status_code != 200: + print("Error while trying to get latest commit hash from the server" + + ", response status = " + str(response.status_code) + + ", message = " + str(response.content)) + exit(-1) + + return response.content.decode("utf-8") + + +def uploadApk(headers, projectBaseDir, latestCommits): + inFile = open(projectBaseDir + "\\app\\build\\outputs\\apk\\debug\\Kuroba.apk", "rb") + try: + if not inFile.readable(): + print("Provided file is not readable, path = " + str(projectBaseDir)) + exit(-1) + + print(latestCommits) + + response = requests.post( + 'http://127.0.0.1:8080/upload', + files=dict(apk=inFile, latest_commits=latestCommits), + headers=headers) + + if response.status_code != 200: + print("Error while trying to upload file" + + ", response status = " + str(response.status_code) + + ", message = " + str(response.content)) + exit(-1) + + print("Successfully uploaded") + except Exception as e: + print("Unhandled exception: " + str(e)) + exit(-1) + finally: + inFile.close() + + +def getLatestCommitsFrom(projectBaseDir, latestCommitHash): + if (len(latestCommitHash) <= 0): + print("Latest commit hash is empty, should be okay") + + gradlewFileName = "gradlew" + + # FIXME: doesn't work on windows + if (os.name == 'nt'): + return testCommits + + # TODO: rename getCheckedOutGitCommitHash + arguments = [projectBaseDir + '\\' + gradlewFileName, '-Pfrom=' + latestCommitHash + ' getCheckedOutGitCommitHash'] + + result = subprocess.run(arguments, stdout=subprocess.PIPE) + stdoutText = str(result.stdout) + + print("text = " + stdoutText) + return stdoutText + + +if __name__ == '__main__': + # fXylnrM1UKQ3IKRmYRTPtYK3U0k0Icl3Z1cOakqr6JidAmfXwR1DY2ORyHV6Ggk10vkHT30cDrZsKX9zn0hpWIdAnuN6FQKfOXlbcTullzbusG8v2I5lbFSql7v1Ttf7 401010 F:\\projects\\android\\forked\\Kuroba\\Kuroba\\app\\build\\outputs\\apk\\debug\\Kuroba.apk + + # git log ${from}^..HEAD --pretty=format:\"%H - %an, %ar : %s\" --first-parent + # Run gradle task with commit parameter -> gradlew -Pfrom=a03149610f28aa241c8e06fe2614645a1a11724d getCheckedOutGitCommitHash + + args = len(sys.argv) + if args != 4: + print("Bad arguments count, should be 4 got " + str(args)) + exit(-1) + + print("secretKey = " + str(sys.argv[1])) + print("apkVersion = " + str(sys.argv[2])) + print("projectBaseDir = " + str(sys.argv[3])) + + headers = dict(SECRET_KEY=sys.argv[1], APK_VERSION=sys.argv[2]) + projectBaseDir = sys.argv[3] + + latestCommitHash = getLatestCommitHash() + latestCommits = getLatestCommitsFrom(projectBaseDir, latestCommitHash) + + if len(latestCommits) <= 0: + print("latestCommits is empty, nothing was commited to the project since last build so do nothing, " + "latestCommitHash = " + latestCommitHash) + exit(0) + + uploadApk(headers, projectBaseDir, latestCommits) + exit(0) diff --git a/Kuroba/scripts/update-apk.sh b/Kuroba/scripts/update-apk.sh deleted file mode 100644 index 72979d56d4..0000000000 --- a/Kuroba/scripts/update-apk.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash -git config --global user.name "Travis CI" -git config --global user.email "noreply+travis@fossasia.org" - -git clone --quiet --branch=apk https://github.com/K1rakishou/Kuroba.git apk > /dev/null -cd apk -\cp -r ../*/**.apk . -\cp -r ../debug/output.json debug-output.json -\cp -r ../release/output.json release-output.json - -git checkout --orphan temporary - -git add --all . -git commit -am "[Auto] Update Test Apk ($(date +%Y-%m-%d.%H:%M:%S))" - -git branch -D apk -git branch -m apk - -git push origin apk --force --quiet > /dev/null \ No newline at end of file From 33e0fa9d3816aa6632a0f966dcfcbb2c387dd424 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 8 Sep 2019 19:33:35 +0300 Subject: [PATCH 039/184] Update build script, trigger CI build --- .travis.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 583f65d71c..6732ca92f8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,15 +14,12 @@ matrix: - extra-android-m2repository - build-tools-28.0.3 - android-28 - script: - - ./gradlew build --console plain -x lint - include: - language: python python: 3.6 install: - pip install requests script: - - ls - cd Kuroba + - ./gradlew build --console plain -x lint - python main.py 1234567890 401010 Kuroba\\app\\build\\outputs\\apk\\debug\\Kuroba.apk From 260200b706d00f8152d557d6a8d7c6f6261dad36 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 8 Sep 2019 19:36:05 +0300 Subject: [PATCH 040/184] Make gradlew executable, trigger CI build --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 6732ca92f8..a9e1bd162f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,5 +21,6 @@ matrix: - pip install requests script: - cd Kuroba + - chmod +x ./gradlew build connectedCheck - ./gradlew build --console plain -x lint - python main.py 1234567890 401010 Kuroba\\app\\build\\outputs\\apk\\debug\\Kuroba.apk From 58ba49bb3c1e241bf57d010be9955931a098919b Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 8 Sep 2019 19:38:46 +0300 Subject: [PATCH 041/184] Fix gradle build command, trigger CI build --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index a9e1bd162f..9527801025 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,6 +21,5 @@ matrix: - pip install requests script: - cd Kuroba - - chmod +x ./gradlew build connectedCheck - - ./gradlew build --console plain -x lint + - chmod +x ./gradlew build --console plain -x lint - python main.py 1234567890 401010 Kuroba\\app\\build\\outputs\\apk\\debug\\Kuroba.apk From ddbee83b46268cbcc12daa75172476830296ea42 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 8 Sep 2019 19:41:04 +0300 Subject: [PATCH 042/184] Attempt to fix it again, trigger CI build, turn off email notifications --- .travis.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 9527801025..24fd8b8c7a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,5 +21,9 @@ matrix: - pip install requests script: - cd Kuroba - - chmod +x ./gradlew build --console plain -x lint + - chmod +x gradlew + - ./gradlew build --console plain -x lint - python main.py 1234567890 401010 Kuroba\\app\\build\\outputs\\apk\\debug\\Kuroba.apk + +notifications: + email: false \ No newline at end of file From 9200627aa42c18b63144980ee6d55b7eac22a17f Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 8 Sep 2019 20:01:04 +0300 Subject: [PATCH 043/184] Attempt to combine scripts into one, trigger CI build --- .travis.yml | 44 ++++++++++++++++++++------------------------ 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/.travis.yml b/.travis.yml index 24fd8b8c7a..f2acecff11 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,29 +1,25 @@ sudo: false -matrix: - include: - - language: android - jdk: - - oraclejdk8 - before_install: - - cd Kuroba && chmod +x gradlew - android: - components: - - platform-tools - - tools - - extra-android-m2repository - - build-tools-28.0.3 - - android-28 - include: - - language: python - python: 3.6 - install: - - pip install requests - script: - - cd Kuroba - - chmod +x gradlew - - ./gradlew build --console plain -x lint - - python main.py 1234567890 401010 Kuroba\\app\\build\\outputs\\apk\\debug\\Kuroba.apk +language: + - android + - python +jdk: + - oraclejdk8 +python: 3.6 +android: + components: + - platform-tools + - tools + - extra-android-m2repository + - build-tools-28.0.3 + - android-28 +install: + - pip install requests +script: + - cd Kuroba + - chmod +x gradlew + - ./gradlew build --console plain -x lint + - python main.py 1234567890 401010 Kuroba\\app\\build\\outputs\\apk\\debug\\Kuroba.apk notifications: email: false \ No newline at end of file From 974b8b35abb341a588e569b6da4db875d6537eb0 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 8 Sep 2019 20:18:33 +0300 Subject: [PATCH 044/184] Update upload_apk script, trigger CI build --- .travis.yml | 3 +- Kuroba/app/build.gradle | 2 +- Kuroba/main.py | 108 ---------------------------------------- Kuroba/upload_apk.py | 82 ++++++++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 110 deletions(-) delete mode 100644 Kuroba/main.py create mode 100644 Kuroba/upload_apk.py diff --git a/.travis.yml b/.travis.yml index f2acecff11..2e86f56299 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,12 +14,13 @@ android: - build-tools-28.0.3 - android-28 install: + - pip install --upgrade pip - pip install requests script: - cd Kuroba - chmod +x gradlew - ./gradlew build --console plain -x lint - - python main.py 1234567890 401010 Kuroba\\app\\build\\outputs\\apk\\debug\\Kuroba.apk + - python upload_apk.py 1234567890 401010 notifications: email: false \ No newline at end of file diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 1aabf18dae..b0d79a1cfe 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -187,7 +187,7 @@ dependencies { implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' } -task getCheckedOutGitCommitHash { +task getLastCommits { doFirst { def command = "git log ${from}^..HEAD --pretty=format:\"%H - %an, %ar : %s\" --first-parent" println command diff --git a/Kuroba/main.py b/Kuroba/main.py deleted file mode 100644 index e4ded7b8c3..0000000000 --- a/Kuroba/main.py +++ /dev/null @@ -1,108 +0,0 @@ -import os -import sys -import requests -import subprocess - -testCommits = """6019ab13ed8dc814ffabbdc76902eb07e2332f30 - Dmitry, 6 weeks ago : Update README.md -a03149610f28aa241c8e06fe2614645a1a11724d - Dmitry, 6 weeks ago : Merge pull request #21 from K1rakishou/dev -abbf9ade1fad68373ab91638ac1e8c598327bda5 - k1rakishou, 7 weeks ago : Trigger CI build -725f752a1f902926c29b504ad0306411ef6bf20c - k1rakishou, 7 weeks ago : Trigger CI build -568197ca26f4916b9213df41d95232dc38bd8719 - k1rakishou, 7 weeks ago : Merge remote-tracking branch 'origin/multi-feature' into multi-feature -c12c6863d3f0ff01b5b7ea971157a54ce1c9e24e - k1rakishou, 7 weeks ago : CI apk uploading -ce880e86b32244c2396b30cf96218c012b82a625 - k1rakishou, 7 weeks ago : Introduce travis CI -1604d021b04f1e428fd2ced9d1b4d61bf27086db - Dmitry, 7 weeks ago : Update README.md -48ca131cfe189c3701b2581d8d4a9ba6ef9cf2a4 - Dmitry, 7 weeks ago : Update README.md -72270df91cd47932a0003c477060036c09e9da36 - Dmitry, 7 weeks ago : Update README.md -3fb60638f572a5062ca1c092e652287ee0abda72 - Dmitry, 7 weeks ago : Update README.md -a59c48553af80a4517827e9a730d8e946ad5b3c0 - Dmitry, 7 weeks ago : Update README.md - -""" - - -def getLatestCommitHash(): - response = requests.get('http://127.0.0.1:8080/latest_commit_hash') - if response.status_code != 200: - print("Error while trying to get latest commit hash from the server" + - ", response status = " + str(response.status_code) + - ", message = " + str(response.content)) - exit(-1) - - return response.content.decode("utf-8") - - -def uploadApk(headers, projectBaseDir, latestCommits): - inFile = open(projectBaseDir + "\\app\\build\\outputs\\apk\\debug\\Kuroba.apk", "rb") - try: - if not inFile.readable(): - print("Provided file is not readable, path = " + str(projectBaseDir)) - exit(-1) - - print(latestCommits) - - response = requests.post( - 'http://127.0.0.1:8080/upload', - files=dict(apk=inFile, latest_commits=latestCommits), - headers=headers) - - if response.status_code != 200: - print("Error while trying to upload file" + - ", response status = " + str(response.status_code) + - ", message = " + str(response.content)) - exit(-1) - - print("Successfully uploaded") - except Exception as e: - print("Unhandled exception: " + str(e)) - exit(-1) - finally: - inFile.close() - - -def getLatestCommitsFrom(projectBaseDir, latestCommitHash): - if (len(latestCommitHash) <= 0): - print("Latest commit hash is empty, should be okay") - - gradlewFileName = "gradlew" - - # FIXME: doesn't work on windows - if (os.name == 'nt'): - return testCommits - - # TODO: rename getCheckedOutGitCommitHash - arguments = [projectBaseDir + '\\' + gradlewFileName, '-Pfrom=' + latestCommitHash + ' getCheckedOutGitCommitHash'] - - result = subprocess.run(arguments, stdout=subprocess.PIPE) - stdoutText = str(result.stdout) - - print("text = " + stdoutText) - return stdoutText - - -if __name__ == '__main__': - # fXylnrM1UKQ3IKRmYRTPtYK3U0k0Icl3Z1cOakqr6JidAmfXwR1DY2ORyHV6Ggk10vkHT30cDrZsKX9zn0hpWIdAnuN6FQKfOXlbcTullzbusG8v2I5lbFSql7v1Ttf7 401010 F:\\projects\\android\\forked\\Kuroba\\Kuroba\\app\\build\\outputs\\apk\\debug\\Kuroba.apk - - # git log ${from}^..HEAD --pretty=format:\"%H - %an, %ar : %s\" --first-parent - # Run gradle task with commit parameter -> gradlew -Pfrom=a03149610f28aa241c8e06fe2614645a1a11724d getCheckedOutGitCommitHash - - args = len(sys.argv) - if args != 4: - print("Bad arguments count, should be 4 got " + str(args)) - exit(-1) - - print("secretKey = " + str(sys.argv[1])) - print("apkVersion = " + str(sys.argv[2])) - print("projectBaseDir = " + str(sys.argv[3])) - - headers = dict(SECRET_KEY=sys.argv[1], APK_VERSION=sys.argv[2]) - projectBaseDir = sys.argv[3] - - latestCommitHash = getLatestCommitHash() - latestCommits = getLatestCommitsFrom(projectBaseDir, latestCommitHash) - - if len(latestCommits) <= 0: - print("latestCommits is empty, nothing was commited to the project since last build so do nothing, " - "latestCommitHash = " + latestCommitHash) - exit(0) - - uploadApk(headers, projectBaseDir, latestCommits) - exit(0) diff --git a/Kuroba/upload_apk.py b/Kuroba/upload_apk.py new file mode 100644 index 0000000000..c6b4667290 --- /dev/null +++ b/Kuroba/upload_apk.py @@ -0,0 +1,82 @@ +import os +import sys +import requests +import subprocess + +def getLatestCommitHash(): + response = requests.get('http://127.0.0.1:8080/latest_commit_hash') + if response.status_code != 200: + print("Error while trying to get latest commit hash from the server" + + ", response status = " + str(response.status_code) + + ", message = " + str(response.content)) + exit(-1) + + return response.content.decode("utf-8") + + +def uploadApk(headers, latestCommits): + apkPath = "Kuroba\\app\\build\\outputs\\apk\\debug\\Kuroba.apk" + inFile = open(apkPath, "rb") + try: + if not inFile.readable(): + print("Provided file is not readable, path = " + str(apkPath)) + exit(-1) + + print(latestCommits) + + response = requests.post( + 'http://127.0.0.1:8080/upload', + files=dict(apk=inFile, latest_commits=latestCommits), + headers=headers) + + if response.status_code != 200: + print("Error while trying to upload file" + + ", response status = " + str(response.status_code) + + ", message = " + str(response.content)) + exit(-1) + + print("Successfully uploaded") + except Exception as e: + print("Unhandled exception: " + str(e)) + exit(-1) + finally: + inFile.close() + + +def getLatestCommitsFrom(latestCommitHash): + if len(latestCommitHash) <= 0: + print("Latest commit hash is empty, should be okay") + + arguments = ['gradlew', '-Pfrom=' + latestCommitHash + ' getLastCommits'] + + result = subprocess.run(arguments, stdout=subprocess.PIPE) + resultText = str(result.stdout) + + print("getLastCommits result: " + resultText) + return resultText + + +if __name__ == '__main__': + args = len(sys.argv) + if args != 3: + print("Bad arguments count, should be 3 got " + str(args)) + exit(-1) + + headers = dict(SECRET_KEY=sys.argv[1], APK_VERSION=sys.argv[2]) + latestCommitHash = "" + + try: + latestCommitHash = getLatestCommitHash() + except Exception as e: + print("Couldn't get latest commit hash from the server: " + str(e)) + exit(-1) + + latestCommits = getLatestCommitsFrom(latestCommitHash) + + if len(latestCommits) <= 0: + print("latestCommits is empty, nothing was commited to the project since last build so do nothing, " + "latestCommitHash = " + latestCommitHash) + exit(0) + + uploadApk(headers, latestCommits) + exit(0) From 49cca30f1396875d01f2f56ecda3b372b5323b0c Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 8 Sep 2019 20:23:18 +0300 Subject: [PATCH 045/184] Remove pip install --upgrade pip command, trigger CI build --- .travis.yml | 3 +-- Kuroba/app/build.gradle | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2e86f56299..5ffa8262d3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,8 +14,7 @@ android: - build-tools-28.0.3 - android-28 install: - - pip install --upgrade pip - - pip install requests + - pip install requests script: - cd Kuroba - chmod +x gradlew diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index b0d79a1cfe..a74c7fabfc 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -158,7 +158,7 @@ android { dependencies { implementation 'androidx.legacy:legacy-support-v13:1.0.0' - implementation 'androidx.appcompat:appcompat:1.0.2' + implementation 'androidx.appcompat:appcompat:1.0.2' implementation 'androidx.recyclerview:recyclerview:1.0.0' implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.annotation:annotation:1.1.0' From 525ea3ef3624de1ba59d0596a2c0429283855dc1 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 8 Sep 2019 20:37:55 +0300 Subject: [PATCH 046/184] Use baseUrl for python scripts that is being passed from the travis.yaml script, trigger CI build --- .travis.yml | 2 +- Kuroba/app/build.gradle | 2 +- Kuroba/upload_apk.py | 17 +++++++++-------- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5ffa8262d3..a3dedd3ea1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,7 @@ script: - cd Kuroba - chmod +x gradlew - ./gradlew build --console plain -x lint - - python upload_apk.py 1234567890 401010 + - python upload_apk.py 1234567890 401010 http://127.0.0.1:8080 notifications: email: false \ No newline at end of file diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index a74c7fabfc..b0d79a1cfe 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -158,7 +158,7 @@ android { dependencies { implementation 'androidx.legacy:legacy-support-v13:1.0.0' - implementation 'androidx.appcompat:appcompat:1.0.2' + implementation 'androidx.appcompat:appcompat:1.0.2' implementation 'androidx.recyclerview:recyclerview:1.0.0' implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.annotation:annotation:1.1.0' diff --git a/Kuroba/upload_apk.py b/Kuroba/upload_apk.py index c6b4667290..06fd3018ad 100644 --- a/Kuroba/upload_apk.py +++ b/Kuroba/upload_apk.py @@ -3,8 +3,8 @@ import requests import subprocess -def getLatestCommitHash(): - response = requests.get('http://127.0.0.1:8080/latest_commit_hash') +def getLatestCommitHash(baseUrl): + response = requests.get(baseUrl + '/latest_commit_hash') if response.status_code != 200: print("Error while trying to get latest commit hash from the server" + ", response status = " + str(response.status_code) + @@ -14,7 +14,7 @@ def getLatestCommitHash(): return response.content.decode("utf-8") -def uploadApk(headers, latestCommits): +def uploadApk(baseUrl, headers, latestCommits): apkPath = "Kuroba\\app\\build\\outputs\\apk\\debug\\Kuroba.apk" inFile = open(apkPath, "rb") try: @@ -25,7 +25,7 @@ def uploadApk(headers, latestCommits): print(latestCommits) response = requests.post( - 'http://127.0.0.1:8080/upload', + baseUrl + '/upload', files=dict(apk=inFile, latest_commits=latestCommits), headers=headers) @@ -58,15 +58,16 @@ def getLatestCommitsFrom(latestCommitHash): if __name__ == '__main__': args = len(sys.argv) - if args != 3: - print("Bad arguments count, should be 3 got " + str(args)) + if args != 4: + print("Bad arguments count, should be 4 got " + str(args)) exit(-1) headers = dict(SECRET_KEY=sys.argv[1], APK_VERSION=sys.argv[2]) + baseUrl = sys.argv[3] latestCommitHash = "" try: - latestCommitHash = getLatestCommitHash() + latestCommitHash = getLatestCommitHash(baseUrl) except Exception as e: print("Couldn't get latest commit hash from the server: " + str(e)) exit(-1) @@ -78,5 +79,5 @@ def getLatestCommitsFrom(latestCommitHash): "latestCommitHash = " + latestCommitHash) exit(0) - uploadApk(headers, latestCommits) + uploadApk(baseUrl, headers, latestCommits) exit(0) From fdd4de4acfd3e290156a4c23740ee658ba2db660 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 8 Sep 2019 20:45:21 +0300 Subject: [PATCH 047/184] Fix lint warning, attempt to stop travis build upon script error --- .travis.yml | 8 ++++---- Kuroba/app/src/main/res/layout/cell_post.xml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index a3dedd3ea1..dc359fd9b9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,10 +16,10 @@ android: install: - pip install requests script: - - cd Kuroba - - chmod +x gradlew - - ./gradlew build --console plain -x lint - - python upload_apk.py 1234567890 401010 http://127.0.0.1:8080 + - cd Kuroba123 || travis_terminate 1; + - chmod +x gradlew || travis_terminate 2; + - ./gradlew build --console plain -x lint || travis_terminate 3; + - python upload_apk.py 1234567890 401010 http://127.0.0.1:8080 || travis_terminate 4; notifications: email: false \ No newline at end of file diff --git a/Kuroba/app/src/main/res/layout/cell_post.xml b/Kuroba/app/src/main/res/layout/cell_post.xml index 052dcb4011..871c187ee1 100644 --- a/Kuroba/app/src/main/res/layout/cell_post.xml +++ b/Kuroba/app/src/main/res/layout/cell_post.xml @@ -21,7 +21,7 @@ along with this program. If not, see . android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" - tools:ignore="RtlHardcoded,RtlSymmetry"> + tools:ignore="RtlHardcoded,RtlSymmetry,NotSibling"> Date: Sun, 8 Sep 2019 20:48:43 +0300 Subject: [PATCH 048/184] Ok, it works, now it should do everything up until apk uploading --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index dc359fd9b9..c2b9e9a751 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ android: install: - pip install requests script: - - cd Kuroba123 || travis_terminate 1; + - cd Kuroba || travis_terminate 1; - chmod +x gradlew || travis_terminate 2; - ./gradlew build --console plain -x lint || travis_terminate 3; - python upload_apk.py 1234567890 401010 http://127.0.0.1:8080 || travis_terminate 4; From daf5f8e9d155fcf19d97a5f8dca8e577529407df Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 14 Sep 2019 12:23:37 +0300 Subject: [PATCH 049/184] Trigger CI build, should finally see a built apk on the apk server --- .travis.yml | 2 +- Kuroba/app/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index c2b9e9a751..362522fd8e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,7 @@ script: - cd Kuroba || travis_terminate 1; - chmod +x gradlew || travis_terminate 2; - ./gradlew build --console plain -x lint || travis_terminate 3; - - python upload_apk.py 1234567890 401010 http://127.0.0.1:8080 || travis_terminate 4; + - python upload_apk.py 1234567890 401010 http://94.140.116.243:8080 || travis_terminate 4; notifications: email: false \ No newline at end of file diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index b0d79a1cfe..26796886d0 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -179,7 +179,7 @@ dependencies { implementation 'org.nibor.autolink:autolink:0.10.0' implementation 'com.google.code.gson:gson:2.8.5' implementation 'me.xdrop:fuzzywuzzy:1.1.10' - implementation 'org.codejargon.feather:feather:1.0' + implementation 'org.codejargon.feather:feather:1.0' implementation 'com.vladsch.flexmark:flexmark:0.42.12' implementation 'com.vladsch.flexmark:flexmark-ext-gfm-issues:0.42.12' implementation 'com.vdurmont:emoji-java:4.0.0' From e2f8ded05dd4a8e946ec2e23cee2e962c0ee3e39 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 14 Sep 2019 12:49:01 +0300 Subject: [PATCH 050/184] Update upload_apk script, trigger CI build --- Kuroba/app/build.gradle | 2 +- Kuroba/upload_apk.py | 30 ++++++++++++++++++------------ 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 26796886d0..b0d79a1cfe 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -179,7 +179,7 @@ dependencies { implementation 'org.nibor.autolink:autolink:0.10.0' implementation 'com.google.code.gson:gson:2.8.5' implementation 'me.xdrop:fuzzywuzzy:1.1.10' - implementation 'org.codejargon.feather:feather:1.0' + implementation 'org.codejargon.feather:feather:1.0' implementation 'com.vladsch.flexmark:flexmark:0.42.12' implementation 'com.vladsch.flexmark:flexmark-ext-gfm-issues:0.42.12' implementation 'com.vdurmont:emoji-java:4.0.0' diff --git a/Kuroba/upload_apk.py b/Kuroba/upload_apk.py index 06fd3018ad..9084f0f731 100644 --- a/Kuroba/upload_apk.py +++ b/Kuroba/upload_apk.py @@ -1,4 +1,3 @@ -import os import sys import requests import subprocess @@ -6,7 +5,7 @@ def getLatestCommitHash(baseUrl): response = requests.get(baseUrl + '/latest_commit_hash') if response.status_code != 200: - print("Error while trying to get latest commit hash from the server" + + print("getLatestCommitHash() Error while trying to get latest commit hash from the server" + ", response status = " + str(response.status_code) + ", message = " + str(response.content)) exit(-1) @@ -19,7 +18,7 @@ def uploadApk(baseUrl, headers, latestCommits): inFile = open(apkPath, "rb") try: if not inFile.readable(): - print("Provided file is not readable, path = " + str(apkPath)) + print("uploadApk() Provided file is not readable, path = " + str(apkPath)) exit(-1) print(latestCommits) @@ -30,14 +29,14 @@ def uploadApk(baseUrl, headers, latestCommits): headers=headers) if response.status_code != 200: - print("Error while trying to upload file" + + print("uploadApk() Error while trying to upload file" + ", response status = " + str(response.status_code) + ", message = " + str(response.content)) exit(-1) - print("Successfully uploaded") + print("uploadApk() Successfully uploaded") except Exception as e: - print("Unhandled exception: " + str(e)) + print("uploadApk() Unhandled exception: " + str(e)) exit(-1) finally: inFile.close() @@ -45,14 +44,15 @@ def uploadApk(baseUrl, headers, latestCommits): def getLatestCommitsFrom(latestCommitHash): if len(latestCommitHash) <= 0: - print("Latest commit hash is empty, should be okay") + print("getLatestCommitsFrom() Latest commit hash is empty, should be okay") arguments = ['gradlew', '-Pfrom=' + latestCommitHash + ' getLastCommits'] + print("getLatestCommitsFrom() arguments: " + str(arguments)) - result = subprocess.run(arguments, stdout=subprocess.PIPE) + result = subprocess.run(args=arguments, stdout=subprocess.PIPE) resultText = str(result.stdout) - print("getLastCommits result: " + resultText) + print("getLatestCommitsFrom() getLastCommits result: " + resultText) return resultText @@ -69,13 +69,19 @@ def getLatestCommitsFrom(latestCommitHash): try: latestCommitHash = getLatestCommitHash(baseUrl) except Exception as e: - print("Couldn't get latest commit hash from the server: " + str(e)) + print("Couldn't get latest commit hash from the server, error: " + str(e)) exit(-1) - latestCommits = getLatestCommitsFrom(latestCommitHash) + latestCommits = "" + + try: + latestCommits = getLatestCommitsFrom(latestCommitHash) + except Exception as e: + print("main() Couldn't get latest commits list from the gradle task, error: " + str(e)) + exit(-1) if len(latestCommits) <= 0: - print("latestCommits is empty, nothing was commited to the project since last build so do nothing, " + print("main() latestCommits is empty, nothing was commited to the project since last build so do nothing, " "latestCommitHash = " + latestCommitHash) exit(0) From efcc2693271661019672571260be1f51b3dd7f8d Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 14 Sep 2019 12:57:06 +0300 Subject: [PATCH 051/184] use pip3, trigger CI build --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 362522fd8e..6a8afceaf4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,7 +14,7 @@ android: - build-tools-28.0.3 - android-28 install: - - pip install requests + - pip3 install requests script: - cd Kuroba || travis_terminate 1; - chmod +x gradlew || travis_terminate 2; From 82b6df69dbb42edbed490b2df256e7bd3f7d24e0 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 14 Sep 2019 13:05:06 +0300 Subject: [PATCH 052/184] install python3-pip, trigger CI build --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index 6a8afceaf4..cf1ff4fbfa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,6 +13,10 @@ android: - extra-android-m2repository - build-tools-28.0.3 - android-28 +before_install: + - sudo apt-get -y install python3-pip + - python3 -V + - pip3 -V install: - pip3 install requests script: From 0fe50d94947c51a57fdb95baf1284412c79590a2 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 14 Sep 2019 13:37:56 +0300 Subject: [PATCH 053/184] add getLastTenCommits gradle task, update upload_apk script, trigger CI build --- .travis.yml | 3 ++- Kuroba/app/build.gradle | 16 ++++++++++++++-- Kuroba/upload_apk.py | 18 +++++++++++++----- 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index cf1ff4fbfa..990ce7a8ef 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,6 +14,7 @@ android: - build-tools-28.0.3 - android-28 before_install: + - sudo apt-get update - sudo apt-get -y install python3-pip - python3 -V - pip3 -V @@ -23,7 +24,7 @@ script: - cd Kuroba || travis_terminate 1; - chmod +x gradlew || travis_terminate 2; - ./gradlew build --console plain -x lint || travis_terminate 3; - - python upload_apk.py 1234567890 401010 http://94.140.116.243:8080 || travis_terminate 4; + - python upload_apk.py 1234567890 401010 http://94.140.116.243:8080 my-multi-feature || travis_terminate 4; notifications: email: false \ No newline at end of file diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index b0d79a1cfe..351e9e2541 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -187,9 +187,9 @@ dependencies { implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' } -task getLastCommits { +task getLastCommitsFromCommitByHash { doFirst { - def command = "git log ${from}^..HEAD --pretty=format:\"%H - %an, %ar : %s\" --first-parent" + def command = "git log ${branch_name} ${from}^..HEAD --pretty=format:\"%H - %an, %ar : %s\" --first-parent" println command def task = command.execute() @@ -197,3 +197,15 @@ task getLastCommits { println task.text } } + + +task getLastTenCommits { + doFirst { + def command = "git log ${branch_name} -n 10 --pretty=format:\"%H - %an, %ar : %s\" --first-parent" + println command + + def task = command.execute() + task.waitFor(15, TimeUnit.SECONDS) + println task.text + } +} \ No newline at end of file diff --git a/Kuroba/upload_apk.py b/Kuroba/upload_apk.py index 9084f0f731..b9e5d4b4b6 100644 --- a/Kuroba/upload_apk.py +++ b/Kuroba/upload_apk.py @@ -42,11 +42,14 @@ def uploadApk(baseUrl, headers, latestCommits): inFile.close() -def getLatestCommitsFrom(latestCommitHash): +def getLatestCommitsFrom(branchName, latestCommitHash): + arguments = ['gradlew', + '-Pfrom=' + latestCommitHash + ' -Pbranch_name=' + branchName + ' getLastCommitsFromCommitByHash'] + if len(latestCommitHash) <= 0: - print("getLatestCommitsFrom() Latest commit hash is empty, should be okay") + arguments = ['gradlew', + '-Pbranch_name=' + branchName + ' getLastTenCommits'] - arguments = ['gradlew', '-Pfrom=' + latestCommitHash + ' getLastCommits'] print("getLatestCommitsFrom() arguments: " + str(arguments)) result = subprocess.run(args=arguments, stdout=subprocess.PIPE) @@ -58,12 +61,17 @@ def getLatestCommitsFrom(latestCommitHash): if __name__ == '__main__': args = len(sys.argv) - if args != 4: - print("Bad arguments count, should be 4 got " + str(args)) + if args != 5: + print("Bad arguments count, should be 5 got " + str(args) + ", expected arguments: " + "\n1. Secret key, " + "\n2. Apk version, " + "\n3. Base url, " + "\n4. Branch name") exit(-1) headers = dict(SECRET_KEY=sys.argv[1], APK_VERSION=sys.argv[2]) baseUrl = sys.argv[3] + branchName = sys.argv[4] latestCommitHash = "" try: From a1830fe66ec89bac130375d110a51134c841f19e Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 14 Sep 2019 13:47:52 +0300 Subject: [PATCH 054/184] update upload_apk script, trigger CI build --- Kuroba/upload_apk.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Kuroba/upload_apk.py b/Kuroba/upload_apk.py index b9e5d4b4b6..940a4719fd 100644 --- a/Kuroba/upload_apk.py +++ b/Kuroba/upload_apk.py @@ -43,6 +43,8 @@ def uploadApk(baseUrl, headers, latestCommits): def getLatestCommitsFrom(branchName, latestCommitHash): + print("branchName = \"" + str(branchName) + "\", latestCommitHash = \"" + str(latestCommitHash) + "\"") + arguments = ['gradlew', '-Pfrom=' + latestCommitHash + ' -Pbranch_name=' + branchName + ' getLastCommitsFromCommitByHash'] @@ -83,7 +85,7 @@ def getLatestCommitsFrom(branchName, latestCommitHash): latestCommits = "" try: - latestCommits = getLatestCommitsFrom(latestCommitHash) + latestCommits = getLatestCommitsFrom(branchName, latestCommitHash) except Exception as e: print("main() Couldn't get latest commits list from the gradle task, error: " + str(e)) exit(-1) From 40dc90bc12d1da4628f1e25f07390244304fb090 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 14 Sep 2019 14:02:35 +0300 Subject: [PATCH 055/184] use python3 to run the script instead of python2, trigger CI build --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 990ce7a8ef..daae48ed3a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,7 +24,7 @@ script: - cd Kuroba || travis_terminate 1; - chmod +x gradlew || travis_terminate 2; - ./gradlew build --console plain -x lint || travis_terminate 3; - - python upload_apk.py 1234567890 401010 http://94.140.116.243:8080 my-multi-feature || travis_terminate 4; + - python3 upload_apk.py 1234567890 401010 http://94.140.116.243:8080 my-multi-feature || travis_terminate 4; notifications: email: false \ No newline at end of file From b4dad884ffb670c8cead12d5f298833636af5e57 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 14 Sep 2019 14:11:37 +0300 Subject: [PATCH 056/184] trigger CI build --- Kuroba/upload_apk.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Kuroba/upload_apk.py b/Kuroba/upload_apk.py index 940a4719fd..19316a6ea0 100644 --- a/Kuroba/upload_apk.py +++ b/Kuroba/upload_apk.py @@ -43,6 +43,7 @@ def uploadApk(baseUrl, headers, latestCommits): def getLatestCommitsFrom(branchName, latestCommitHash): + print(subprocess.__file__) print("branchName = \"" + str(branchName) + "\", latestCommitHash = \"" + str(latestCommitHash) + "\"") arguments = ['gradlew', From d6943ef82e4a96b7d249acdf545275be707d56e4 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 14 Sep 2019 14:19:40 +0300 Subject: [PATCH 057/184] use python 3.6, trigger CI build --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index daae48ed3a..5cf8f89a50 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,6 +15,7 @@ android: - android-28 before_install: - sudo apt-get update + - sudo apt-get install python3.6 - sudo apt-get -y install python3-pip - python3 -V - pip3 -V From c51d6f19c73920170acfaf5631f619d9d608a854 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 14 Sep 2019 14:25:36 +0300 Subject: [PATCH 058/184] trigger CI build --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5cf8f89a50..078c0d020d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,8 @@ language: - python jdk: - oraclejdk8 -python: 3.6 +python: + - "3.6" android: components: - platform-tools @@ -15,7 +16,6 @@ android: - android-28 before_install: - sudo apt-get update - - sudo apt-get install python3.6 - sudo apt-get -y install python3-pip - python3 -V - pip3 -V From 2a959723b8dd436f08d9a6c9c19db9a211a04735 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 14 Sep 2019 15:03:00 +0300 Subject: [PATCH 059/184] move before_install commands into install commands, trigger CI build --- .travis.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 078c0d020d..9bf9d956cc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,8 +5,7 @@ language: - python jdk: - oraclejdk8 -python: - - "3.6" +python: "3.6" android: components: - platform-tools @@ -14,12 +13,11 @@ android: - extra-android-m2repository - build-tools-28.0.3 - android-28 -before_install: +install: - sudo apt-get update - sudo apt-get -y install python3-pip - python3 -V - pip3 -V -install: - pip3 install requests script: - cd Kuroba || travis_terminate 1; From 4ed813d32b820a212e05685eeb84aa3395470679 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 14 Sep 2019 15:09:17 +0300 Subject: [PATCH 060/184] another attempt to use python 3.6, trigger CI build --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 9bf9d956cc..763e01fdc7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,6 +15,7 @@ android: - android-28 install: - sudo apt-get update + - sudo apt-get install python3 - sudo apt-get -y install python3-pip - python3 -V - pip3 -V From de3fe27dd8d755014aecc0ac0f21f7a02ca4f72d Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 14 Sep 2019 15:19:25 +0300 Subject: [PATCH 061/184] update upload_apk script, trigger CI build --- Kuroba/upload_apk.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/Kuroba/upload_apk.py b/Kuroba/upload_apk.py index 19316a6ea0..700a491faa 100644 --- a/Kuroba/upload_apk.py +++ b/Kuroba/upload_apk.py @@ -2,6 +2,25 @@ import requests import subprocess +def run(*popenargs, input=None, check=False, **kwargs): + if input is not None: + if 'stdin' in kwargs: + raise ValueError('stdin and input arguments may not both be used.') + kwargs['stdin'] = subprocess.PIPE + + process = subprocess.Popen(*popenargs, **kwargs) + try: + stdout, stderr = process.communicate(input) + except: + process.kill() + process.wait() + raise + retcode = process.poll() + if check and retcode: + raise subprocess.CalledProcessError( + retcode, process.args, output=stdout, stderr=stderr) + return retcode, stdout, stderr + def getLatestCommitHash(baseUrl): response = requests.get(baseUrl + '/latest_commit_hash') if response.status_code != 200: @@ -41,9 +60,7 @@ def uploadApk(baseUrl, headers, latestCommits): finally: inFile.close() - def getLatestCommitsFrom(branchName, latestCommitHash): - print(subprocess.__file__) print("branchName = \"" + str(branchName) + "\", latestCommitHash = \"" + str(latestCommitHash) + "\"") arguments = ['gradlew', @@ -55,10 +72,10 @@ def getLatestCommitsFrom(branchName, latestCommitHash): print("getLatestCommitsFrom() arguments: " + str(arguments)) - result = subprocess.run(args=arguments, stdout=subprocess.PIPE) - resultText = str(result.stdout) + retcode, stdout, _ = run(args=arguments, stdout=subprocess.PIPE) + resultText = str(stdout) - print("getLatestCommitsFrom() getLastCommits result: " + resultText) + print("getLatestCommitsFrom() getLastCommits result: " + resultText + ", retcode = " + str(retcode)) return resultText From 06e395bf63ea3bcb5e6829cb8fc794042efc105e Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 14 Sep 2019 15:32:13 +0300 Subject: [PATCH 062/184] update upload_apk script, trigger CI build --- Kuroba/upload_apk.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/Kuroba/upload_apk.py b/Kuroba/upload_apk.py index 700a491faa..de4f8ecb6e 100644 --- a/Kuroba/upload_apk.py +++ b/Kuroba/upload_apk.py @@ -1,7 +1,9 @@ +import os import sys import requests import subprocess + def run(*popenargs, input=None, check=False, **kwargs): if input is not None: if 'stdin' in kwargs: @@ -21,6 +23,7 @@ def run(*popenargs, input=None, check=False, **kwargs): retcode, process.args, output=stdout, stderr=stderr) return retcode, stdout, stderr + def getLatestCommitHash(baseUrl): response = requests.get(baseUrl + '/latest_commit_hash') if response.status_code != 200: @@ -60,14 +63,18 @@ def uploadApk(baseUrl, headers, latestCommits): finally: inFile.close() + def getLatestCommitsFrom(branchName, latestCommitHash): - print("branchName = \"" + str(branchName) + "\", latestCommitHash = \"" + str(latestCommitHash) + "\"") + gradlewFullPath = os.path.dirname(os.path.abspath(__file__)) + "\\gradlew" + + print("branchName = \"" + str(branchName) + "\", latestCommitHash = \"" + str( + latestCommitHash) + "\", gradlewFullPath = \"" + gradlewFullPath + "\"") - arguments = ['gradlew', + arguments = [gradlewFullPath, '-Pfrom=' + latestCommitHash + ' -Pbranch_name=' + branchName + ' getLastCommitsFromCommitByHash'] if len(latestCommitHash) <= 0: - arguments = ['gradlew', + arguments = [gradlewFullPath, '-Pbranch_name=' + branchName + ' getLastTenCommits'] print("getLatestCommitsFrom() arguments: " + str(arguments)) From 37393b2be34235a8f7d51df196e4c7e91bde8c7c Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 14 Sep 2019 15:42:50 +0300 Subject: [PATCH 063/184] update upload_apk script, trigger CI build --- Kuroba/upload_apk.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Kuroba/upload_apk.py b/Kuroba/upload_apk.py index de4f8ecb6e..b9b5b52666 100644 --- a/Kuroba/upload_apk.py +++ b/Kuroba/upload_apk.py @@ -2,7 +2,7 @@ import sys import requests import subprocess - +from pathlib import Path def run(*popenargs, input=None, check=False, **kwargs): if input is not None: @@ -65,7 +65,7 @@ def uploadApk(baseUrl, headers, latestCommits): def getLatestCommitsFrom(branchName, latestCommitHash): - gradlewFullPath = os.path.dirname(os.path.abspath(__file__)) + "\\gradlew" + gradlewFullPath = os.path.join(Path(__file__).parent.absolute(), "gradlew") print("branchName = \"" + str(branchName) + "\", latestCommitHash = \"" + str( latestCommitHash) + "\", gradlewFullPath = \"" + gradlewFullPath + "\"") From 09cbd946cb631575cb8d5001925f296b4826f7c3 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 14 Sep 2019 15:53:45 +0300 Subject: [PATCH 064/184] update upload_apk script, trigger CI build --- Kuroba/upload_apk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kuroba/upload_apk.py b/Kuroba/upload_apk.py index b9b5b52666..dcc685b623 100644 --- a/Kuroba/upload_apk.py +++ b/Kuroba/upload_apk.py @@ -65,7 +65,7 @@ def uploadApk(baseUrl, headers, latestCommits): def getLatestCommitsFrom(branchName, latestCommitHash): - gradlewFullPath = os.path.join(Path(__file__).parent.absolute(), "gradlew") + gradlewFullPath = str(os.path.join(Path(__file__).parent.absolute(), "gradlew")) print("branchName = \"" + str(branchName) + "\", latestCommitHash = \"" + str( latestCommitHash) + "\", gradlewFullPath = \"" + gradlewFullPath + "\"") From c6e8bf989732ee42102b7a56e0858b01ac77353c Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 14 Sep 2019 16:26:36 +0300 Subject: [PATCH 065/184] update upload_apk script, trigger CI build --- Kuroba/upload_apk.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Kuroba/upload_apk.py b/Kuroba/upload_apk.py index dcc685b623..3a2324b201 100644 --- a/Kuroba/upload_apk.py +++ b/Kuroba/upload_apk.py @@ -1,4 +1,3 @@ -import os import sys import requests import subprocess @@ -65,7 +64,7 @@ def uploadApk(baseUrl, headers, latestCommits): def getLatestCommitsFrom(branchName, latestCommitHash): - gradlewFullPath = str(os.path.join(Path(__file__).parent.absolute(), "gradlew")) + gradlewFullPath = str(Path(__file__).parent.absolute() + "/gradlew") print("branchName = \"" + str(branchName) + "\", latestCommitHash = \"" + str( latestCommitHash) + "\", gradlewFullPath = \"" + gradlewFullPath + "\"") @@ -89,7 +88,7 @@ def getLatestCommitsFrom(branchName, latestCommitHash): if __name__ == '__main__': args = len(sys.argv) if args != 5: - print("Bad arguments count, should be 5 got " + str(args) + ", expected arguments: " + print("Bad arguments count, should be 4 got " + str(args) + ", expected arguments: " "\n1. Secret key, " "\n2. Apk version, " "\n3. Base url, " From 90c58d380fbf4a9175f173bb384af5d9f6f58fb2 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 14 Sep 2019 16:36:23 +0300 Subject: [PATCH 066/184] update upload_apk script, trigger CI build --- Kuroba/upload_apk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kuroba/upload_apk.py b/Kuroba/upload_apk.py index 3a2324b201..bf061fa350 100644 --- a/Kuroba/upload_apk.py +++ b/Kuroba/upload_apk.py @@ -64,7 +64,7 @@ def uploadApk(baseUrl, headers, latestCommits): def getLatestCommitsFrom(branchName, latestCommitHash): - gradlewFullPath = str(Path(__file__).parent.absolute() + "/gradlew") + gradlewFullPath = str(Path(__file__).parent.absolute()) + "/gradlew" print("branchName = \"" + str(branchName) + "\", latestCommitHash = \"" + str( latestCommitHash) + "\", gradlewFullPath = \"" + gradlewFullPath + "\"") From 63aa5c9851bab0489591d17d45bbb326ab47d6c1 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 14 Sep 2019 16:51:30 +0300 Subject: [PATCH 067/184] update upload_apk script, trigger CI build --- Kuroba/upload_apk.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Kuroba/upload_apk.py b/Kuroba/upload_apk.py index bf061fa350..1a478707b3 100644 --- a/Kuroba/upload_apk.py +++ b/Kuroba/upload_apk.py @@ -35,7 +35,7 @@ def getLatestCommitHash(baseUrl): def uploadApk(baseUrl, headers, latestCommits): - apkPath = "Kuroba\\app\\build\\outputs\\apk\\debug\\Kuroba.apk" + apkPath = "Kuroba/app/build/outputs/apk/debug/Kuroba.apk" inFile = open(apkPath, "rb") try: if not inFile.readable(): @@ -81,7 +81,13 @@ def getLatestCommitsFrom(branchName, latestCommitHash): retcode, stdout, _ = run(args=arguments, stdout=subprocess.PIPE) resultText = str(stdout) + print("\n\n") print("getLatestCommitsFrom() getLastCommits result: " + resultText + ", retcode = " + str(retcode)) + + if retcode != 0: + print("Command returned non zero return code: retcode = " + str(retcode)) + exit(-1) + return resultText From ea2c64273afaf9d4a778632160dd2b6476777d94 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 14 Sep 2019 17:06:48 +0300 Subject: [PATCH 068/184] update upload_apk script, trigger CI build --- Kuroba/upload_apk.py | 34 +++++----------------------------- 1 file changed, 5 insertions(+), 29 deletions(-) diff --git a/Kuroba/upload_apk.py b/Kuroba/upload_apk.py index 1a478707b3..518010ce79 100644 --- a/Kuroba/upload_apk.py +++ b/Kuroba/upload_apk.py @@ -3,26 +3,6 @@ import subprocess from pathlib import Path -def run(*popenargs, input=None, check=False, **kwargs): - if input is not None: - if 'stdin' in kwargs: - raise ValueError('stdin and input arguments may not both be used.') - kwargs['stdin'] = subprocess.PIPE - - process = subprocess.Popen(*popenargs, **kwargs) - try: - stdout, stderr = process.communicate(input) - except: - process.kill() - process.wait() - raise - retcode = process.poll() - if check and retcode: - raise subprocess.CalledProcessError( - retcode, process.args, output=stdout, stderr=stderr) - return retcode, stdout, stderr - - def getLatestCommitHash(baseUrl): response = requests.get(baseUrl + '/latest_commit_hash') if response.status_code != 200: @@ -35,7 +15,7 @@ def getLatestCommitHash(baseUrl): def uploadApk(baseUrl, headers, latestCommits): - apkPath = "Kuroba/app/build/outputs/apk/debug/Kuroba.apk" + apkPath = "app/build/outputs/apk/debug/Kuroba.apk" inFile = open(apkPath, "rb") try: if not inFile.readable(): @@ -78,17 +58,13 @@ def getLatestCommitsFrom(branchName, latestCommitHash): print("getLatestCommitsFrom() arguments: " + str(arguments)) - retcode, stdout, _ = run(args=arguments, stdout=subprocess.PIPE) - resultText = str(stdout) + output = subprocess.Popen(["date"], stdout=subprocess.PIPE) + stdout, _ = str(output.communicate()) print("\n\n") - print("getLatestCommitsFrom() getLastCommits result: " + resultText + ", retcode = " + str(retcode)) - - if retcode != 0: - print("Command returned non zero return code: retcode = " + str(retcode)) - exit(-1) + print("getLatestCommitsFrom() getLastCommits result: " + stdout) - return resultText + return stdout if __name__ == '__main__': From fed21f15924b2d2198f0b17c6ebb0357f6904b00 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 14 Sep 2019 17:17:02 +0300 Subject: [PATCH 069/184] update upload_apk script, trigger CI build --- Kuroba/upload_apk.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Kuroba/upload_apk.py b/Kuroba/upload_apk.py index 518010ce79..9af34012ad 100644 --- a/Kuroba/upload_apk.py +++ b/Kuroba/upload_apk.py @@ -59,10 +59,10 @@ def getLatestCommitsFrom(branchName, latestCommitHash): print("getLatestCommitsFrom() arguments: " + str(arguments)) output = subprocess.Popen(["date"], stdout=subprocess.PIPE) - stdout, _ = str(output.communicate()) + stdout, stderr = str(output.communicate()) - print("\n\n") - print("getLatestCommitsFrom() getLastCommits result: " + stdout) + print("\n\ngetLatestCommitsFrom() getLastCommits stderr: " + stderr) + print("\n\ngetLatestCommitsFrom() getLastCommits stdout: " + stdout) return stdout @@ -97,7 +97,7 @@ def getLatestCommitsFrom(branchName, latestCommitHash): exit(-1) if len(latestCommits) <= 0: - print("main() latestCommits is empty, nothing was commited to the project since last build so do nothing, " + print("main() latestCommits is empty, nothing was committed to the project since last build so do nothing, " "latestCommitHash = " + latestCommitHash) exit(0) From 58eb4ac554a173ba2846242543f5bb85c501d689 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 14 Sep 2019 17:41:04 +0300 Subject: [PATCH 070/184] update upload_apk script, trigger CI build --- Kuroba/upload_apk.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/Kuroba/upload_apk.py b/Kuroba/upload_apk.py index 9af34012ad..0a0b1262d5 100644 --- a/Kuroba/upload_apk.py +++ b/Kuroba/upload_apk.py @@ -58,13 +58,17 @@ def getLatestCommitsFrom(branchName, latestCommitHash): print("getLatestCommitsFrom() arguments: " + str(arguments)) - output = subprocess.Popen(["date"], stdout=subprocess.PIPE) - stdout, stderr = str(output.communicate()) - - print("\n\ngetLatestCommitsFrom() getLastCommits stderr: " + stderr) - print("\n\ngetLatestCommitsFrom() getLastCommits stdout: " + stdout) - - return stdout + proc = subprocess.Popen(args=arguments, stdout=subprocess.PIPE) + while True: + line = proc.stdout.readline() + if not line: + break + # the real code does filtering here + print("test:" + str(line.rstrip())) + + # print("\n\ngetLatestCommitsFrom() getLastCommits stdout: " + stdout) + # return stdout + return "" if __name__ == '__main__': From f709fa8dafb29821637c463f5bb9abed993ebf25 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 14 Sep 2019 18:01:54 +0300 Subject: [PATCH 071/184] update upload_apk script, trigger CI build --- Kuroba/upload_apk.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Kuroba/upload_apk.py b/Kuroba/upload_apk.py index 0a0b1262d5..6c1dc7d331 100644 --- a/Kuroba/upload_apk.py +++ b/Kuroba/upload_apk.py @@ -50,11 +50,13 @@ def getLatestCommitsFrom(branchName, latestCommitHash): latestCommitHash) + "\", gradlewFullPath = \"" + gradlewFullPath + "\"") arguments = [gradlewFullPath, - '-Pfrom=' + latestCommitHash + ' -Pbranch_name=' + branchName + ' getLastCommitsFromCommitByHash'] + '-Pfrom=' + latestCommitHash + ' -Pbranch_name=' + branchName, + 'getLastCommitsFromCommitByHash'] if len(latestCommitHash) <= 0: arguments = [gradlewFullPath, - '-Pbranch_name=' + branchName + ' getLastTenCommits'] + '-Pbranch_name=' + branchName, + 'getLastTenCommits'] print("getLatestCommitsFrom() arguments: " + str(arguments)) From 04e2933059f24f363f569e7c7b7e8cd5b915a17c Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 14 Sep 2019 18:37:08 +0300 Subject: [PATCH 072/184] update upload_apk script, trigger CI build --- Kuroba/app/build.gradle | 6 ------ Kuroba/upload_apk.py | 20 +++++++++----------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 351e9e2541..5a437218fe 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -1,5 +1,3 @@ -import java.util.concurrent.TimeUnit - apply plugin: 'com.android.application' android { @@ -190,8 +188,6 @@ dependencies { task getLastCommitsFromCommitByHash { doFirst { def command = "git log ${branch_name} ${from}^..HEAD --pretty=format:\"%H - %an, %ar : %s\" --first-parent" - println command - def task = command.execute() task.waitFor(15, TimeUnit.SECONDS) println task.text @@ -202,8 +198,6 @@ task getLastCommitsFromCommitByHash { task getLastTenCommits { doFirst { def command = "git log ${branch_name} -n 10 --pretty=format:\"%H - %an, %ar : %s\" --first-parent" - println command - def task = command.execute() task.waitFor(15, TimeUnit.SECONDS) println task.text diff --git a/Kuroba/upload_apk.py b/Kuroba/upload_apk.py index 6c1dc7d331..f963642096 100644 --- a/Kuroba/upload_apk.py +++ b/Kuroba/upload_apk.py @@ -60,17 +60,15 @@ def getLatestCommitsFrom(branchName, latestCommitHash): print("getLatestCommitsFrom() arguments: " + str(arguments)) - proc = subprocess.Popen(args=arguments, stdout=subprocess.PIPE) - while True: - line = proc.stdout.readline() - if not line: - break - # the real code does filtering here - print("test:" + str(line.rstrip())) - - # print("\n\ngetLatestCommitsFrom() getLastCommits stdout: " + stdout) - # return stdout - return "" + p = subprocess.Popen(args=arguments, stdout=subprocess.PIPE) + (stdout, stderr) = p.communicate() + + p_status = p.wait() + print("Command output : " + str(stdout)) + print("Command error : " + str(stderr)) + print("Command exit status/return code : ", p_status) + + return str(stdout) if __name__ == '__main__': From 7ac62ac315d409e329663513de8526abd3afaa81 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 14 Sep 2019 18:47:42 +0300 Subject: [PATCH 073/184] update upload_apk script, trigger CI build --- Kuroba/app/build.gradle | 2 -- 1 file changed, 2 deletions(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 5a437218fe..47afdcdca4 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -189,7 +189,6 @@ task getLastCommitsFromCommitByHash { doFirst { def command = "git log ${branch_name} ${from}^..HEAD --pretty=format:\"%H - %an, %ar : %s\" --first-parent" def task = command.execute() - task.waitFor(15, TimeUnit.SECONDS) println task.text } } @@ -199,7 +198,6 @@ task getLastTenCommits { doFirst { def command = "git log ${branch_name} -n 10 --pretty=format:\"%H - %an, %ar : %s\" --first-parent" def task = command.execute() - task.waitFor(15, TimeUnit.SECONDS) println task.text } } \ No newline at end of file From e6b696b692038c8c74115e8142b1c4b9a237f52e Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 14 Sep 2019 19:01:48 +0300 Subject: [PATCH 074/184] update upload_apk script, trigger CI build --- Kuroba/app/build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 47afdcdca4..12eca3dca1 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -1,3 +1,4 @@ +import java.util.concurrent.TimeUnit apply plugin: 'com.android.application' android { @@ -189,6 +190,7 @@ task getLastCommitsFromCommitByHash { doFirst { def command = "git log ${branch_name} ${from}^..HEAD --pretty=format:\"%H - %an, %ar : %s\" --first-parent" def task = command.execute() + task.waitFor(15, TimeUnit.SECONDS) println task.text } } @@ -198,6 +200,7 @@ task getLastTenCommits { doFirst { def command = "git log ${branch_name} -n 10 --pretty=format:\"%H - %an, %ar : %s\" --first-parent" def task = command.execute() + task.waitFor(15, TimeUnit.SECONDS) println task.text } } \ No newline at end of file From 383d434d26db143c3fe3f6588f41fe86b13e7c07 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 14 Sep 2019 19:13:35 +0300 Subject: [PATCH 075/184] update upload_apk script, trigger CI build --- Kuroba/upload_apk.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/Kuroba/upload_apk.py b/Kuroba/upload_apk.py index f963642096..8617f121bb 100644 --- a/Kuroba/upload_apk.py +++ b/Kuroba/upload_apk.py @@ -60,13 +60,16 @@ def getLatestCommitsFrom(branchName, latestCommitHash): print("getLatestCommitsFrom() arguments: " + str(arguments)) - p = subprocess.Popen(args=arguments, stdout=subprocess.PIPE) - (stdout, stderr) = p.communicate() - - p_status = p.wait() - print("Command output : " + str(stdout)) - print("Command error : " + str(stderr)) - print("Command exit status/return code : ", p_status) + # p = subprocess.Popen(args=arguments, stdout=subprocess.PIPE) + # (stdout, stderr) = p.communicate() + # + # p_status = p.wait() + # print("Command output : " + str(stdout)) + # print("Command error : " + str(stderr)) + # print("Command exit status/return code : ", p_status) + + stdout = subprocess.check_output(arguments) + print("result = " + str(stdout)) return str(stdout) From afbab46ee37cd5367b1b6d143bfc3f9fb70e75de Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 14 Sep 2019 19:42:04 +0300 Subject: [PATCH 076/184] update upload_apk script, trigger CI build --- Kuroba/app/build.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 12eca3dca1..f16ce9af53 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -200,7 +200,8 @@ task getLastTenCommits { doFirst { def command = "git log ${branch_name} -n 10 --pretty=format:\"%H - %an, %ar : %s\" --first-parent" def task = command.execute() - task.waitFor(15, TimeUnit.SECONDS) + println "111" println task.text + println "222" } } \ No newline at end of file From 9ca37852fb244c4bf102cbb47aaf02ac4607e920 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 14 Sep 2019 19:53:19 +0300 Subject: [PATCH 077/184] update upload_apk script, trigger CI build --- Kuroba/app/build.gradle | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index f16ce9af53..92c903784a 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -198,10 +198,13 @@ task getLastCommitsFromCommitByHash { task getLastTenCommits { doFirst { - def command = "git log ${branch_name} -n 10 --pretty=format:\"%H - %an, %ar : %s\" --first-parent" + def command = "git log -n 10 --pretty=format:\"%H - %an, %ar : %s\" --first-parent" def task = command.execute() + def text = task.text + println "111" - println task.text + println "size = ${text.size()}" + println text println "222" } } \ No newline at end of file From 0f4351f0168b9bf172ca9cda1c3c56dca0522c7d Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 14 Sep 2019 20:03:14 +0300 Subject: [PATCH 078/184] update upload_apk script, trigger CI build --- Kuroba/app/build.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 92c903784a..d1986d7d2b 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -198,7 +198,8 @@ task getLastCommitsFromCommitByHash { task getLastTenCommits { doFirst { - def command = "git log -n 10 --pretty=format:\"%H - %an, %ar : %s\" --first-parent" +// def command = "git log -n 10 --pretty=format:\"%H - %an, %ar : %s\" --first-parent" + def command = "git log -n 10" def task = command.execute() def text = task.text From 8b319596943d61b92aebd66b2f5e43d99f32a2fe Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 14 Sep 2019 20:05:37 +0300 Subject: [PATCH 079/184] update upload_apk script, trigger CI build --- Kuroba/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index d1986d7d2b..e8d441bfb5 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -202,7 +202,7 @@ task getLastTenCommits { def command = "git log -n 10" def task = command.execute() def text = task.text - + println "111" println "size = ${text.size()}" println text From a51c9a9be779508bd3fc954a2e1a727bd7fb7a08 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 14 Sep 2019 20:13:03 +0300 Subject: [PATCH 080/184] update upload_apk script, trigger CI build --- Kuroba/app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index e8d441bfb5..31ad9bceff 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -201,11 +201,11 @@ task getLastTenCommits { // def command = "git log -n 10 --pretty=format:\"%H - %an, %ar : %s\" --first-parent" def command = "git log -n 10" def task = command.execute() - def text = task.text + def text = task.err.text println "111" println "size = ${text.size()}" - println text + println "error = ${text}" println "222" } } \ No newline at end of file From 56de0d2deac13aa548dc8a2d62d7a29d1445400c Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 14 Sep 2019 20:29:01 +0300 Subject: [PATCH 081/184] update upload_apk script, trigger CI build --- Kuroba/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 31ad9bceff..a64d394500 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -205,7 +205,7 @@ task getLastTenCommits { println "111" println "size = ${text.size()}" - println "error = ${text}" + println "error = ${text}" println "222" } } \ No newline at end of file From 7500c012b8681c0978ba497181eab542c24d75c9 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 14 Sep 2019 20:48:05 +0300 Subject: [PATCH 082/184] update upload_apk script, trigger CI build --- Kuroba/app/build.gradle | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index a64d394500..ddd3351487 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -197,15 +197,22 @@ task getLastCommitsFromCommitByHash { task getLastTenCommits { - doFirst { +// doFirst { // def command = "git log -n 10 --pretty=format:\"%H - %an, %ar : %s\" --first-parent" - def command = "git log -n 10" - def task = command.execute() - def text = task.err.text - - println "111" - println "size = ${text.size()}" - println "error = ${text}" - println "222" - } +// def command = "git log -n 10" +// def task = command.execute() +// def text = task.err.text +// +// println "111" +// println "size = ${text.size()}" +// println "error = ${text}" +// println "222" +// } + + def branch = "" + def proc = "git log -n 10".execute() + proc.in.eachLine { line -> println line } + proc.err.eachLine { line -> println line } + proc.waitFor() + branch } \ No newline at end of file From 07d69e1d506fa8dd443da655a9f9dfe6d1b8bc08 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 15 Sep 2019 12:37:41 +0300 Subject: [PATCH 083/184] update upload_apk script, trigger CI build --- Kuroba/app/build.gradle | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index ddd3351487..7573589cc0 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -209,10 +209,10 @@ task getLastTenCommits { // println "222" // } - def branch = "" - def proc = "git log -n 10".execute() - proc.in.eachLine { line -> println line } - proc.err.eachLine { line -> println line } - proc.waitFor() - branch + doLast { + def proc = "git log -n 10".execute() + proc.in.eachLine { line -> println line } + proc.err.eachLine { line -> println line } + proc.waitFor(15, TimeUnit.SECONDS) + } } \ No newline at end of file From e7c80543ddfd8b6a7514779b5ed70c5b727b9d5f Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 15 Sep 2019 12:47:43 +0300 Subject: [PATCH 084/184] update upload_apk script, trigger CI build --- Kuroba/app/build.gradle | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 7573589cc0..be2d0b9793 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -187,7 +187,7 @@ dependencies { } task getLastCommitsFromCommitByHash { - doFirst { + doLast { def command = "git log ${branch_name} ${from}^..HEAD --pretty=format:\"%H - %an, %ar : %s\" --first-parent" def task = command.execute() task.waitFor(15, TimeUnit.SECONDS) @@ -197,22 +197,10 @@ task getLastCommitsFromCommitByHash { task getLastTenCommits { -// doFirst { -// def command = "git log -n 10 --pretty=format:\"%H - %an, %ar : %s\" --first-parent" -// def command = "git log -n 10" -// def task = command.execute() -// def text = task.err.text -// -// println "111" -// println "size = ${text.size()}" -// println "error = ${text}" -// println "222" -// } - doLast { - def proc = "git log -n 10".execute() - proc.in.eachLine { line -> println line } - proc.err.eachLine { line -> println line } - proc.waitFor(15, TimeUnit.SECONDS) + def command = "git log -n 10 --pretty=format:\"%H - %an, %ar : %s\" --first-parent" + def task = command.execute() + task.waitFor(15, TimeUnit.SECONDS) + println task.text } } \ No newline at end of file From 2eef0bea93960842305838f921c10646022098ca Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 15 Sep 2019 12:57:05 +0300 Subject: [PATCH 085/184] update upload_apk script, trigger CI build --- .travis.yml | 24 ++++++++++++------------ Kuroba/app/build.gradle | 9 +++++---- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/.travis.yml b/.travis.yml index 763e01fdc7..58d17c25d6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,18 +1,18 @@ sudo: false language: - - android + #- android - python -jdk: - - oraclejdk8 +#jdk: + #- oraclejdk8 python: "3.6" -android: - components: - - platform-tools - - tools - - extra-android-m2repository - - build-tools-28.0.3 - - android-28 +#android: +# components: +# - platform-tools +# - tools +# - extra-android-m2repository +# - build-tools-28.0.3 +# - android-28 install: - sudo apt-get update - sudo apt-get install python3 @@ -22,8 +22,8 @@ install: - pip3 install requests script: - cd Kuroba || travis_terminate 1; - - chmod +x gradlew || travis_terminate 2; - - ./gradlew build --console plain -x lint || travis_terminate 3; + # - chmod +x gradlew || travis_terminate 2; + # - ./gradlew build --console plain -x lint || travis_terminate 3; - python3 upload_apk.py 1234567890 401010 http://94.140.116.243:8080 my-multi-feature || travis_terminate 4; notifications: diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index be2d0b9793..81deb53163 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -186,6 +186,7 @@ dependencies { implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' } +// TODO task getLastCommitsFromCommitByHash { doLast { def command = "git log ${branch_name} ${from}^..HEAD --pretty=format:\"%H - %an, %ar : %s\" --first-parent" @@ -198,9 +199,9 @@ task getLastCommitsFromCommitByHash { task getLastTenCommits { doLast { - def command = "git log -n 10 --pretty=format:\"%H - %an, %ar : %s\" --first-parent" - def task = command.execute() - task.waitFor(15, TimeUnit.SECONDS) - println task.text + def proc = "git log -n 10 --pretty=format:\"%H - %an, %ar : %s\" --first-parent".execute() + proc.in.eachLine { line -> println line } + proc.err.eachLine { line -> println line } + proc.waitFor(15, TimeUnit.SECONDS) } } \ No newline at end of file From 89917a6e31dc1c5eae7664a94cb8dcd95a05cc26 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 15 Sep 2019 12:58:59 +0300 Subject: [PATCH 086/184] update upload_apk script, trigger CI build --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 58d17c25d6..8d87f8a9aa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,7 +22,7 @@ install: - pip3 install requests script: - cd Kuroba || travis_terminate 1; - # - chmod +x gradlew || travis_terminate 2; + - chmod +x gradlew || travis_terminate 2; # - ./gradlew build --console plain -x lint || travis_terminate 3; - python3 upload_apk.py 1234567890 401010 http://94.140.116.243:8080 my-multi-feature || travis_terminate 4; From e631e08c268ec8c0fa5ef0b61ef7ff12ec322a44 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 15 Sep 2019 13:01:57 +0300 Subject: [PATCH 087/184] update upload_apk script, trigger CI build --- .travis.yml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8d87f8a9aa..e20651c667 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,18 +1,18 @@ sudo: false language: - #- android + - android - python -#jdk: - #- oraclejdk8 +jdk: + - oraclejdk8 python: "3.6" -#android: -# components: -# - platform-tools -# - tools -# - extra-android-m2repository -# - build-tools-28.0.3 -# - android-28 +android: + components: + - platform-tools + - tools + - extra-android-m2repository + - build-tools-28.0.3 + - android-28 install: - sudo apt-get update - sudo apt-get install python3 @@ -23,7 +23,7 @@ install: script: - cd Kuroba || travis_terminate 1; - chmod +x gradlew || travis_terminate 2; - # - ./gradlew build --console plain -x lint || travis_terminate 3; + - ./gradlew build --console plain -x lint || travis_terminate 3; - python3 upload_apk.py 1234567890 401010 http://94.140.116.243:8080 my-multi-feature || travis_terminate 4; notifications: From d22d43624296622435ace8273b8792db9415db9b Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 15 Sep 2019 13:04:28 +0300 Subject: [PATCH 088/184] trigger CI build --- Kuroba/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 81deb53163..5fef200ad9 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -174,7 +174,7 @@ dependencies { implementation 'org.jsoup:jsoup:1.11.3' implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.12' implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0' - implementation 'org.greenrobot:eventbus:3.1.1' + implementation 'org.greenrobot:eventbus:3.1.1' implementation 'org.nibor.autolink:autolink:0.10.0' implementation 'com.google.code.gson:gson:2.8.5' implementation 'me.xdrop:fuzzywuzzy:1.1.10' From e30da582132f17d8b84ca696a2e50c8f291de5ff Mon Sep 17 00:00:00 2001 From: Dmitry Date: Sun, 15 Sep 2019 13:07:55 +0300 Subject: [PATCH 089/184] Trigger CI build --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e20651c667..93d0acc4c5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,4 +27,4 @@ script: - python3 upload_apk.py 1234567890 401010 http://94.140.116.243:8080 my-multi-feature || travis_terminate 4; notifications: - email: false \ No newline at end of file + email: false From da8f651719132fc8ab35ad4ff5d21cf010d3b8d0 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 15 Sep 2019 13:11:56 +0300 Subject: [PATCH 090/184] trigger CI build --- Kuroba/app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 5fef200ad9..ad1692e748 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -174,7 +174,7 @@ dependencies { implementation 'org.jsoup:jsoup:1.11.3' implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.12' implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0' - implementation 'org.greenrobot:eventbus:3.1.1' + implementation 'org.greenrobot:eventbus:3.1.1' implementation 'org.nibor.autolink:autolink:0.10.0' implementation 'com.google.code.gson:gson:2.8.5' implementation 'me.xdrop:fuzzywuzzy:1.1.10' @@ -190,7 +190,7 @@ dependencies { task getLastCommitsFromCommitByHash { doLast { def command = "git log ${branch_name} ${from}^..HEAD --pretty=format:\"%H - %an, %ar : %s\" --first-parent" - def task = command.execute() + def task = command.execute() task.waitFor(15, TimeUnit.SECONDS) println task.text } From 770656aae9d622fdd88fd768a4557592ae82727a Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 15 Sep 2019 13:25:53 +0300 Subject: [PATCH 091/184] add getVersionCode gradle task, trigger CI build --- Kuroba/app/build.gradle | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index ad1692e748..fc6a9289e6 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -187,10 +187,10 @@ dependencies { } // TODO -task getLastCommitsFromCommitByHash { +task getLastCommitsFromCommitByHash { doLast { def command = "git log ${branch_name} ${from}^..HEAD --pretty=format:\"%H - %an, %ar : %s\" --first-parent" - def task = command.execute() + def task = command.execute() task.waitFor(15, TimeUnit.SECONDS) println task.text } @@ -204,4 +204,10 @@ task getLastTenCommits { proc.err.eachLine { line -> println line } proc.waitFor(15, TimeUnit.SECONDS) } +} + +task getVersionCode { + doLast { + println(project.android.defaultConfig.versionCode) + } } \ No newline at end of file From 0cff1d3022a924e28f9cf0a89762d3897400ac8a Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 15 Sep 2019 13:36:16 +0300 Subject: [PATCH 092/184] trigger CI build --- Kuroba/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index fc6a9289e6..94864044d8 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -185,7 +185,7 @@ dependencies { implementation "io.reactivex.rxjava2:rxjava:2.2.9" implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' } - + // TODO task getLastCommitsFromCommitByHash { doLast { From 55a43b1285a5a00b285eaa3ac28564c60c15a761 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 15 Sep 2019 13:41:58 +0300 Subject: [PATCH 093/184] trigger CI build --- Kuroba/app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 94864044d8..2781c1884e 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -171,7 +171,7 @@ dependencies { implementation 'com.squareup.okhttp3:okhttp:3.14.1' implementation 'com.j256.ormlite:ormlite-core:5.1' implementation 'com.j256.ormlite:ormlite-android:5.1' - implementation 'org.jsoup:jsoup:1.11.3' + implementation 'org.jsoup:jsoup:1.11.3' implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.12' implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0' implementation 'org.greenrobot:eventbus:3.1.1' @@ -185,7 +185,7 @@ dependencies { implementation "io.reactivex.rxjava2:rxjava:2.2.9" implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' } - + // TODO task getLastCommitsFromCommitByHash { doLast { From 27702ecbeedc832ed739954f121183db6d63a2c1 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 15 Sep 2019 13:55:23 +0300 Subject: [PATCH 094/184] trigger CI build --- Kuroba/app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 2781c1884e..670e0c3027 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -171,9 +171,9 @@ dependencies { implementation 'com.squareup.okhttp3:okhttp:3.14.1' implementation 'com.j256.ormlite:ormlite-core:5.1' implementation 'com.j256.ormlite:ormlite-android:5.1' - implementation 'org.jsoup:jsoup:1.11.3' + implementation 'org.jsoup:jsoup:1.11.3' implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.12' - implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0' + implementation 'com.davemorrissey.labs:subsampling-scale-image-view: 3.10.0' implementation 'org.greenrobot:eventbus:3.1.1' implementation 'org.nibor.autolink:autolink:0.10.0' implementation 'com.google.code.gson:gson:2.8.5' From b75531c86d67d7ce640b38b57a3e2034e1c1d632 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 15 Sep 2019 13:58:41 +0300 Subject: [PATCH 095/184] trigger CI build --- Kuroba/app/build.gradle | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 670e0c3027..c5833c5470 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -1,4 +1,5 @@ import java.util.concurrent.TimeUnit + apply plugin: 'com.android.application' android { @@ -135,9 +136,9 @@ android { dimension "default" //these are manifest placeholders for the application name and icon location manifestPlaceholders = [ - appName: "Kuroba", - iconLoc: "@mipmap/ic_launcher", - fileProviderAuthority:"${applicationIdSuffix}.fileprovider" + appName : "Kuroba", + iconLoc : "@mipmap/ic_launcher", + fileProviderAuthority: "${applicationIdSuffix}.fileprovider" ] } dev { @@ -147,9 +148,9 @@ android { //these are manifest placeholders for the application name and icon location manifestPlaceholders = [ - appName: "Kuroba${versionNameSuffix}", - iconLoc: "@mipmap/ic_launcher", - fileProviderAuthority:"${applicationIdSuffix}.fileprovider" + appName : "Kuroba${versionNameSuffix}", + iconLoc : "@mipmap/ic_launcher", + fileProviderAuthority: "${applicationIdSuffix}.fileprovider" ] } } @@ -187,7 +188,7 @@ dependencies { } // TODO -task getLastCommitsFromCommitByHash { +task getLastCommitsFromCommitByHash { doLast { def command = "git log ${branch_name} ${from}^..HEAD --pretty=format:\"%H - %an, %ar : %s\" --first-parent" def task = command.execute() From 8bf82ca22022101582a46ca070688fd12f1ac4d7 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 15 Sep 2019 14:01:26 +0300 Subject: [PATCH 096/184] trigger CI build --- .travis.yml | 16 ++++++++-------- Kuroba/app/build.gradle | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index 93d0acc4c5..763e01fdc7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,12 +7,12 @@ jdk: - oraclejdk8 python: "3.6" android: - components: - - platform-tools - - tools - - extra-android-m2repository - - build-tools-28.0.3 - - android-28 + components: + - platform-tools + - tools + - extra-android-m2repository + - build-tools-28.0.3 + - android-28 install: - sudo apt-get update - sudo apt-get install python3 @@ -23,8 +23,8 @@ install: script: - cd Kuroba || travis_terminate 1; - chmod +x gradlew || travis_terminate 2; - - ./gradlew build --console plain -x lint || travis_terminate 3; + - ./gradlew build --console plain -x lint || travis_terminate 3; - python3 upload_apk.py 1234567890 401010 http://94.140.116.243:8080 my-multi-feature || travis_terminate 4; notifications: - email: false + email: false \ No newline at end of file diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index c5833c5470..754f383378 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -174,7 +174,7 @@ dependencies { implementation 'com.j256.ormlite:ormlite-android:5.1' implementation 'org.jsoup:jsoup:1.11.3' implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.12' - implementation 'com.davemorrissey.labs:subsampling-scale-image-view: 3.10.0' + implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0' implementation 'org.greenrobot:eventbus:3.1.1' implementation 'org.nibor.autolink:autolink:0.10.0' implementation 'com.google.code.gson:gson:2.8.5' From 01cf30211c27a4a425622036891c8ff8213fa118 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 15 Sep 2019 14:13:40 +0300 Subject: [PATCH 097/184] trigger CI build --- Kuroba/app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 754f383378..c817d56b9f 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -200,10 +200,10 @@ task getLastCommitsFromCommitByHash { task getLastTenCommits { doLast { - def proc = "git log -n 10 --pretty=format:\"%H - %an, %ar : %s\" --first-parent".execute() + def proc = "git log -n 10 --pretty=format:\"%H - %an, %ad : %s\" --first-parent".execute() + proc.waitFor(15, TimeUnit.SECONDS) proc.in.eachLine { line -> println line } proc.err.eachLine { line -> println line } - proc.waitFor(15, TimeUnit.SECONDS) } } From e4b0b9fcfc987efd2a44c934f86a36a545aac5b2 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 15 Sep 2019 14:22:28 +0300 Subject: [PATCH 098/184] use getLatestCommit, trigger CI build --- Kuroba/app/build.gradle | 4 ++-- Kuroba/upload_apk.py | 11 +---------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index c817d56b9f..23855e63a2 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -198,9 +198,9 @@ task getLastCommitsFromCommitByHash { } -task getLastTenCommits { +task getLatestCommit { doLast { - def proc = "git log -n 10 --pretty=format:\"%H - %an, %ad : %s\" --first-parent".execute() + def proc = "git log -n 1 --pretty=format:\"%H - %an, %ad : %s\" --first-parent".execute() proc.waitFor(15, TimeUnit.SECONDS) proc.in.eachLine { line -> println line } proc.err.eachLine { line -> println line } diff --git a/Kuroba/upload_apk.py b/Kuroba/upload_apk.py index 8617f121bb..e3b98e402b 100644 --- a/Kuroba/upload_apk.py +++ b/Kuroba/upload_apk.py @@ -56,18 +56,9 @@ def getLatestCommitsFrom(branchName, latestCommitHash): if len(latestCommitHash) <= 0: arguments = [gradlewFullPath, '-Pbranch_name=' + branchName, - 'getLastTenCommits'] + 'getLatestCommit'] print("getLatestCommitsFrom() arguments: " + str(arguments)) - - # p = subprocess.Popen(args=arguments, stdout=subprocess.PIPE) - # (stdout, stderr) = p.communicate() - # - # p_status = p.wait() - # print("Command output : " + str(stdout)) - # print("Command error : " + str(stderr)) - # print("Command exit status/return code : ", p_status) - stdout = subprocess.check_output(arguments) print("result = " + str(stdout)) From f10fc3e4fc8d40f3efa18a23588a8665b5c79030 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 15 Sep 2019 14:23:53 +0300 Subject: [PATCH 099/184] trigger CI build --- Kuroba/app/build.gradle | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 23855e63a2..e4264ec9e7 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -200,7 +200,10 @@ task getLastCommitsFromCommitByHash { task getLatestCommit { doLast { - def proc = "git log -n 1 --pretty=format:\"%H - %an, %ad : %s\" --first-parent".execute() + def command = "git log -n 1 --pretty=format:\"%H - %an, %ad : %s\" --first-parent" + println "command = ${command}" + + def proc = command.execute() proc.waitFor(15, TimeUnit.SECONDS) proc.in.eachLine { line -> println line } proc.err.eachLine { line -> println line } From 7cb6939c0b1428ef1de450de4ebb4e7566e18392 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 15 Sep 2019 14:51:17 +0300 Subject: [PATCH 100/184] trigger CI build --- Kuroba/app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index e4264ec9e7..959f8e242b 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -190,7 +190,7 @@ dependencies { // TODO task getLastCommitsFromCommitByHash { doLast { - def command = "git log ${branch_name} ${from}^..HEAD --pretty=format:\"%H - %an, %ar : %s\" --first-parent" + def command = 'git log ${branch_name} ${from}^..HEAD --pretty=format:"%H - %an, %ar : %s" --first-parent' def task = command.execute() task.waitFor(15, TimeUnit.SECONDS) println task.text @@ -200,7 +200,7 @@ task getLastCommitsFromCommitByHash { task getLatestCommit { doLast { - def command = "git log -n 1 --pretty=format:\"%H - %an, %ad : %s\" --first-parent" + def command = 'git log -n 1 --pretty=format:"%H - %an, %ad : %s" --first-parent' println "command = ${command}" def proc = command.execute() From 3a85370612b5f3d78ec5a99696582705b2e64b77 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 15 Sep 2019 15:15:18 +0300 Subject: [PATCH 101/184] trigger CI build --- Kuroba/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 959f8e242b..c52305f3d3 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -200,7 +200,7 @@ task getLastCommitsFromCommitByHash { task getLatestCommit { doLast { - def command = 'git log -n 1 --pretty=format:"%H - %an, %ad : %s" --first-parent' + def command = 'git log -n 1 --pretty=format:\'%H - %an, %ad : %s\' --first-parent' println "command = ${command}" def proc = command.execute() From aea3ecd0c7fc723fe9cfd1e9042e2cd5374e142b Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 15 Sep 2019 15:40:38 +0300 Subject: [PATCH 102/184] trigger CI build --- Kuroba/app/build.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index c52305f3d3..107067c876 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -200,7 +200,8 @@ task getLastCommitsFromCommitByHash { task getLatestCommit { doLast { - def command = 'git log -n 1 --pretty=format:\'%H - %an, %ad : %s\' --first-parent' +// def command = 'git log -n 1 --pretty=format:\'%H - %an, %ad : %s\' --first-parent' + def command = 'git log -n 1 --pretty=format:\'%H %ad : %s\' --first-parent' println "command = ${command}" def proc = command.execute() From 4d5a28b5110e72d43ae89b10808086814ae29f2a Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 15 Sep 2019 15:49:11 +0300 Subject: [PATCH 103/184] trigger CI build --- Kuroba/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 107067c876..19df7fb00a 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -201,7 +201,7 @@ task getLastCommitsFromCommitByHash { task getLatestCommit { doLast { // def command = 'git log -n 1 --pretty=format:\'%H - %an, %ad : %s\' --first-parent' - def command = 'git log -n 1 --pretty=format:\'%H %ad : %s\' --first-parent' + def command = 'git log -n 1 --pretty=format:\'%H %s\' --first-parent' println "command = ${command}" def proc = command.execute() From 28b6e4a76fadf43fa8f2241726d6791f4f1f4e97 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 15 Sep 2019 16:00:21 +0300 Subject: [PATCH 104/184] trigger CI build --- Kuroba/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 19df7fb00a..b7697644ac 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -201,7 +201,7 @@ task getLastCommitsFromCommitByHash { task getLatestCommit { doLast { // def command = 'git log -n 1 --pretty=format:\'%H - %an, %ad : %s\' --first-parent' - def command = 'git log -n 1 --pretty=format:\'%H %s\' --first-parent' + def command = 'git log -n 1 --pretty=format:\'%H | %s\' --first-parent' println "command = ${command}" def proc = command.execute() From 29fcac360bec1ee31e2552f9dea14a2ca2b1207f Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 15 Sep 2019 16:19:36 +0300 Subject: [PATCH 105/184] trigger CI build --- Kuroba/app/build.gradle | 3 +-- Kuroba/upload_apk.py | 36 ++++++++++++++++++++++++++++-------- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index b7697644ac..0924ab2f01 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -200,8 +200,7 @@ task getLastCommitsFromCommitByHash { task getLatestCommit { doLast { -// def command = 'git log -n 1 --pretty=format:\'%H - %an, %ad : %s\' --first-parent' - def command = 'git log -n 1 --pretty=format:\'%H | %s\' --first-parent' + def command = 'git log ${branch_name} -n 1 --first-parent' println "command = ${command}" def proc = command.execute() diff --git a/Kuroba/upload_apk.py b/Kuroba/upload_apk.py index e3b98e402b..44555f5fd1 100644 --- a/Kuroba/upload_apk.py +++ b/Kuroba/upload_apk.py @@ -3,6 +3,18 @@ import subprocess from pathlib import Path + +def getApkVersionCode(): + gradlewFullPath = str(Path(__file__).parent.absolute()) + "/gradlew" + arguments = [gradlewFullPath, 'getVersionCode'] + + print("getApkVersionCode() arguments: " + str(arguments)) + stdout = subprocess.check_output(arguments) + print("result = " + str(stdout)) + + return str(stdout) + + def getLatestCommitHash(baseUrl): response = requests.get(baseUrl + '/latest_commit_hash') if response.status_code != 200: @@ -67,19 +79,23 @@ def getLatestCommitsFrom(branchName, latestCommitHash): if __name__ == '__main__': args = len(sys.argv) - if args != 5: - print("Bad arguments count, should be 4 got " + str(args) + ", expected arguments: " + if args != 4: + print("Bad arguments count, should be 3 got " + str(args) + ", expected arguments: " "\n1. Secret key, " - "\n2. Apk version, " - "\n3. Base url, " - "\n4. Branch name") + "\n2. Base url, " + "\n3. Branch name") exit(-1) - headers = dict(SECRET_KEY=sys.argv[1], APK_VERSION=sys.argv[2]) - baseUrl = sys.argv[3] - branchName = sys.argv[4] + secretKey = sys.argv[1] + apkVersion = getApkVersionCode() + baseUrl = sys.argv[2] + branchName = sys.argv[3] latestCommitHash = "" + if len(apkVersion) <= 0: + print("main() Bad apk version code " + apkVersion) + exit(-1) + try: latestCommitHash = getLatestCommitHash(baseUrl) except Exception as e: @@ -99,5 +115,9 @@ def getLatestCommitsFrom(branchName, latestCommitHash): "latestCommitHash = " + latestCommitHash) exit(0) + headers = dict( + SECRET_KEY=secretKey, + APK_VERSION=apkVersion) + uploadApk(baseUrl, headers, latestCommits) exit(0) From 8512269230aee865d8084dcc5539af6bc9a7a186 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 15 Sep 2019 16:36:16 +0300 Subject: [PATCH 106/184] update upload_apk script, trigger CI build --- Kuroba/app/build.gradle | 32 +++++++++++++++++++++----------- Kuroba/upload_apk.py | 5 +++-- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 0924ab2f01..51845abed7 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -187,19 +187,18 @@ dependencies { implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' } -// TODO -task getLastCommitsFromCommitByHash { - doLast { - def command = 'git log ${branch_name} ${from}^..HEAD --pretty=format:"%H - %an, %ar : %s" --first-parent' - def task = command.execute() - task.waitFor(15, TimeUnit.SECONDS) - println task.text - } -} - - task getLatestCommit { doLast { + // FIXME: + // I want to use --pretty=format:"%H - %an, %ar : %s" here but I CAN'T. For some + // reason this shit gives me the following error: + // "fatal: ambiguous argument '%ad': unknown revision or path not in the working tree." + // when this task is being executed on travis CI. But the same thing works perfectly + // locally!!!! I don't understand why it doesn't work on travis CI and I'm so fucking tired + // of trying to figure it out so I'm giving up. If someone knows how to fix this shit + // PLEASE make a PR. + // Here is the full command that doesn't work: + // git log ${branch_name} -n 1 --pretty=format:"%H - %an, %ar : %s" --first-parent def command = 'git log ${branch_name} -n 1 --first-parent' println "command = ${command}" @@ -210,6 +209,17 @@ task getLatestCommit { } } +task getLastCommitsFromCommitByHash { + doLast { + // FIXME: the same problem as above + def command = 'git log ${branch_name} ${from}^..HEAD --pretty=format:"%H - %an, %ar : %s" --first-parent' + def task = command.execute() + task.waitFor(15, TimeUnit.SECONDS) + println task.text + } +} + + task getVersionCode { doLast { println(project.android.defaultConfig.versionCode) diff --git a/Kuroba/upload_apk.py b/Kuroba/upload_apk.py index 44555f5fd1..6a14fa5715 100644 --- a/Kuroba/upload_apk.py +++ b/Kuroba/upload_apk.py @@ -78,8 +78,9 @@ def getLatestCommitsFrom(branchName, latestCommitHash): if __name__ == '__main__': - args = len(sys.argv) - if args != 4: + # First argument is the script full path which we don't need + args = len(sys.argv) - 1 + if args != 3: print("Bad arguments count, should be 3 got " + str(args) + ", expected arguments: " "\n1. Secret key, " "\n2. Base url, " From 8c46a0b9a5afb9ec4ff94a8df8b96dd59415b378 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 15 Sep 2019 16:45:24 +0300 Subject: [PATCH 107/184] trigger CI build --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 763e01fdc7..01a229cd6a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,7 +24,7 @@ script: - cd Kuroba || travis_terminate 1; - chmod +x gradlew || travis_terminate 2; - ./gradlew build --console plain -x lint || travis_terminate 3; - - python3 upload_apk.py 1234567890 401010 http://94.140.116.243:8080 my-multi-feature || travis_terminate 4; + - python3 upload_apk.py 1234567890 http://94.140.116.243:8080 multi-feature || travis_terminate 4; notifications: email: false \ No newline at end of file From 53558ee1d8e7fb272121d59d1a9d451a3ee87049 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 15 Sep 2019 16:55:30 +0300 Subject: [PATCH 108/184] trigger CI build --- Kuroba/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 51845abed7..a41f63097b 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -199,7 +199,7 @@ task getLatestCommit { // PLEASE make a PR. // Here is the full command that doesn't work: // git log ${branch_name} -n 1 --pretty=format:"%H - %an, %ar : %s" --first-parent - def command = 'git log ${branch_name} -n 1 --first-parent' + def command = "git log ${branch_name} -n 1 --first-parent" println "command = ${command}" def proc = command.execute() From fbb99b0a5cc793773d016225b3840c96db3dd9d9 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 15 Sep 2019 17:03:56 +0300 Subject: [PATCH 109/184] trigger CI build --- Kuroba/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index a41f63097b..492d456208 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -199,7 +199,7 @@ task getLatestCommit { // PLEASE make a PR. // Here is the full command that doesn't work: // git log ${branch_name} -n 1 --pretty=format:"%H - %an, %ar : %s" --first-parent - def command = "git log ${branch_name} -n 1 --first-parent" + def command = "git log ${branch_name} -n 1 --pretty=format:\"%H - %an, %ar : %s\" --first-parent" println "command = ${command}" def proc = command.execute() From 5ed0a65ce7a7d803c8cd2d1a191415bdbb685f02 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 15 Sep 2019 17:12:23 +0300 Subject: [PATCH 110/184] trigger CI build --- Kuroba/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 492d456208..a41f63097b 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -199,7 +199,7 @@ task getLatestCommit { // PLEASE make a PR. // Here is the full command that doesn't work: // git log ${branch_name} -n 1 --pretty=format:"%H - %an, %ar : %s" --first-parent - def command = "git log ${branch_name} -n 1 --pretty=format:\"%H - %an, %ar : %s\" --first-parent" + def command = "git log ${branch_name} -n 1 --first-parent" println "command = ${command}" def proc = command.execute() From c945f7a8a5b866622535df1a417ab13a71b89fe1 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 15 Sep 2019 17:25:21 +0300 Subject: [PATCH 111/184] trigger CI build --- Kuroba/app/build.gradle | 51 ++++++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index a41f63097b..cba5861b60 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -187,26 +187,39 @@ dependencies { implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' } -task getLatestCommit { - doLast { - // FIXME: - // I want to use --pretty=format:"%H - %an, %ar : %s" here but I CAN'T. For some - // reason this shit gives me the following error: - // "fatal: ambiguous argument '%ad': unknown revision or path not in the working tree." - // when this task is being executed on travis CI. But the same thing works perfectly - // locally!!!! I don't understand why it doesn't work on travis CI and I'm so fucking tired - // of trying to figure it out so I'm giving up. If someone knows how to fix this shit - // PLEASE make a PR. - // Here is the full command that doesn't work: - // git log ${branch_name} -n 1 --pretty=format:"%H - %an, %ar : %s" --first-parent - def command = "git log ${branch_name} -n 1 --first-parent" - println "command = ${command}" - - def proc = command.execute() - proc.waitFor(15, TimeUnit.SECONDS) - proc.in.eachLine { line -> println line } - proc.err.eachLine { line -> println line } +def getGitCommand = { -> + def stdout = new ByteArrayOutputStream() + exec { + def printArgs = "%H - %an, %ar : %s" + commandLine 'git', 'log', branch_name, '-n 1', "--pretty=format:${printArgs}",'--first-parent' + standardOutput = stdout } + + return stdout.toString().trim() +} + +task getLatestCommit { +// doLast { +// // FIXME: +// // I want to use --pretty=format:"%H - %an, %ar : %s" here but I CAN'T. For some +// // reason this shit gives me the following error: +// // "fatal: ambiguous argument '%ad': unknown revision or path not in the working tree." +// // when this task is being executed on travis CI. But the same thing works perfectly +// // locally!!!! I don't understand why it doesn't work on travis CI and I'm so fucking tired +// // of trying to figure it out so I'm giving up. If someone knows how to fix this shit +// // PLEASE make a PR. +// // Here is the full command that doesn't work: +// // git log ${branch_name} -n 1 --pretty=format:"%H - %an, %ar : %s" --first-parent +// def command = "git log ${branch_name} -n 1 --first-parent" +// println "command = ${command}" +// +// def proc = command.execute() +// proc.waitFor(15, TimeUnit.SECONDS) +// proc.in.eachLine { line -> println line } +// proc.err.eachLine { line -> println line } +// } + + println(getGitCommand()) } task getLastCommitsFromCommitByHash { From d5b07fec46a3edfb8e4e5cc94de68118d0d46704 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 15 Sep 2019 17:29:58 +0300 Subject: [PATCH 112/184] trigger CI build --- Kuroba/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index cba5861b60..d9f7ce7e62 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -191,7 +191,7 @@ def getGitCommand = { -> def stdout = new ByteArrayOutputStream() exec { def printArgs = "%H - %an, %ar : %s" - commandLine 'git', 'log', branch_name, '-n 1', "--pretty=format:${printArgs}",'--first-parent' + commandLine 'git', 'log', "${branch_name}", '-n 1', "--pretty=format:${printArgs}", '--first-parent' standardOutput = stdout } From 781b4fc8c575281859d73029232ce10318cb70e7 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 15 Sep 2019 17:42:52 +0300 Subject: [PATCH 113/184] trigger CI build --- Kuroba/app/build.gradle | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index d9f7ce7e62..3bfe638712 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -187,11 +187,11 @@ dependencies { implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' } -def getGitCommand = { -> +def getGitCommand(String branchName) { def stdout = new ByteArrayOutputStream() exec { def printArgs = "%H - %an, %ar : %s" - commandLine 'git', 'log', "${branch_name}", '-n 1', "--pretty=format:${printArgs}", '--first-parent' + commandLine 'git', 'log', branchName, '-n 1', "--pretty=format:${printArgs}", '--first-parent' standardOutput = stdout } @@ -199,6 +199,7 @@ def getGitCommand = { -> } task getLatestCommit { + doLast { // doLast { // // FIXME: // // I want to use --pretty=format:"%H - %an, %ar : %s" here but I CAN'T. For some @@ -219,7 +220,8 @@ task getLatestCommit { // proc.err.eachLine { line -> println line } // } - println(getGitCommand()) + println(getGitCommand(branch_name)) + } } task getLastCommitsFromCommitByHash { From 974a9a48dad7fcd0c3e13ec3267d36ad0029c5cc Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 15 Sep 2019 17:53:03 +0300 Subject: [PATCH 114/184] trigger CI build --- Kuroba/app/build.gradle | 48 +++++++++++++++-------------------------- 1 file changed, 17 insertions(+), 31 deletions(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 3bfe638712..9136410fa9 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -187,10 +187,10 @@ dependencies { implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' } -def getGitCommand(String branchName) { +def getLatestCommit(String branchName) { def stdout = new ByteArrayOutputStream() exec { - def printArgs = "%H - %an, %ar : %s" + def printArgs = "%H - %ad, %ar : %s" commandLine 'git', 'log', branchName, '-n 1', "--pretty=format:${printArgs}", '--first-parent' standardOutput = stdout } @@ -198,42 +198,28 @@ def getGitCommand(String branchName) { return stdout.toString().trim() } -task getLatestCommit { - doLast { -// doLast { -// // FIXME: -// // I want to use --pretty=format:"%H - %an, %ar : %s" here but I CAN'T. For some -// // reason this shit gives me the following error: -// // "fatal: ambiguous argument '%ad': unknown revision or path not in the working tree." -// // when this task is being executed on travis CI. But the same thing works perfectly -// // locally!!!! I don't understand why it doesn't work on travis CI and I'm so fucking tired -// // of trying to figure it out so I'm giving up. If someone knows how to fix this shit -// // PLEASE make a PR. -// // Here is the full command that doesn't work: -// // git log ${branch_name} -n 1 --pretty=format:"%H - %an, %ar : %s" --first-parent -// def command = "git log ${branch_name} -n 1 --first-parent" -// println "command = ${command}" -// -// def proc = command.execute() -// proc.waitFor(15, TimeUnit.SECONDS) -// proc.in.eachLine { line -> println line } -// proc.err.eachLine { line -> println line } -// } - - println(getGitCommand(branch_name)) +def getLastCommitsFromCommitByHash(String branchName, String from) { + def stdout = new ByteArrayOutputStream() + exec { + def printArgs = "%H - %ad, %ar : %s" + commandLine 'git', 'log', branchName, '-n 1', "${from}^..HEAD", "--pretty=format:${printArgs}", '--first-parent' + standardOutput = stdout } + + return stdout.toString().trim() } -task getLastCommitsFromCommitByHash { +task getLatestCommitTask { doLast { - // FIXME: the same problem as above - def command = 'git log ${branch_name} ${from}^..HEAD --pretty=format:"%H - %an, %ar : %s" --first-parent' - def task = command.execute() - task.waitFor(15, TimeUnit.SECONDS) - println task.text + println(getLatestCommit(branch_name)) } } +task getLastCommitsFromCommitByHashTask { + doLast { + println(getLastCommitsFromCommitByHash(branch_name)) + } +} task getVersionCode { doLast { From f9aeb7a96f90943f184b1a2701c69e7b109316c7 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 15 Sep 2019 18:07:57 +0300 Subject: [PATCH 115/184] trigger CI build --- Kuroba/app/build.gradle | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 9136410fa9..33907390c9 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -190,7 +190,7 @@ dependencies { def getLatestCommit(String branchName) { def stdout = new ByteArrayOutputStream() exec { - def printArgs = "%H - %ad, %ar : %s" + def printArgs = "%H; %ad; %s" commandLine 'git', 'log', branchName, '-n 1', "--pretty=format:${printArgs}", '--first-parent' standardOutput = stdout } @@ -201,8 +201,8 @@ def getLatestCommit(String branchName) { def getLastCommitsFromCommitByHash(String branchName, String from) { def stdout = new ByteArrayOutputStream() exec { - def printArgs = "%H - %ad, %ar : %s" - commandLine 'git', 'log', branchName, '-n 1', "${from}^..HEAD", "--pretty=format:${printArgs}", '--first-parent' + def printArgs = "%H; %ad; %s" + commandLine 'git', 'log', branchName, "${from}^..HEAD", "--pretty=format:${printArgs}", '--first-parent' standardOutput = stdout } @@ -217,7 +217,7 @@ task getLatestCommitTask { task getLastCommitsFromCommitByHashTask { doLast { - println(getLastCommitsFromCommitByHash(branch_name)) + println(getLastCommitsFromCommitByHash(branch_name, from)) } } From 63b13b3d73fd9487517e858ce204f302e3b9b092 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 15 Sep 2019 18:11:58 +0300 Subject: [PATCH 116/184] trigger CI build --- Kuroba/app/build.gradle | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 33907390c9..9dc6d2c026 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -191,7 +191,9 @@ def getLatestCommit(String branchName) { def stdout = new ByteArrayOutputStream() exec { def printArgs = "%H; %ad; %s" - commandLine 'git', 'log', branchName, '-n 1', "--pretty=format:${printArgs}", '--first-parent' + def dateFormat = "%Y-%m-%d %H:%M:%S" + + commandLine 'git', 'log', branchName, '-n 1', "--pretty=format:${printArgs}", '--first-parent', "--date=format:${dateFormat}" standardOutput = stdout } @@ -202,7 +204,9 @@ def getLastCommitsFromCommitByHash(String branchName, String from) { def stdout = new ByteArrayOutputStream() exec { def printArgs = "%H; %ad; %s" - commandLine 'git', 'log', branchName, "${from}^..HEAD", "--pretty=format:${printArgs}", '--first-parent' + def dateFormat = "%Y-%m-%d %H:%M:%S" + + commandLine 'git', 'log', branchName, "${from}^..HEAD", "--pretty=format:${printArgs}", '--first-parent', "--date=format:${dateFormat}" standardOutput = stdout } From 10809f356ca657d222d678728aada9b0f52d51c6 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 15 Sep 2019 19:11:58 +0300 Subject: [PATCH 117/184] trigger CI build --- Kuroba/app/build.gradle | 2 -- Kuroba/upload_apk.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 9dc6d2c026..65e6aaaf77 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -1,5 +1,3 @@ -import java.util.concurrent.TimeUnit - apply plugin: 'com.android.application' android { diff --git a/Kuroba/upload_apk.py b/Kuroba/upload_apk.py index 6a14fa5715..536013e3fc 100644 --- a/Kuroba/upload_apk.py +++ b/Kuroba/upload_apk.py @@ -27,7 +27,7 @@ def getLatestCommitHash(baseUrl): def uploadApk(baseUrl, headers, latestCommits): - apkPath = "app/build/outputs/apk/debug/Kuroba.apk" + apkPath = "app/build/outputs/apk/dev/debug/null.apk" # FIXME: change null to Kuroba when it works inFile = open(apkPath, "rb") try: if not inFile.readable(): From 4c97a4587bb30d3fe4cb8997ee6d3034b390edba Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 15 Sep 2019 19:28:20 +0300 Subject: [PATCH 118/184] trigger CI build --- .travis.yml | 2 +- Kuroba/upload_apk.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 01a229cd6a..f3787f6107 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,7 +23,7 @@ install: script: - cd Kuroba || travis_terminate 1; - chmod +x gradlew || travis_terminate 2; - - ./gradlew build --console plain -x lint || travis_terminate 3; + - ./gradlew assembleDevDebug --console plain -x lint || travis_terminate 3; - python3 upload_apk.py 1234567890 http://94.140.116.243:8080 multi-feature || travis_terminate 4; notifications: diff --git a/Kuroba/upload_apk.py b/Kuroba/upload_apk.py index 536013e3fc..6c325befa6 100644 --- a/Kuroba/upload_apk.py +++ b/Kuroba/upload_apk.py @@ -6,7 +6,7 @@ def getApkVersionCode(): gradlewFullPath = str(Path(__file__).parent.absolute()) + "/gradlew" - arguments = [gradlewFullPath, 'getVersionCode'] + arguments = [gradlewFullPath, 'getVersionCode', '-q'] print("getApkVersionCode() arguments: " + str(arguments)) stdout = subprocess.check_output(arguments) @@ -63,12 +63,14 @@ def getLatestCommitsFrom(branchName, latestCommitHash): arguments = [gradlewFullPath, '-Pfrom=' + latestCommitHash + ' -Pbranch_name=' + branchName, - 'getLastCommitsFromCommitByHash'] + 'getLastCommitsFromCommitByHash', + '-q'] if len(latestCommitHash) <= 0: arguments = [gradlewFullPath, '-Pbranch_name=' + branchName, - 'getLatestCommit'] + 'getLatestCommit', + '-q'] print("getLatestCommitsFrom() arguments: " + str(arguments)) stdout = subprocess.check_output(arguments) From f331d427addf7e7324e077b91b5043594298ed82 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 15 Sep 2019 19:54:42 +0300 Subject: [PATCH 119/184] trigger CI build --- Kuroba/upload_apk.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Kuroba/upload_apk.py b/Kuroba/upload_apk.py index 6c325befa6..71a63491a1 100644 --- a/Kuroba/upload_apk.py +++ b/Kuroba/upload_apk.py @@ -12,7 +12,7 @@ def getApkVersionCode(): stdout = subprocess.check_output(arguments) print("result = " + str(stdout)) - return str(stdout) + return str(stdout).strip() def getLatestCommitHash(baseUrl): @@ -27,7 +27,7 @@ def getLatestCommitHash(baseUrl): def uploadApk(baseUrl, headers, latestCommits): - apkPath = "app/build/outputs/apk/dev/debug/null.apk" # FIXME: change null to Kuroba when it works + apkPath = "app/build/outputs/apk/dev/debug/null.apk" # FIXME: change null to Kuroba when it works inFile = open(apkPath, "rb") try: if not inFile.readable(): From 34a1f35a27006dd379c8b00bcdf64d1bf4344824 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 15 Sep 2019 20:04:09 +0300 Subject: [PATCH 120/184] trigger CI build --- Kuroba/upload_apk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kuroba/upload_apk.py b/Kuroba/upload_apk.py index 71a63491a1..dbf91c64f9 100644 --- a/Kuroba/upload_apk.py +++ b/Kuroba/upload_apk.py @@ -9,7 +9,7 @@ def getApkVersionCode(): arguments = [gradlewFullPath, 'getVersionCode', '-q'] print("getApkVersionCode() arguments: " + str(arguments)) - stdout = subprocess.check_output(arguments) + stdout = subprocess.check_output(arguments).decode("utf-8") print("result = " + str(stdout)) return str(stdout).strip() From a11afb685dd18fe0ce350d292b65c627c6f2e63c Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Mon, 16 Sep 2019 14:04:11 +0300 Subject: [PATCH 121/184] test commit 1 --- Kuroba/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 65e6aaaf77..1edb945dea 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -197,7 +197,7 @@ def getLatestCommit(String branchName) { return stdout.toString().trim() } - + def getLastCommitsFromCommitByHash(String branchName, String from) { def stdout = new ByteArrayOutputStream() exec { From d81e17724a6207872843d30ba72635195e08fc48 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Wed, 18 Sep 2019 14:49:42 +0300 Subject: [PATCH 122/184] test commit 1 --- Kuroba/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 1edb945dea..65e6aaaf77 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -197,7 +197,7 @@ def getLatestCommit(String branchName) { return stdout.toString().trim() } - + def getLastCommitsFromCommitByHash(String branchName, String from) { def stdout = new ByteArrayOutputStream() exec { From 1e4c8bf89f6169a8e24170fe2c73955b3b0fee76 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Wed, 18 Sep 2019 14:49:52 +0300 Subject: [PATCH 123/184] test commit 2 --- Kuroba/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 65e6aaaf77..370ba9ba9f 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -197,7 +197,7 @@ def getLatestCommit(String branchName) { return stdout.toString().trim() } - + def getLastCommitsFromCommitByHash(String branchName, String from) { def stdout = new ByteArrayOutputStream() exec { From ec2934213831374436b9d1560c9f9f582e4a00d1 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Wed, 18 Sep 2019 14:50:02 +0300 Subject: [PATCH 124/184] test commit 3 --- Kuroba/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 370ba9ba9f..71c27b749f 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -197,7 +197,7 @@ def getLatestCommit(String branchName) { return stdout.toString().trim() } - + def getLastCommitsFromCommitByHash(String branchName, String from) { def stdout = new ByteArrayOutputStream() exec { From caf2b4184c8dcf7d229b2a35fa42b65571e4db2c Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Wed, 18 Sep 2019 14:53:36 +0300 Subject: [PATCH 125/184] test commit 0 --- Kuroba/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 71c27b749f..65e6aaaf77 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -197,7 +197,7 @@ def getLatestCommit(String branchName) { return stdout.toString().trim() } - + def getLastCommitsFromCommitByHash(String branchName, String from) { def stdout = new ByteArrayOutputStream() exec { From a89723e6790c15de4930ef64c9475fad31f4b51f Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Wed, 18 Sep 2019 15:00:19 +0300 Subject: [PATCH 126/184] test commit 1 --- Kuroba/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 65e6aaaf77..c98fa85b51 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -196,7 +196,7 @@ def getLatestCommit(String branchName) { } return stdout.toString().trim() -} +} def getLastCommitsFromCommitByHash(String branchName, String from) { def stdout = new ByteArrayOutputStream() From 4e046242dcc4f88cce3c0561a88cb4fa3b3fbbbc Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Wed, 18 Sep 2019 15:00:34 +0300 Subject: [PATCH 127/184] test commit 2 --- Kuroba/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index c98fa85b51..288bc1ce86 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -196,7 +196,7 @@ def getLatestCommit(String branchName) { } return stdout.toString().trim() -} +} def getLastCommitsFromCommitByHash(String branchName, String from) { def stdout = new ByteArrayOutputStream() From 1e99d9be988f3fed94248fc3397e52b692b9b334 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Wed, 18 Sep 2019 15:00:46 +0300 Subject: [PATCH 128/184] test commit 3 --- Kuroba/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 288bc1ce86..f71a2a5aa8 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -196,7 +196,7 @@ def getLatestCommit(String branchName) { } return stdout.toString().trim() -} +} def getLastCommitsFromCommitByHash(String branchName, String from) { def stdout = new ByteArrayOutputStream() From 3119f4c7b6606581ad5e79c18cdee7bff7d8dc5a Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Wed, 18 Sep 2019 15:53:24 +0300 Subject: [PATCH 129/184] test commit 4 --- Kuroba/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index f71a2a5aa8..c98fa85b51 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -196,7 +196,7 @@ def getLatestCommit(String branchName) { } return stdout.toString().trim() -} +} def getLastCommitsFromCommitByHash(String branchName, String from) { def stdout = new ByteArrayOutputStream() From 57bd380e471ab549b3359aa6d47579be64f25b93 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Wed, 18 Sep 2019 16:02:10 +0300 Subject: [PATCH 130/184] test commit 5 --- Kuroba/app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index c98fa85b51..23c1adddbc 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -196,7 +196,7 @@ def getLatestCommit(String branchName) { } return stdout.toString().trim() -} +} def getLastCommitsFromCommitByHash(String branchName, String from) { def stdout = new ByteArrayOutputStream() @@ -208,7 +208,7 @@ def getLastCommitsFromCommitByHash(String branchName, String from) { standardOutput = stdout } - return stdout.toString().trim() + return stdout.toString().trim() } task getLatestCommitTask { From d8cc0f8e6a708e6309dc7f524672528e57a8b16f Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Wed, 18 Sep 2019 16:11:54 +0300 Subject: [PATCH 131/184] upload_apk scripts fixes --- Kuroba/app/build.gradle | 4 ++-- Kuroba/upload_apk.py | 14 +++++++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 23c1adddbc..a5b72397e0 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -208,7 +208,7 @@ def getLastCommitsFromCommitByHash(String branchName, String from) { standardOutput = stdout } - return stdout.toString().trim() + return stdout.toString().trim() } task getLatestCommitTask { @@ -223,7 +223,7 @@ task getLastCommitsFromCommitByHashTask { } } -task getVersionCode { +task getVersionCodeTask { doLast { println(project.android.defaultConfig.versionCode) } diff --git a/Kuroba/upload_apk.py b/Kuroba/upload_apk.py index dbf91c64f9..4445d5993f 100644 --- a/Kuroba/upload_apk.py +++ b/Kuroba/upload_apk.py @@ -6,7 +6,7 @@ def getApkVersionCode(): gradlewFullPath = str(Path(__file__).parent.absolute()) + "/gradlew" - arguments = [gradlewFullPath, 'getVersionCode', '-q'] + arguments = [gradlewFullPath, 'getVersionCodeTask', '-q'] print("getApkVersionCode() arguments: " + str(arguments)) stdout = subprocess.check_output(arguments).decode("utf-8") @@ -16,7 +16,10 @@ def getApkVersionCode(): def getLatestCommitHash(baseUrl): - response = requests.get(baseUrl + '/latest_commit_hash') + response = requests.get( + baseUrl + '/latest_commit_hash', + timeout=30) + if response.status_code != 200: print("getLatestCommitHash() Error while trying to get latest commit hash from the server" + ", response status = " + str(response.status_code) + @@ -39,7 +42,8 @@ def uploadApk(baseUrl, headers, latestCommits): response = requests.post( baseUrl + '/upload', files=dict(apk=inFile, latest_commits=latestCommits), - headers=headers) + headers=headers, + timeout=30) if response.status_code != 200: print("uploadApk() Error while trying to upload file" + @@ -63,13 +67,13 @@ def getLatestCommitsFrom(branchName, latestCommitHash): arguments = [gradlewFullPath, '-Pfrom=' + latestCommitHash + ' -Pbranch_name=' + branchName, - 'getLastCommitsFromCommitByHash', + 'getLastCommitsFromCommitByHashTask', '-q'] if len(latestCommitHash) <= 0: arguments = [gradlewFullPath, '-Pbranch_name=' + branchName, - 'getLatestCommit', + 'getLatestCommitTask', '-q'] print("getLatestCommitsFrom() arguments: " + str(arguments)) From 6dc0265482a0d70232eb740f9a31e5c16f95549a Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Wed, 18 Sep 2019 16:21:17 +0300 Subject: [PATCH 132/184] change branch name --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f3787f6107..ccf5853c5f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,7 +24,7 @@ script: - cd Kuroba || travis_terminate 1; - chmod +x gradlew || travis_terminate 2; - ./gradlew assembleDevDebug --console plain -x lint || travis_terminate 3; - - python3 upload_apk.py 1234567890 http://94.140.116.243:8080 multi-feature || travis_terminate 4; + - python3 upload_apk.py 1234567890 http://94.140.116.243:8080 my-multi-feature || travis_terminate 4; notifications: email: false \ No newline at end of file From 0a80dc164098e437627e05122692f7f8836fd985 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Wed, 18 Sep 2019 16:30:07 +0300 Subject: [PATCH 133/184] update upload_apk script --- Kuroba/upload_apk.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Kuroba/upload_apk.py b/Kuroba/upload_apk.py index 4445d5993f..58ca93ffdd 100644 --- a/Kuroba/upload_apk.py +++ b/Kuroba/upload_apk.py @@ -66,7 +66,8 @@ def getLatestCommitsFrom(branchName, latestCommitHash): latestCommitHash) + "\", gradlewFullPath = \"" + gradlewFullPath + "\"") arguments = [gradlewFullPath, - '-Pfrom=' + latestCommitHash + ' -Pbranch_name=' + branchName, + '-Pbranch_name=' + branchName, + '-Pfrom=' + latestCommitHash, 'getLastCommitsFromCommitByHashTask', '-q'] From c7f815f61a164c975fbe890e7f3b4636400f44a5 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Wed, 18 Sep 2019 16:40:23 +0300 Subject: [PATCH 134/184] Change branch name back to multi-feature --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index ccf5853c5f..f3787f6107 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,7 +24,7 @@ script: - cd Kuroba || travis_terminate 1; - chmod +x gradlew || travis_terminate 2; - ./gradlew assembleDevDebug --console plain -x lint || travis_terminate 3; - - python3 upload_apk.py 1234567890 http://94.140.116.243:8080 my-multi-feature || travis_terminate 4; + - python3 upload_apk.py 1234567890 http://94.140.116.243:8080 multi-feature || travis_terminate 4; notifications: email: false \ No newline at end of file From 0b66bd2959d5a3ac6b97995e4e077fecdd9ce0e6 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Wed, 18 Sep 2019 16:52:12 +0300 Subject: [PATCH 135/184] upload_apk script fixes --- Kuroba/upload_apk.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Kuroba/upload_apk.py b/Kuroba/upload_apk.py index 58ca93ffdd..da10a93492 100644 --- a/Kuroba/upload_apk.py +++ b/Kuroba/upload_apk.py @@ -12,7 +12,7 @@ def getApkVersionCode(): stdout = subprocess.check_output(arguments).decode("utf-8") print("result = " + str(stdout)) - return str(stdout).strip() + return stdout.decode("utf-8").strip() def getLatestCommitHash(baseUrl): @@ -48,7 +48,7 @@ def uploadApk(baseUrl, headers, latestCommits): if response.status_code != 200: print("uploadApk() Error while trying to upload file" + ", response status = " + str(response.status_code) + - ", message = " + str(response.content)) + ", message = " + response.content.decode("utf-8")) exit(-1) print("uploadApk() Successfully uploaded") @@ -81,7 +81,7 @@ def getLatestCommitsFrom(branchName, latestCommitHash): stdout = subprocess.check_output(arguments) print("result = " + str(stdout)) - return str(stdout) + return stdout.decode("utf-8") if __name__ == '__main__': @@ -107,7 +107,7 @@ def getLatestCommitsFrom(branchName, latestCommitHash): try: latestCommitHash = getLatestCommitHash(baseUrl) except Exception as e: - print("Couldn't get latest commit hash from the server, error: " + str(e)) + print("main() Couldn't get latest commit hash from the server, error: " + str(e)) exit(-1) latestCommits = "" From 0f1451aa9915e53ff6ed2ed94625cfd0da62eb95 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Wed, 18 Sep 2019 16:59:43 +0300 Subject: [PATCH 136/184] upload_apk script fixes --- Kuroba/upload_apk.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Kuroba/upload_apk.py b/Kuroba/upload_apk.py index da10a93492..143563448c 100644 --- a/Kuroba/upload_apk.py +++ b/Kuroba/upload_apk.py @@ -9,7 +9,7 @@ def getApkVersionCode(): arguments = [gradlewFullPath, 'getVersionCodeTask', '-q'] print("getApkVersionCode() arguments: " + str(arguments)) - stdout = subprocess.check_output(arguments).decode("utf-8") + stdout = subprocess.check_output(arguments) print("result = " + str(stdout)) return stdout.decode("utf-8").strip() @@ -26,7 +26,7 @@ def getLatestCommitHash(baseUrl): ", message = " + str(response.content)) exit(-1) - return response.content.decode("utf-8") + return response.content.decode("utf-8").strip() def uploadApk(baseUrl, headers, latestCommits): @@ -81,7 +81,7 @@ def getLatestCommitsFrom(branchName, latestCommitHash): stdout = subprocess.check_output(arguments) print("result = " + str(stdout)) - return stdout.decode("utf-8") + return stdout.decode("utf-8").strip() if __name__ == '__main__': From 551fe3b651a79177314ab2513ffde1bd0248e15e Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Wed, 18 Sep 2019 17:07:28 +0300 Subject: [PATCH 137/184] shit works! --- Kuroba/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index a5b72397e0..56c1d398d3 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -210,7 +210,7 @@ def getLastCommitsFromCommitByHash(String branchName, String from) { return stdout.toString().trim() } - + task getLatestCommitTask { doLast { println(getLatestCommit(branch_name)) From d923fef2e718e78b15d9c26b49b0dbca141d0e97 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Wed, 18 Sep 2019 18:02:52 +0300 Subject: [PATCH 138/184] test commit 1 --- Kuroba/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 56c1d398d3..a5b72397e0 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -210,7 +210,7 @@ def getLastCommitsFromCommitByHash(String branchName, String from) { return stdout.toString().trim() } - + task getLatestCommitTask { doLast { println(getLatestCommit(branch_name)) From d92492c96739df9a58cbfe083527984166f1b9bd Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Wed, 18 Sep 2019 18:09:23 +0300 Subject: [PATCH 139/184] test commit 2 --- Kuroba/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index a5b72397e0..689ccf01d4 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -207,7 +207,7 @@ def getLastCommitsFromCommitByHash(String branchName, String from) { commandLine 'git', 'log', branchName, "${from}^..HEAD", "--pretty=format:${printArgs}", '--first-parent', "--date=format:${dateFormat}" standardOutput = stdout } - + return stdout.toString().trim() } From 59cce6b24527afb957738af9cf919e5e3f4c1e92 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Wed, 18 Sep 2019 18:09:36 +0300 Subject: [PATCH 140/184] test commit 3 --- Kuroba/app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 689ccf01d4..52f3ab0f01 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -207,8 +207,8 @@ def getLastCommitsFromCommitByHash(String branchName, String from) { commandLine 'git', 'log', branchName, "${from}^..HEAD", "--pretty=format:${printArgs}", '--first-parent', "--date=format:${dateFormat}" standardOutput = stdout } - - return stdout.toString().trim() + + return stdout.toString().trim() } task getLatestCommitTask { From ee19beffce337748824bb6e68192b41a9e0b9c9c Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Wed, 18 Sep 2019 18:21:55 +0300 Subject: [PATCH 141/184] Fix git log command would include a commit with hash that we are passing to the gradle task (we actually want to exclude that commit since it's already added) --- Kuroba/app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 52f3ab0f01..e009718a71 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -204,11 +204,11 @@ def getLastCommitsFromCommitByHash(String branchName, String from) { def printArgs = "%H; %ad; %s" def dateFormat = "%Y-%m-%d %H:%M:%S" - commandLine 'git', 'log', branchName, "${from}^..HEAD", "--pretty=format:${printArgs}", '--first-parent', "--date=format:${dateFormat}" + commandLine 'git', 'log', branchName, "${from}..HEAD", "--pretty=format:${printArgs}", '--first-parent', "--date=format:${dateFormat}" standardOutput = stdout } - return stdout.toString().trim() + return stdout.toString().trim() } task getLatestCommitTask { From 1c9213938cb9a4f4ff40cf8a866f83aa55f3d81f Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Wed, 18 Sep 2019 18:30:57 +0300 Subject: [PATCH 142/184] test commit 1 --- Kuroba/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index e009718a71..caa4e92848 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -209,7 +209,7 @@ def getLastCommitsFromCommitByHash(String branchName, String from) { } return stdout.toString().trim() -} +} task getLatestCommitTask { doLast { From 15db7929f7d261e77f88027e55dadf4ec4ba63cc Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Wed, 18 Sep 2019 18:31:09 +0300 Subject: [PATCH 143/184] test commit 2 --- Kuroba/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index caa4e92848..f6643bbca1 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -209,7 +209,7 @@ def getLastCommitsFromCommitByHash(String branchName, String from) { } return stdout.toString().trim() -} +} task getLatestCommitTask { doLast { From f31fcd1b02c777a6418699a50160ae8b6e31c32f Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Wed, 18 Sep 2019 18:49:29 +0300 Subject: [PATCH 144/184] Check whether a branch exists before doing the uploading --- Kuroba/app/build.gradle | 19 ++++++++++++++++++- Kuroba/upload_apk.py | 24 ++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index f6643bbca1..e72a17ad09 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -209,7 +209,24 @@ def getLastCommitsFromCommitByHash(String branchName, String from) { } return stdout.toString().trim() -} +} + +def checkBranchExists(String branchName) { + def stdout = new ByteArrayOutputStream() + exec { + + commandLine 'git', 'rev-parse', '--verify', branchName + standardOutput = stdout + } + + return stdout.toString().trim() +} + +task checkBranchExistsTask { + doLast { + println(checkBranchExists(branch_name)) + } +} task getLatestCommitTask { doLast { diff --git a/Kuroba/upload_apk.py b/Kuroba/upload_apk.py index 143563448c..a370faa9b3 100644 --- a/Kuroba/upload_apk.py +++ b/Kuroba/upload_apk.py @@ -83,6 +83,25 @@ def getLatestCommitsFrom(branchName, latestCommitHash): return stdout.decode("utf-8").strip() +def checkBranchExists(branchName): + gradlewFullPath = str(Path(__file__).parent.absolute()) + "/gradlew" + + print("branchName = \"" + str(branchName) + "\", gradlewFullPath = \"" + gradlewFullPath + "\"") + + arguments = [gradlewFullPath, + '-Pbranch_name=' + branchName, + 'checkBranchExistsTask', + '-q'] + + print("getLatestCommitsFrom() arguments: " + str(arguments)) + stdout = subprocess.check_output(arguments) + result = str(stdout.decode("utf-8").strip()) + print("result = " + result) + + if "fatal" in result: + return False + + return True if __name__ == '__main__': # First argument is the script full path which we don't need @@ -100,6 +119,11 @@ def getLatestCommitsFrom(branchName, latestCommitHash): branchName = sys.argv[3] latestCommitHash = "" + if not checkBranchExists: + print("main() requested branch does not exist, this is probably because it's a PR branch, so we don't want to " + "do anything") + exit(0) + if len(apkVersion) <= 0: print("main() Bad apk version code " + apkVersion) exit(-1) From 5fb53dfcbb900f2663db9311a077d729de170b3b Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Wed, 18 Sep 2019 18:57:37 +0300 Subject: [PATCH 145/184] test commit 1 --- Kuroba/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index e72a17ad09..9f0e016b0f 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -210,7 +210,7 @@ def getLastCommitsFromCommitByHash(String branchName, String from) { return stdout.toString().trim() } - + def checkBranchExists(String branchName) { def stdout = new ByteArrayOutputStream() exec { From 20be69fa314cad782bd724bb5efc2a1dc722c68e Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Wed, 18 Sep 2019 18:57:47 +0300 Subject: [PATCH 146/184] test commit 2 --- Kuroba/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 9f0e016b0f..c877250a32 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -210,7 +210,7 @@ def getLastCommitsFromCommitByHash(String branchName, String from) { return stdout.toString().trim() } - + def checkBranchExists(String branchName) { def stdout = new ByteArrayOutputStream() exec { From c6f0fd0279d19bda90acc16e06a6a419ceff47b3 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Wed, 18 Sep 2019 19:06:44 +0300 Subject: [PATCH 147/184] remove --first-parent --- Kuroba/app/build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index c877250a32..5fb6382f14 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -191,7 +191,7 @@ def getLatestCommit(String branchName) { def printArgs = "%H; %ad; %s" def dateFormat = "%Y-%m-%d %H:%M:%S" - commandLine 'git', 'log', branchName, '-n 1', "--pretty=format:${printArgs}", '--first-parent', "--date=format:${dateFormat}" + commandLine 'git', 'log', branchName, '-n 1', "--pretty=format:${printArgs}", "--date=format:${dateFormat}" standardOutput = stdout } @@ -204,13 +204,13 @@ def getLastCommitsFromCommitByHash(String branchName, String from) { def printArgs = "%H; %ad; %s" def dateFormat = "%Y-%m-%d %H:%M:%S" - commandLine 'git', 'log', branchName, "${from}..HEAD", "--pretty=format:${printArgs}", '--first-parent', "--date=format:${dateFormat}" + commandLine 'git', 'log', branchName, "${from}..HEAD", "--pretty=format:${printArgs}", "--date=format:${dateFormat}" standardOutput = stdout } return stdout.toString().trim() } - + def checkBranchExists(String branchName) { def stdout = new ByteArrayOutputStream() exec { From 2fcfb9feb9c9b2a206a1b92d87e8e660165a7e5b Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Wed, 18 Sep 2019 19:07:42 +0300 Subject: [PATCH 148/184] remove --first-parent, fix upload_apk script --- Kuroba/app/build.gradle | 4 ++-- Kuroba/upload_apk.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index e72a17ad09..5fb6382f14 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -191,7 +191,7 @@ def getLatestCommit(String branchName) { def printArgs = "%H; %ad; %s" def dateFormat = "%Y-%m-%d %H:%M:%S" - commandLine 'git', 'log', branchName, '-n 1', "--pretty=format:${printArgs}", '--first-parent', "--date=format:${dateFormat}" + commandLine 'git', 'log', branchName, '-n 1', "--pretty=format:${printArgs}", "--date=format:${dateFormat}" standardOutput = stdout } @@ -204,7 +204,7 @@ def getLastCommitsFromCommitByHash(String branchName, String from) { def printArgs = "%H; %ad; %s" def dateFormat = "%Y-%m-%d %H:%M:%S" - commandLine 'git', 'log', branchName, "${from}..HEAD", "--pretty=format:${printArgs}", '--first-parent', "--date=format:${dateFormat}" + commandLine 'git', 'log', branchName, "${from}..HEAD", "--pretty=format:${printArgs}", "--date=format:${dateFormat}" standardOutput = stdout } diff --git a/Kuroba/upload_apk.py b/Kuroba/upload_apk.py index a370faa9b3..086684caa5 100644 --- a/Kuroba/upload_apk.py +++ b/Kuroba/upload_apk.py @@ -119,7 +119,7 @@ def checkBranchExists(branchName): branchName = sys.argv[3] latestCommitHash = "" - if not checkBranchExists: + if not checkBranchExists(branchName): print("main() requested branch does not exist, this is probably because it's a PR branch, so we don't want to " "do anything") exit(0) From 98e23156d8c2a15a1a36b818df29431ff47b3b58 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Wed, 18 Sep 2019 19:15:57 +0300 Subject: [PATCH 149/184] upload_apk script improvements --- Kuroba/upload_apk.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Kuroba/upload_apk.py b/Kuroba/upload_apk.py index 086684caa5..045555d2a4 100644 --- a/Kuroba/upload_apk.py +++ b/Kuroba/upload_apk.py @@ -113,21 +113,21 @@ def checkBranchExists(branchName): "\n3. Branch name") exit(-1) - secretKey = sys.argv[1] - apkVersion = getApkVersionCode() - baseUrl = sys.argv[2] branchName = sys.argv[3] - latestCommitHash = "" - if not checkBranchExists(branchName): print("main() requested branch does not exist, this is probably because it's a PR branch, so we don't want to " "do anything") exit(0) + apkVersion = getApkVersionCode() if len(apkVersion) <= 0: print("main() Bad apk version code " + apkVersion) exit(-1) + secretKey = sys.argv[1] + baseUrl = sys.argv[2] + latestCommitHash = "" + try: latestCommitHash = getLatestCommitHash(baseUrl) except Exception as e: From 857bc4b700aa900c4e875d90bc8363996e4fbb2d Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Wed, 18 Sep 2019 19:39:20 +0300 Subject: [PATCH 150/184] test commit 3 --- Kuroba/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 5fb6382f14..015063d62d 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -226,7 +226,7 @@ task checkBranchExistsTask { doLast { println(checkBranchExists(branch_name)) } -} +} task getLatestCommitTask { doLast { From 56e140606036bdfc541c377ee54c6c254d6565be Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Wed, 18 Sep 2019 19:47:52 +0300 Subject: [PATCH 151/184] test commit 4 --- Kuroba/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 015063d62d..5fb6382f14 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -226,7 +226,7 @@ task checkBranchExistsTask { doLast { println(checkBranchExists(branch_name)) } -} +} task getLatestCommitTask { doLast { From ceb1021d73d0ac8edbf9a629192ee4e94935ec75 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Wed, 18 Sep 2019 19:48:13 +0300 Subject: [PATCH 152/184] update upload_apk script --- Kuroba/upload_apk.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Kuroba/upload_apk.py b/Kuroba/upload_apk.py index 045555d2a4..79b1f8e6c7 100644 --- a/Kuroba/upload_apk.py +++ b/Kuroba/upload_apk.py @@ -93,15 +93,20 @@ def checkBranchExists(branchName): 'checkBranchExistsTask', '-q'] - print("getLatestCommitsFrom() arguments: " + str(arguments)) - stdout = subprocess.check_output(arguments) - result = str(stdout.decode("utf-8").strip()) - print("result = " + result) + print("checkBranchExists() arguments: " + str(arguments)) - if "fatal" in result: - return False + try: + stdout = subprocess.check_output(arguments) + result = str(stdout.decode("utf-8").strip()) + print("result = " + result) - return True + if "fatal" in result: + return False + + return True + except Exception as e: + print("checkBranchExists() threw an exception, error: " + str(e)) + return False if __name__ == '__main__': # First argument is the script full path which we don't need From fc1bfd3f5860ffb5d06ca6939b560c5ea161c50a Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Wed, 18 Sep 2019 19:54:47 +0300 Subject: [PATCH 153/184] update upload_apk script --- Kuroba/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 5fb6382f14..015063d62d 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -226,7 +226,7 @@ task checkBranchExistsTask { doLast { println(checkBranchExists(branch_name)) } -} +} task getLatestCommitTask { doLast { From 342b7b6090165634f1596676fa4e588159f6a3b6 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Thu, 19 Sep 2019 15:50:52 +0300 Subject: [PATCH 154/184] Update gradle to use apk name with versionNameSuffix that depends on the current flavor In our case we will have two different flavors - "stable" and "dev" and the apk names are going to be "Kuroba.apk" for stable flavor and "Kuroba-dev.apk" for dev flavor Update upload_apk to support new apk names --- Kuroba/app/build.gradle | 88 ++++++++++++++++++++++++++--------------- Kuroba/upload_apk.py | 2 +- 2 files changed, 57 insertions(+), 33 deletions(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 5fb6382f14..4c1f92c38c 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -53,12 +53,6 @@ android { */ versionCode major * 10000 + minor * 100 + patch versionName "v" + major + "." + minor + "." + patch - - android.applicationVariants.all { variant -> - variant.outputs.all { - outputFileName = manifestPlaceholders.get("appName").toString() + ".apk" - } - } } compileOptions { @@ -87,7 +81,51 @@ android { exclude 'META-INF/LICENSE-W3C-TEST' } + flavorDimensions "default" + + productFlavors { + stable { + dimension "default" + applicationIdSuffix "" + versionNameSuffix "" + + //these are manifest placeholders for the application name and icon location + manifestPlaceholders = [ + appName : "Kuroba", + iconLoc : "@mipmap/ic_launcher", + fileProviderAuthority: "${applicationIdSuffix}.fileprovider" + ] + } + dev { + dimension "default" + applicationIdSuffix ".dev" + versionNameSuffix "-dev" + + //these are manifest placeholders for the application name and icon location + manifestPlaceholders = [ + appName : "Kuroba${versionNameSuffix}", + iconLoc : "@mipmap/ic_launcher", + fileProviderAuthority: "${applicationIdSuffix}.fileprovider" + ] + } + } + buildTypes { + // manifestPlaceholders do not work here for some reason so here is a little hack. + // We need to iterate each build variant and for each build variant we need to find it's + // flavor then we need to extract the versionNameSuffix from the flavor and update the output + // apk name with it. + android.applicationVariants.all { variant -> + variant.outputs.all { + Object flavor = getCurrentFlavor(variant.flavorName) + if (flavor == null) { + throw new GradleException("Coudln't find flavor by variant.flavorName = ${variant.flavorName}") + } + + outputFileName = "Kuroba${flavor.versionNameSuffix}.apk" + } + } + release { /* If you want to sign releases without using "Generate Signed APK", make a file in app/keys.properties with the following content: @@ -126,32 +164,6 @@ android { debuggable = true } } - - flavorDimensions "default" - - productFlavors { - stable { - dimension "default" - //these are manifest placeholders for the application name and icon location - manifestPlaceholders = [ - appName : "Kuroba", - iconLoc : "@mipmap/ic_launcher", - fileProviderAuthority: "${applicationIdSuffix}.fileprovider" - ] - } - dev { - dimension "default" - applicationIdSuffix ".dev" - versionNameSuffix "-dev" - - //these are manifest placeholders for the application name and icon location - manifestPlaceholders = [ - appName : "Kuroba${versionNameSuffix}", - iconLoc : "@mipmap/ic_launcher", - fileProviderAuthority: "${applicationIdSuffix}.fileprovider" - ] - } - } } dependencies { @@ -185,6 +197,18 @@ dependencies { implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' } +def getCurrentFlavor(String name) { + Object resultFlavor = null + + android.productFlavors.all { flavor -> + if (flavor.name == name) { + resultFlavor = flavor + } + } + + return resultFlavor +} + def getLatestCommit(String branchName) { def stdout = new ByteArrayOutputStream() exec { diff --git a/Kuroba/upload_apk.py b/Kuroba/upload_apk.py index 79b1f8e6c7..26b57b8162 100644 --- a/Kuroba/upload_apk.py +++ b/Kuroba/upload_apk.py @@ -30,7 +30,7 @@ def getLatestCommitHash(baseUrl): def uploadApk(baseUrl, headers, latestCommits): - apkPath = "app/build/outputs/apk/dev/debug/null.apk" # FIXME: change null to Kuroba when it works + apkPath = "app/build/outputs/apk/dev/debug/Kuroba-dev.apk" inFile = open(apkPath, "rb") try: if not inFile.readable(): From 425928dc14397c1986df0b5ba8b0596102c0b3fc Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Thu, 19 Sep 2019 16:20:12 +0300 Subject: [PATCH 155/184] Use ISO datetime for commits --- Kuroba/app/build.gradle | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 75cb4a004b..0c2ffec370 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -213,9 +213,8 @@ def getLatestCommit(String branchName) { def stdout = new ByteArrayOutputStream() exec { def printArgs = "%H; %ad; %s" - def dateFormat = "%Y-%m-%d %H:%M:%S" - commandLine 'git', 'log', branchName, '-n 1', "--pretty=format:${printArgs}", "--date=format:${dateFormat}" + commandLine 'git', 'log', branchName, '-n 1', "--pretty=format:${printArgs}", "--date=iso8601-strict" standardOutput = stdout } @@ -226,9 +225,8 @@ def getLastCommitsFromCommitByHash(String branchName, String from) { def stdout = new ByteArrayOutputStream() exec { def printArgs = "%H; %ad; %s" - def dateFormat = "%Y-%m-%d %H:%M:%S" - commandLine 'git', 'log', branchName, "${from}..HEAD", "--pretty=format:${printArgs}", "--date=format:${dateFormat}" + commandLine 'git', 'log', branchName, "${from}..HEAD", "--pretty=format:${printArgs}", "--date=iso8601-strict" standardOutput = stdout } From cd3a908bc91e9881c5e8ba97faa3683871530033 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Thu, 19 Sep 2019 16:42:47 +0300 Subject: [PATCH 156/184] test commit 1 --- Kuroba/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 0c2ffec370..af69c25775 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -219,7 +219,7 @@ def getLatestCommit(String branchName) { } return stdout.toString().trim() -} +} def getLastCommitsFromCommitByHash(String branchName, String from) { def stdout = new ByteArrayOutputStream() From ceeb4ecec5f4b114b3cbdb1678b116d9ef80957d Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Thu, 19 Sep 2019 16:42:57 +0300 Subject: [PATCH 157/184] test commit 2 --- Kuroba/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index af69c25775..e611d83cdd 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -219,7 +219,7 @@ def getLatestCommit(String branchName) { } return stdout.toString().trim() -} +} def getLastCommitsFromCommitByHash(String branchName, String from) { def stdout = new ByteArrayOutputStream() From 06ca9f1cd06977643c4a52d975b4f18849e5d881 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Thu, 19 Sep 2019 16:43:10 +0300 Subject: [PATCH 158/184] test commit 3 --- Kuroba/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index e611d83cdd..0c2ffec370 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -219,7 +219,7 @@ def getLatestCommit(String branchName) { } return stdout.toString().trim() -} +} def getLastCommitsFromCommitByHash(String branchName, String from) { def stdout = new ByteArrayOutputStream() From acf04aedba1bfbb3a1bb145885537f4a9df1572e Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Thu, 19 Sep 2019 17:00:17 +0300 Subject: [PATCH 159/184] Pass secret key via travis secure variables --- .travis.yml | 5 ++++- Kuroba/upload_apk.py | 20 ++++++++++++-------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index f3787f6107..f5307d3b3d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,7 +24,10 @@ script: - cd Kuroba || travis_terminate 1; - chmod +x gradlew || travis_terminate 2; - ./gradlew assembleDevDebug --console plain -x lint || travis_terminate 3; - - python3 upload_apk.py 1234567890 http://94.140.116.243:8080 multi-feature || travis_terminate 4; + - python3 upload_apk.py http://94.140.116.243:8080 multi-feature || travis_terminate 4; +env: + global: + - secure: "fnEtEqDioVaJ13bixTzHPpccuW5mZeBB5Fr7mF3KKffhfT+y0KSQd8UqO863MjtyiPCATvEs/P6GO/4Sm239fqime7regZbNuyIZ8EHZEOLM7Xkoe0zTxFXRFptN3T7VAIF/3FM9YYQ9m7DRWpu1MdJJGUMgl6V79roGGycHjHq6A8q9kCsp47lSxbMnTI/IVCjtB4Eo3RDSgXpPpk8b1jkXO35mMMEPGU9/d6+zXn8YCYcnMYrqqiPdb7G1gRwZ/snaYDRvr1KP3A8n17BXA8dSvBo7OyF1NU/f5KjK0kiQEmEqmT6bvIWQOBjG+b3VmkDA5EHnF0k0QZQnn5n/sYEWe0wUGlw2dOob8WQRaEESw6s43fAxG8qVu0+inM9tz8iHWvsPKUvzYy0sdi6NNMrvAO56fCqKZ/TqEiS5iv5k47TAeN+J3lrjqd0Qrc0zyAEa3mWmuCeZFclfeoxMWSorYSSlR9OegKXebqLPE3JCsVv+2W+zTGOahRVvflwCgYbgTeBwb19oc0X2WBaPdoJY4Wr8KpWYEqCUcoabhfKIFF5q9O7YToSancx6UMRZthd155XLkGJUUugQkBzARHzRpLOV99UJ8nmNIZnOMb7DmFUG+11X/RoUxQ2hcHHUH8K4GHC0azl2nCr7t8xX5pzFrwlQ9aQS26hp29FTMiI=" notifications: email: false \ No newline at end of file diff --git a/Kuroba/upload_apk.py b/Kuroba/upload_apk.py index 26b57b8162..b7973303ec 100644 --- a/Kuroba/upload_apk.py +++ b/Kuroba/upload_apk.py @@ -1,3 +1,4 @@ +import os import sys import requests import subprocess @@ -111,14 +112,18 @@ def checkBranchExists(branchName): if __name__ == '__main__': # First argument is the script full path which we don't need args = len(sys.argv) - 1 - if args != 3: - print("Bad arguments count, should be 3 got " + str(args) + ", expected arguments: " - "\n1. Secret key, " - "\n2. Base url, " - "\n3. Branch name") + if args != 2: + print("Bad arguments count, should be 2 got " + str(args) + ", expected arguments: " + "\n1. Base url, " + "\n2. Branch name") exit(-1) - branchName = sys.argv[3] + secretKey = os.environ.get('SECRETKEY') + print("secretKey = " + str(secretKey)) + + # TODO: exit with error when secretKey variable does not exist + + branchName = sys.argv[2] if not checkBranchExists(branchName): print("main() requested branch does not exist, this is probably because it's a PR branch, so we don't want to " "do anything") @@ -129,8 +134,7 @@ def checkBranchExists(branchName): print("main() Bad apk version code " + apkVersion) exit(-1) - secretKey = sys.argv[1] - baseUrl = sys.argv[2] + baseUrl = sys.argv[1] latestCommitHash = "" try: From d054039aff3d3c06e9625e3cd3ac6df7133b1920 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Thu, 19 Sep 2019 17:12:44 +0300 Subject: [PATCH 160/184] Update upload_apk script --- Kuroba/upload_apk.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Kuroba/upload_apk.py b/Kuroba/upload_apk.py index b7973303ec..04e9c0e664 100644 --- a/Kuroba/upload_apk.py +++ b/Kuroba/upload_apk.py @@ -119,9 +119,9 @@ def checkBranchExists(branchName): exit(-1) secretKey = os.environ.get('SECRETKEY') - print("secretKey = " + str(secretKey)) - - # TODO: exit with error when secretKey variable does not exist + if (secretKey is None): + print("Secret key is not provided via travis secure environment variable") + exit(-1) branchName = sys.argv[2] if not checkBranchExists(branchName): From 79fb0d0d782b2a9775c1d403e4e8587750f33029 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Thu, 19 Sep 2019 17:37:14 +0300 Subject: [PATCH 161/184] test commit 1 --- Kuroba/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 0c2ffec370..a3322e45ec 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -76,7 +76,7 @@ android { exclude 'META-INF/NOTICE.txt' exclude 'META-INF/notice.txt' exclude 'META-INF/ASL2.0' - exclude 'META-INF/LICENSE-LGPL-3.txt' + exclude 'META-INF/LICENSE-LGPL-3.txt' exclude 'META-INF/LICENSE-LGPL-2.1.txt' exclude 'META-INF/LICENSE-W3C-TEST' } From 89bcfd9c861b2c403f7c48defc2796d5e808be5d Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Thu, 19 Sep 2019 17:37:32 +0300 Subject: [PATCH 162/184] test commit 2 --- Kuroba/app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index a3322e45ec..1f7de20854 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -46,7 +46,7 @@ android { * * DON'T CHANGE THESE AUTOCALCULATIONS * USED FOR VERSION CODE GENERATION - * USED FOR VERSION NAME GENERATION + * USED FOR VERSION NAME GENERATION * USED FOR AUTO UPDATER NAMING CONSISTENCY * * ------------------------------------------------------------ @@ -76,7 +76,7 @@ android { exclude 'META-INF/NOTICE.txt' exclude 'META-INF/notice.txt' exclude 'META-INF/ASL2.0' - exclude 'META-INF/LICENSE-LGPL-3.txt' + exclude 'META-INF/LICENSE-LGPL-3.txt' exclude 'META-INF/LICENSE-LGPL-2.1.txt' exclude 'META-INF/LICENSE-W3C-TEST' } From 6338da8fa5843b1cf8cabc92b81823c479c48e03 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Thu, 19 Sep 2019 17:37:43 +0300 Subject: [PATCH 163/184] test commit 3 --- Kuroba/app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 1f7de20854..b17f036150 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -46,7 +46,7 @@ android { * * DON'T CHANGE THESE AUTOCALCULATIONS * USED FOR VERSION CODE GENERATION - * USED FOR VERSION NAME GENERATION + * USED FOR VERSION NAME GENERATION * USED FOR AUTO UPDATER NAMING CONSISTENCY * * ------------------------------------------------------------ @@ -248,7 +248,7 @@ task checkBranchExistsTask { doLast { println(checkBranchExists(branch_name)) } -} +} task getLatestCommitTask { doLast { From ee240fd32886376805f81ea4e0438a207f2eac31 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Thu, 19 Sep 2019 17:45:43 +0300 Subject: [PATCH 164/184] Disable ApkUpdater for dev builds for now (it doesn't work with the apk server now) --- Kuroba/app/build.gradle | 4 ++++ .../adamantcheese/chan/core/manager/UpdateManager.java | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index b17f036150..67ef7c17c1 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -89,6 +89,8 @@ android { applicationIdSuffix "" versionNameSuffix "" + buildConfigField "boolean", "DEV_BUILD", "false" + //these are manifest placeholders for the application name and icon location manifestPlaceholders = [ appName : "Kuroba", @@ -101,6 +103,8 @@ android { applicationIdSuffix ".dev" versionNameSuffix "-dev" + buildConfigField "boolean", "DEV_BUILD", "true" + //these are manifest placeholders for the application name and icon location manifestPlaceholders = [ appName : "Kuroba${versionNameSuffix}", diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/UpdateManager.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/UpdateManager.java index 5eccee9bb7..3ef883da1e 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/UpdateManager.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/UpdateManager.java @@ -28,6 +28,7 @@ import android.text.Html; import android.text.Spanned; import android.widget.Button; +import android.widget.Toast; import androidx.appcompat.app.AlertDialog; @@ -110,6 +111,14 @@ public void manualUpdateCheck() { } private void runUpdateApi(final boolean manual) { + if (BuildConfig.DEV_BUILD) { + Toast.makeText( + context, + "Updater is currently disabled for dev builds. Should be fixed pretty soon!", + Toast.LENGTH_LONG).show(); + return; + } + if (!manual) { long lastUpdateTime = ChanSettings.updateCheckTime.get(); long interval = 1000 * 60 * 60 * 24 * 5; //5 days From 9dff30cb0147b17a00305197c43bd54596b85ef9 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Fri, 20 Sep 2019 16:13:10 +0300 Subject: [PATCH 165/184] Add comments --- Kuroba/app/build.gradle | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 67ef7c17c1..48f4ca5881 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -91,7 +91,9 @@ android { buildConfigField "boolean", "DEV_BUILD", "false" - //these are manifest placeholders for the application name and icon location + // These are manifest placeholders for the application name, icon location and file + // provider authority (the file provider authority should differ for different flavors + // otherwise the app will not work) manifestPlaceholders = [ appName : "Kuroba", iconLoc : "@mipmap/ic_launcher", @@ -100,12 +102,15 @@ android { } dev { dimension "default" + // Different app ids for different flavors so that the users are able to install both + // of them without deleting anything applicationIdSuffix ".dev" versionNameSuffix "-dev" + // To easily figure out whether the app uses development flavors or not buildConfigField "boolean", "DEV_BUILD", "true" - //these are manifest placeholders for the application name and icon location + // The same as in stable flavor manifestPlaceholders = [ appName : "Kuroba${versionNameSuffix}", iconLoc : "@mipmap/ic_launcher", @@ -201,6 +206,10 @@ dependencies { implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' } +//======================================================== +// All of the below is being used by the upload_apk script +//======================================================== + def getCurrentFlavor(String name) { Object resultFlavor = null From 3f69a6f97f91b773a2444d402c465a66d3656f69 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Fri, 20 Sep 2019 20:42:59 +0300 Subject: [PATCH 166/184] Remove redundant line --- Kuroba/app/build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 48f4ca5881..0e202f481c 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -249,7 +249,6 @@ def getLastCommitsFromCommitByHash(String branchName, String from) { def checkBranchExists(String branchName) { def stdout = new ByteArrayOutputStream() exec { - commandLine 'git', 'rev-parse', '--verify', branchName standardOutput = stdout } From 15aa2b1987c5c78d59f1a114e86c4aeae8e526c5 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Fri, 20 Sep 2019 20:47:08 +0300 Subject: [PATCH 167/184] Add one important comment --- Kuroba/app/build.gradle | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 0e202f481c..068c3aa7dd 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -1,6 +1,11 @@ apply plugin: 'com.android.application' android { + // !!! IMPORTANT !!! + // When changing any of the following: compileSdkVersion, targetSdkVersion + // don't forget to also update travis.yml config + // !!! IMPORTANT !!! + compileSdkVersion 28 defaultConfig { From d410a80cf20f6fa13c8d4bbfcc05537248af9b23 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Fri, 20 Sep 2019 20:51:01 +0300 Subject: [PATCH 168/184] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index f81431d93c..39bf00a097 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[![Build Status](https://travis-ci.org/K1rakishou/Kuroba.svg?branch=multi-feature)](https://travis-ci.org/K1rakishou/Kuroba) + # Kuroba - imageboard browser for Android ## All releases are dev. Do not assume they are stable. From 80fe48d8194e6c8d188fd24b1d65f23505e14066 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Fri, 20 Sep 2019 20:52:06 +0300 Subject: [PATCH 169/184] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 39bf00a097..75354cc951 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ [APK releases](https://github.com/Adamantcheese/Kuroba/releases) +[DEV APKs](http://94.140.116.243:8080/) + Kuroba is a fast Android app for browsing imageboards, such as 4chan and 8chan. It adds inline replying, thread watching, notifications, themes, pass support, filters and a whole lot more. It is based on Clover by Floens, but has additional features added in because Floens doesn't want to merge PRs. ## License From 25262f220dc6defd0de63dcd2e01ce55ad045a60 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 21 Sep 2019 09:59:05 +0300 Subject: [PATCH 170/184] Update upload_apk script, trigger CI build --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f5307d3b3d..87a8c19d1d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,7 +27,7 @@ script: - python3 upload_apk.py http://94.140.116.243:8080 multi-feature || travis_terminate 4; env: global: - - secure: "fnEtEqDioVaJ13bixTzHPpccuW5mZeBB5Fr7mF3KKffhfT+y0KSQd8UqO863MjtyiPCATvEs/P6GO/4Sm239fqime7regZbNuyIZ8EHZEOLM7Xkoe0zTxFXRFptN3T7VAIF/3FM9YYQ9m7DRWpu1MdJJGUMgl6V79roGGycHjHq6A8q9kCsp47lSxbMnTI/IVCjtB4Eo3RDSgXpPpk8b1jkXO35mMMEPGU9/d6+zXn8YCYcnMYrqqiPdb7G1gRwZ/snaYDRvr1KP3A8n17BXA8dSvBo7OyF1NU/f5KjK0kiQEmEqmT6bvIWQOBjG+b3VmkDA5EHnF0k0QZQnn5n/sYEWe0wUGlw2dOob8WQRaEESw6s43fAxG8qVu0+inM9tz8iHWvsPKUvzYy0sdi6NNMrvAO56fCqKZ/TqEiS5iv5k47TAeN+J3lrjqd0Qrc0zyAEa3mWmuCeZFclfeoxMWSorYSSlR9OegKXebqLPE3JCsVv+2W+zTGOahRVvflwCgYbgTeBwb19oc0X2WBaPdoJY4Wr8KpWYEqCUcoabhfKIFF5q9O7YToSancx6UMRZthd155XLkGJUUugQkBzARHzRpLOV99UJ8nmNIZnOMb7DmFUG+11X/RoUxQ2hcHHUH8K4GHC0azl2nCr7t8xX5pzFrwlQ9aQS26hp29FTMiI=" + - secure: "gprEvVKY2eT3+fIMyMMYRlfxatt29pj2X6f6K7J4BnqwuZ9/EoHoE7sLKM/Z6H4Ssx2kaNFY25vTwtnv+D2rIxDsNbRFZEiGQ88nfQ6+0sqD8z+0/qgW1qZehzwBwj46wMb9wgMLxY9+LGIb/794/RQobHDaIbuKuwWak+23Qq7a445vNwkewSEyO57zh6GvVKMmWrFWBmb7DO/KItS+FFcKRy2k8qMpfgYShxjn0+Rm20oB3iZxACtyBDHxglahuCTo5h3ZMFmyBviiRDPS09rs8BbmMLGiSi6WQKVrWBTIkq+DbL6EIyIyGs3pNZZde38g8nES2Y02rKWbISwr2akVaTR7C+WRTV8VgPDDu4s4aAc8qQMyfVh921rdAmkoeMqikbNbJ7Z9PdRtD0axXlKqGdwKH9jRE639x4aIzDg6v8mMCdRzsO/2MCDc8XT0ylglFrpzlXZuvCrSI+eyUiN24nujufKR606SPPaf1Ifas6vh7NlZkpOzoH1Wj1nKksvfoeNxGADHgdTz5tsb//6MO1yTcdZgiLEC1u0pM79FXPvuV0bXlA8RTb+3s3wG0kxrMqXCfBVgGVURerCufOavfbENeMJiMdH0qMiNp0hGLIL/504ySYwOK+GuxjZOrXDnZ/ePKoYHOGkIyhAYYj9O0FFxFty7XgqdzrgdIhg=" notifications: email: false \ No newline at end of file From 07065238f6b963fa5f84548284d6f4e75bf1bd19 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 21 Sep 2019 10:08:31 +0300 Subject: [PATCH 171/184] Remove redundant line in travis script --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 87a8c19d1d..16f7ccba9d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,8 +17,6 @@ install: - sudo apt-get update - sudo apt-get install python3 - sudo apt-get -y install python3-pip - - python3 -V - - pip3 -V - pip3 install requests script: - cd Kuroba || travis_terminate 1; From 4861305146d2a8afa537cf63f17d180ddb10512c Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 19 Oct 2019 22:08:00 +0300 Subject: [PATCH 172/184] (#172) Migrate to a separate (home made) library that handles SAF bullshit a little bit more gracefully, called Fuck Storage Access Framework Fuck Storage Access Framework --- Kuroba/app/build.gradle | 11 +- .../adamantcheese/chan/StartActivity.java | 20 +- .../chan/core/cache/CacheHandler.java | 52 +-- .../chan/core/cache/FileCache.java | 38 +- .../chan/core/cache/FileCacheDownloader.java | 31 +- .../chan/core/cache/FileCacheListener.java | 2 +- .../database/DatabaseSavedThreadManager.java | 26 +- .../adamantcheese/chan/core/di/AppModule.java | 85 +++- .../chan/core/di/ManagerModule.java | 2 +- .../adamantcheese/chan/core/di/NetModule.java | 2 +- .../chan/core/di/RepositoryModule.java | 13 +- .../chan/core/image/ImageLoaderV2.java | 36 +- .../core/manager/SavedThreadLoaderManager.kt | 53 ++- .../chan/core/manager/ThreadSaveManager.java | 336 +++++++++------- .../chan/core/manager/UpdateManager.java | 2 +- .../core/presenter/ImageViewerPresenter.java | 3 +- .../ImportExportSettingsPresenter.java | 2 +- .../chan/core/presenter/ThreadPresenter.java | 9 +- .../core/repository/ImportExportRepository.kt | 29 +- .../repository/SavedThreadLoaderRepository.kt | 40 +- .../chan/core/saf/FileChooser.kt | 309 --------------- .../chan/core/saf/FileManager.kt | 362 ------------------ .../core/saf/annotation/ImmutableMethod.kt | 4 - .../chan/core/saf/annotation/MutableMethod.kt | 4 - .../chan/core/saf/callback/ChooserCallback.kt | 8 - .../saf/callback/DirectoryChooserCallback.kt | 3 - .../core/saf/callback/FileChooserCallback.kt | 3 - .../core/saf/callback/FileCreateCallback.kt | 3 - .../saf/callback/StartActivityCallbacks.kt | 7 - .../chan/core/saf/file/AbstractFile.kt | 302 --------------- .../chan/core/saf/file/ExternalFile.kt | 355 ----------------- .../chan/core/saf/file/FileDescriptorMode.kt | 13 - .../chan/core/saf/file/RawFile.kt | 209 ---------- .../chan/core/saver/ImageSaveTask.java | 30 +- .../chan/core/saver/ImageSaver.java | 30 +- .../ImportExportSettingsController.java | 17 +- .../ui/controller/LoadingViewController.java | 12 + .../controller/MediaSettingsController.java | 329 +++++----------- .../ui/controller/ViewThreadController.java | 17 +- .../chan/ui/helper/ImagePickDelegate.java | 8 +- .../base_directory/FilesBaseDirectory.kt | 10 + .../LocalThreadsBaseDirectory.kt | 10 + .../chan/ui/view/MultiImageView.java | 5 +- Kuroba/build.gradle | 6 +- 44 files changed, 683 insertions(+), 2165 deletions(-) delete mode 100644 Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileChooser.kt delete mode 100644 Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt delete mode 100644 Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/annotation/ImmutableMethod.kt delete mode 100644 Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/annotation/MutableMethod.kt delete mode 100644 Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/callback/ChooserCallback.kt delete mode 100644 Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/callback/DirectoryChooserCallback.kt delete mode 100644 Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/callback/FileChooserCallback.kt delete mode 100644 Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/callback/FileCreateCallback.kt delete mode 100644 Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/callback/StartActivityCallbacks.kt delete mode 100644 Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt delete mode 100644 Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt delete mode 100644 Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/FileDescriptorMode.kt delete mode 100644 Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt create mode 100644 Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/settings/base_directory/FilesBaseDirectory.kt create mode 100644 Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/settings/base_directory/LocalThreadsBaseDirectory.kt diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 4945b2ea39..02d432dc49 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -96,6 +96,12 @@ android { exclude 'META-INF/LICENSE-W3C-TEST' } + // TODO: delete before pushing + configurations.all { + // Check for updates every build + resolutionStrategy.cacheChangingModulesFor 0, 'seconds' + } + buildTypes { release { /* @@ -144,7 +150,6 @@ dependencies { implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'com.google.android.exoplayer:exoplayer:2.10.4' - implementation 'com.squareup.okhttp3:okhttp:4.1.0' implementation 'com.j256.ormlite:ormlite-core:5.1' @@ -162,5 +167,9 @@ dependencies { implementation 'io.reactivex.rxjava2:rxjava:2.2.12' implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + + // TODO: delete "changing = true" before pushing + implementation ('com.github.K1rakishou:Fuck-Storage-Access-Framework:develop-SNAPSHOT') { changing = true } + testImplementation 'junit:junit:4.12' } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/StartActivity.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/StartActivity.java index 962e732bfa..8a2e70c86e 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/StartActivity.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/StartActivity.java @@ -45,8 +45,6 @@ import com.github.adamantcheese.chan.core.model.orm.Loadable; import com.github.adamantcheese.chan.core.model.orm.Pin; import com.github.adamantcheese.chan.core.repository.SiteRepository; -import com.github.adamantcheese.chan.core.saf.FileManager; -import com.github.adamantcheese.chan.core.saf.callback.StartActivityCallbacks; import com.github.adamantcheese.chan.core.settings.ChanSettings; import com.github.adamantcheese.chan.core.site.Site; import com.github.adamantcheese.chan.core.site.SiteResolver; @@ -63,6 +61,8 @@ import com.github.adamantcheese.chan.ui.theme.ThemeHelper; import com.github.adamantcheese.chan.utils.AndroidUtils; import com.github.adamantcheese.chan.utils.Logger; +import com.github.k1rakishou.fsaf.FileChooser; +import com.github.k1rakishou.fsaf.callback.FSAFActivityCallbacks; import org.jetbrains.annotations.NotNull; @@ -80,7 +80,7 @@ public class StartActivity extends AppCompatActivity implements NfcAdapter.CreateNdefMessageCallback, - StartActivityCallbacks { + FSAFActivityCallbacks { private static final String TAG = "StartActivity"; private static final String STATE_KEY = "chan_state"; @@ -101,18 +101,14 @@ public class StartActivity @Inject DatabaseManager databaseManager; - @Inject WatchManager watchManager; - @Inject SiteResolver siteResolver; - @Inject SiteService siteService; - @Inject - FileManager fileManager; + FileChooser fileChooser; @Override protected void onCreate(Bundle savedInstanceState) { @@ -125,7 +121,7 @@ protected void onCreate(Bundle savedInstanceState) { Chan.injector().instance(ThemeHelper.class).setupContext(this); - fileManager.setCallbacks(this); + fileChooser.setCallbacks(this); imagePickDelegate = new ImagePickDelegate(this); runtimePermissionsHelper = new RuntimePermissionsHelper(this); @@ -546,7 +542,7 @@ protected void onDestroy() { return; } - fileManager.removeCallbacks(); + fileChooser.removeCallbacks(); // TODO: clear whole stack? stackTop().onHide(); @@ -558,7 +554,7 @@ protected void onDestroy() { protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); - if (fileManager.onActivityResult(requestCode, resultCode, data)) { + if (fileChooser.onActivityResult(requestCode, resultCode, data)) { return; } @@ -603,7 +599,7 @@ public void restartApp() { } @Override - public void myStartActivityForResult(@NotNull Intent intent, int requestCode) { + public void fsafStartActivityForResult(@NotNull Intent intent, int requestCode) { startActivityForResult(intent, requestCode); } } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/CacheHandler.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/CacheHandler.java index cfee9aaf78..786e7e004c 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/CacheHandler.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/CacheHandler.java @@ -22,10 +22,12 @@ import androidx.annotation.MainThread; import androidx.annotation.WorkerThread; -import com.github.adamantcheese.chan.core.saf.file.AbstractFile; -import com.github.adamantcheese.chan.core.saf.file.RawFile; import com.github.adamantcheese.chan.core.settings.ChanSettings; import com.github.adamantcheese.chan.utils.Logger; +import com.github.k1rakishou.fsaf.FileManager; +import com.github.k1rakishou.fsaf.file.AbstractFile; +import com.github.k1rakishou.fsaf.file.FileSegment; +import com.github.k1rakishou.fsaf.file.RawFile; import java.io.File; import java.io.IOException; @@ -44,6 +46,7 @@ public class CacheHandler { private static final String CACHE_EXTENSION = "cache"; private final ExecutorService pool = Executors.newSingleThreadExecutor(); + private final FileManager fileManager; private final RawFile cacheDirFile; /** @@ -53,7 +56,8 @@ public class CacheHandler { private AtomicLong size = new AtomicLong(); private AtomicBoolean trimRunning = new AtomicBoolean(false); - public CacheHandler(RawFile cacheDirFile) { + public CacheHandler(FileManager fileManager, RawFile cacheDirFile) { + this.fileManager = fileManager; this.cacheDirFile = cacheDirFile; createDirectories(); @@ -62,7 +66,7 @@ public CacheHandler(RawFile cacheDirFile) { @MainThread public boolean exists(String key) { - return get(key).exists(); + return fileManager.exists(get(key)); } @MainThread @@ -75,8 +79,8 @@ public RawFile get(String key) { // extensions String.valueOf(key.hashCode()), CACHE_EXTENSION); - return cacheDirFile.clone() - .appendFileNameSegment(fileName); + return (RawFile) cacheDirFile + .clone(new FileSegment(fileName)); } public File randomCacheFile() throws IOException { @@ -123,11 +127,11 @@ protected void fileWasAdded(long fileLen) { public void clearCache() { Logger.d(TAG, "Clearing cache"); - if (cacheDirFile.exists() && cacheDirFile.isDirectory()) { - for (AbstractFile file : cacheDirFile.listFiles()) { - if (!file.delete()) { + if (fileManager.exists(cacheDirFile) && fileManager.isDirectory(cacheDirFile)) { + for (AbstractFile file : fileManager.listFiles(cacheDirFile)) { + if (!fileManager.delete(file)) { Logger.d(TAG, "Could not delete cache file while clearing cache " + - file.getName()); + fileManager.getName(file)); } } } @@ -137,7 +141,7 @@ public void clearCache() { @MainThread public void createDirectories() { - if (!cacheDirFile.exists() && !cacheDirFile.create()) { + if (!fileManager.exists(cacheDirFile) && fileManager.create(cacheDirFile) == null) { throw new RuntimeException("Unable to create file cache dir " + cacheDirFile.getFullPath()); } } @@ -151,9 +155,9 @@ private void backgroundRecalculateSize() { private void recalculateSize() { long calculatedSize = 0; - List files = cacheDirFile.listFiles(); - for (RawFile file : files) { - calculatedSize += file.getLength(); + List files = fileManager.listFiles(cacheDirFile); + for (AbstractFile file : files) { + calculatedSize += fileManager.getLength(file); } size.set(calculatedSize); @@ -161,7 +165,7 @@ private void recalculateSize() { @WorkerThread private void trim() { - List directoryFiles = cacheDirFile.listFiles(); + List directoryFiles = fileManager.listFiles(cacheDirFile); // Don't try to trim empty directories or just one file in it. if (directoryFiles.size() <= 1) { @@ -169,26 +173,26 @@ private void trim() { } // Get all files with their last modified times. - List> files = new ArrayList<>(directoryFiles.size()); - for (RawFile file : directoryFiles) { - files.add(new Pair<>(file, file.lastModified())); + List> files = new ArrayList<>(directoryFiles.size()); + for (AbstractFile file : directoryFiles) { + files.add(new Pair<>(file, fileManager.lastModified(file))); } // Sort by oldest first. Collections.sort(files, (o1, o2) -> Long.signum(o1.second - o2.second)); //Pre-trim based on time, trash anything older than 6 hours - List> removed = new ArrayList<>(); - for (Pair fileLongPair : files) { + List> removed = new ArrayList<>(); + for (Pair fileLongPair : files) { if (fileLongPair.second + 6 * 60 * 60 * 1000 < System.currentTimeMillis()) { Logger.d(TAG, "Delete for trim " + fileLongPair.first.getFullPath()); - if (!fileLongPair.first.delete()) { + if (!fileManager.delete(fileLongPair.first)) { Logger.e(TAG, "Failed to delete cache file for trim"); } removed.add(fileLongPair); } else break; //only because we sorted earlier } - for (Pair deleted : removed) { + for (Pair deleted : removed) { files.remove(deleted); } recalculateSize(); @@ -199,9 +203,9 @@ private void trim() { AbstractFile file = files.get(i).first; Logger.d(TAG, "Delete for trim " + file.getFullPath()); - workingSize -= file.getLength(); + workingSize -= fileManager.getLength(file); - if (!file.delete()) { + if (!fileManager.delete(file)) { Logger.e(TAG, "Failed to delete cache file for trim"); } } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/FileCache.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/FileCache.java index bfce3b23f9..93258697e5 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/FileCache.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/FileCache.java @@ -22,10 +22,13 @@ import com.github.adamantcheese.chan.core.manager.ThreadSaveManager; import com.github.adamantcheese.chan.core.model.PostImage; import com.github.adamantcheese.chan.core.model.orm.Loadable; -import com.github.adamantcheese.chan.core.saf.FileManager; -import com.github.adamantcheese.chan.core.saf.file.AbstractFile; -import com.github.adamantcheese.chan.core.saf.file.RawFile; +import com.github.adamantcheese.chan.ui.settings.base_directory.LocalThreadsBaseDirectory; import com.github.adamantcheese.chan.utils.Logger; +import com.github.k1rakishou.fsaf.FileManager; +import com.github.k1rakishou.fsaf.file.AbstractFile; +import com.github.k1rakishou.fsaf.file.DirectorySegment; +import com.github.k1rakishou.fsaf.file.FileSegment; +import com.github.k1rakishou.fsaf.file.RawFile; import java.io.File; import java.io.IOException; @@ -50,7 +53,7 @@ public FileCache(File cacheDir, FileManager fileManager) { RawFile cacheDirFile = fileManager.fromRawFile( new File(cacheDir, FILE_CACHE_DIR)); - cacheHandler = new CacheHandler(cacheDirFile); + cacheHandler = new CacheHandler(fileManager, cacheDirFile); } public void clearCache() { @@ -71,12 +74,15 @@ public FileCacheDownloader downloadFile( postImage.originalName, postImage.extension); - if (!fileManager.baseLocalThreadsDirectoryExists()) { + if (!fileManager.baseDirectoryExists(LocalThreadsBaseDirectory.class)) { Logger.e(TAG, "Base local threads directory does not exist"); return null; } - AbstractFile baseDirFile = fileManager.newLocalThreadFile(); + AbstractFile baseDirFile = fileManager.newBaseDirectoryFile( + LocalThreadsBaseDirectory.class + ); + if (baseDirFile == null) { Logger.e(TAG, "downloadFile() fileManager.newLocalThreadFile() returned null"); return null; @@ -84,13 +90,14 @@ public FileCacheDownloader downloadFile( String imageDir = ThreadSaveManager.getImagesSubDir(loadable); - AbstractFile imageOnDiskFile = baseDirFile - .appendSubDirSegment(imageDir) - .appendFileNameSegment(filename); + AbstractFile imageOnDiskFile = baseDirFile.clone( + new DirectorySegment(imageDir), + new FileSegment(filename) + ); - if (imageOnDiskFile.exists() - && imageOnDiskFile.isFile() - && imageOnDiskFile.canRead()) { + if (fileManager.exists(imageOnDiskFile) + && fileManager.isFile(imageOnDiskFile) + && fileManager.canRead(imageOnDiskFile)) { handleFileImmediatelyAvailable(listener, imageOnDiskFile); } else { Logger.e(TAG, "Cannot load saved image from the disk, path: " @@ -130,11 +137,11 @@ public FileCacheDownloader downloadFile(@NonNull String url, FileCacheListener l } RawFile file = get(url); - if (file.exists()) { + if (fileManager.exists(file)) { handleFileImmediatelyAvailable(listener, file); return null; } else { - return handleStartDownload(listener, file, url); + return handleStartDownload(fileManager, listener, file, url); } } @@ -199,11 +206,12 @@ private void handleFileImmediatelyAvailable(FileCacheListener listener, Abstract } private FileCacheDownloader handleStartDownload( + FileManager fileManager, FileCacheListener listener, RawFile file, String url ) { - FileCacheDownloader downloader = new FileCacheDownloader(this, url, file); + FileCacheDownloader downloader = new FileCacheDownloader(fileManager, this, url, file); if (listener != null) { downloader.addListener(listener); } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/FileCacheDownloader.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/FileCacheDownloader.java index ec8b717958..a7ce60a679 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/FileCacheDownloader.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/FileCacheDownloader.java @@ -25,8 +25,9 @@ import com.github.adamantcheese.chan.Chan; import com.github.adamantcheese.chan.core.di.NetModule; -import com.github.adamantcheese.chan.core.saf.file.RawFile; import com.github.adamantcheese.chan.utils.Logger; +import com.github.k1rakishou.fsaf.FileManager; +import com.github.k1rakishou.fsaf.file.RawFile; import java.io.Closeable; import java.io.IOException; @@ -56,6 +57,7 @@ public class FileCacheDownloader implements Runnable { private final Handler handler; // Main thread only. + private final FileManager fileManager; private final Callback callback; private final List listeners = new ArrayList<>(); @@ -67,7 +69,8 @@ public class FileCacheDownloader implements Runnable { private Call call; private ResponseBody body; - public FileCacheDownloader(Callback callback, String url, RawFile output) { + public FileCacheDownloader(FileManager fileManager, Callback callback, String url, RawFile output) { + this.fileManager = fileManager; this.callback = callback; this.url = url; this.output = output; @@ -134,12 +137,12 @@ private void execute() { Source source = body.source(); sourceCloseable = source; - if (!output.exists() && !output.create()) { + if (!fileManager.exists(output) && fileManager.create(output) == null) { throw new IOException("Couldn't create output file, output = " + output.getFullPath()); } - outputFileOutputStream = output.getOutputStream(); + outputFileOutputStream = fileManager.getOutputStream(output); if (outputFileOutputStream == null) { throw new IOException("Couldn't get output file's OutputStream"); } @@ -153,7 +156,7 @@ private void execute() { pipeBody(source, sink); log("done"); - long fileLen = output.getLength(); + long fileLen = fileManager.getLength(output); handler.post(() -> { if (callback != null) { @@ -198,9 +201,17 @@ private void execute() { } }); } finally { - Util.closeQuietly(sourceCloseable); - Util.closeQuietly(sinkCloseable); - Util.closeQuietly(outputFileOutputStream); + if (sourceCloseable != null) { + Util.closeQuietly(sourceCloseable); + } + + if (sinkCloseable != null) { + Util.closeQuietly(sinkCloseable); + } + + if (outputFileOutputStream != null) { + Util.closeQuietly(outputFileOutputStream); + } if (call != null) { call.cancel(); @@ -280,8 +291,8 @@ private void checkCancel() throws IOException { @WorkerThread private void purgeOutput() { - if (output.exists()) { - final boolean deleteResult = output.delete(); + if (fileManager.exists(output)) { + final boolean deleteResult = fileManager.delete(output); if (!deleteResult) { log("could not delete the file in purgeOutput"); diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/FileCacheListener.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/FileCacheListener.java index 6584343bb0..d53a362a6f 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/FileCacheListener.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/FileCacheListener.java @@ -16,7 +16,7 @@ */ package com.github.adamantcheese.chan.core.cache; -import com.github.adamantcheese.chan.core.saf.file.RawFile; +import com.github.k1rakishou.fsaf.file.RawFile; public abstract class FileCacheListener { public void onProgress(long downloaded, long total) { diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/database/DatabaseSavedThreadManager.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/database/DatabaseSavedThreadManager.java index 6301849633..2bfa0fd230 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/database/DatabaseSavedThreadManager.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/database/DatabaseSavedThreadManager.java @@ -1,15 +1,15 @@ package com.github.adamantcheese.chan.core.database; -import android.net.Uri; - import com.github.adamantcheese.chan.core.manager.ThreadSaveManager; import com.github.adamantcheese.chan.core.model.orm.Loadable; import com.github.adamantcheese.chan.core.model.orm.SavedThread; -import com.github.adamantcheese.chan.core.saf.FileManager; -import com.github.adamantcheese.chan.core.saf.file.AbstractFile; import com.github.adamantcheese.chan.core.settings.ChanSettings; +import com.github.adamantcheese.chan.ui.settings.base_directory.LocalThreadsBaseDirectory; import com.github.adamantcheese.chan.utils.IOUtils; import com.github.adamantcheese.chan.utils.Logger; +import com.github.k1rakishou.fsaf.FileManager; +import com.github.k1rakishou.fsaf.file.AbstractFile; +import com.github.k1rakishou.fsaf.file.DirectorySegment; import com.j256.ormlite.stmt.DeleteBuilder; import java.io.File; @@ -182,21 +182,27 @@ public Callable deleteSavedThread(Loadable loadable) { public void deleteThreadFromDisk(Loadable loadable, boolean usesSAF) { if (usesSAF) { String threadSubDir = ThreadSaveManager.getThreadSubDir(loadable); - Uri uri = Uri.parse(ChanSettings.localThreadsLocationUri.get()); - AbstractFile localThreadsDir = fileManager.fromUri(uri); - if (localThreadsDir == null || !localThreadsDir.exists() || !localThreadsDir.isDirectory()) { + AbstractFile localThreadsDir = fileManager.newBaseDirectoryFile( + LocalThreadsBaseDirectory.class + ); + + if (localThreadsDir == null + || !fileManager.exists(localThreadsDir) + || !fileManager.isDirectory(localThreadsDir)) { // Probably already deleted return; } - AbstractFile threadDir = localThreadsDir.appendSubDirSegment(threadSubDir); - if (!threadDir.exists() || !threadDir.isDirectory()) { + AbstractFile threadDir = localThreadsDir + .clone(new DirectorySegment(threadSubDir)); + + if (!fileManager.exists(threadDir) || !fileManager.isDirectory(threadDir)) { // Probably already deleted return; } - if (!threadDir.delete()) { + if (!fileManager.delete(threadDir)) { Logger.d(TAG, "deleteThreadFromDisk() Could not delete SAF directory " + threadDir.getFullPath()); } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/AppModule.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/AppModule.java index 545dfe1883..79c23995e4 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/AppModule.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/AppModule.java @@ -18,19 +18,31 @@ import android.app.NotificationManager; import android.content.Context; +import android.net.Uri; + +import androidx.annotation.NonNull; import com.android.volley.RequestQueue; import com.android.volley.toolbox.ImageLoader; import com.github.adamantcheese.chan.core.image.ImageLoaderV2; import com.github.adamantcheese.chan.core.net.BitmapLruImageCache; -import com.github.adamantcheese.chan.core.saf.FileManager; import com.github.adamantcheese.chan.core.saver.ImageSaver; +import com.github.adamantcheese.chan.core.settings.ChanSettings; import com.github.adamantcheese.chan.ui.captcha.CaptchaHolder; +import com.github.adamantcheese.chan.ui.settings.base_directory.FilesBaseDirectory; +import com.github.adamantcheese.chan.ui.settings.base_directory.LocalThreadsBaseDirectory; import com.github.adamantcheese.chan.ui.theme.ThemeHelper; import com.github.adamantcheese.chan.utils.Logger; +import com.github.k1rakishou.fsaf.FileChooser; +import com.github.k1rakishou.fsaf.FileManager; +import com.github.k1rakishou.fsaf.manager.ExternalFileManager; +import com.github.k1rakishou.fsaf.manager.RawFileManager; +import com.github.k1rakishou.fsaf.manager.base_directory.DirectoryManager; import org.codejargon.feather.Provides; +import java.io.File; + import javax.inject.Singleton; import static android.content.Context.NOTIFICATION_SERVICE; @@ -98,6 +110,75 @@ public CaptchaHolder provideCaptchaHolder() { @Provides @Singleton public FileManager provideFileManager() { - return new FileManager(applicationContext); + DirectoryManager directoryManager = new DirectoryManager(); + ExternalFileManager externalFileManager = new ExternalFileManager( + applicationContext, + directoryManager + ); + RawFileManager rawFileManager = new RawFileManager(); + + LocalThreadsBaseDirectory localThreadsBaseDirectory = buildLocalThreadsBaseDirectory(); + FilesBaseDirectory filesBaseDirectory = buildImagesBaseDirectory(); + + // Add your base directories here + + FileManager fileManager = new FileManager( + applicationContext, + directoryManager, + externalFileManager, + rawFileManager + ); + + fileManager.registerBaseDir( + LocalThreadsBaseDirectory.class, + localThreadsBaseDirectory + ); + fileManager.registerBaseDir( + FilesBaseDirectory.class, + filesBaseDirectory + ); + + return fileManager; + } + + @Provides + @Singleton + public FileChooser provideFileChooser() { + return new FileChooser(applicationContext); + } + + private FilesBaseDirectory buildImagesBaseDirectory() { + Uri dirUri = null; + if (ChanSettings.saveLocationUri.get().length() > 0) { + dirUri = Uri.parse(ChanSettings.saveLocationUri.get()); + } + + File dirFile = null; + if (ChanSettings.saveLocation.get().length() > 0) { + dirFile = new File(ChanSettings.saveLocation.get()); + } + + return new FilesBaseDirectory( + dirUri, + dirFile + ); + } + + @NonNull + private LocalThreadsBaseDirectory buildLocalThreadsBaseDirectory() { + Uri dirUri = null; + if (ChanSettings.localThreadsLocationUri.get().length() > 0) { + dirUri = Uri.parse(ChanSettings.localThreadsLocationUri.get()); + } + + File dirFile = null; + if (ChanSettings.localThreadLocation.get().length() > 0) { + dirFile = new File(ChanSettings.localThreadLocation.get()); + } + + return new LocalThreadsBaseDirectory( + dirUri, + dirFile + ); } } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/ManagerModule.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/ManagerModule.java index 5cc9d19c2a..62d73c0fb4 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/ManagerModule.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/ManagerModule.java @@ -33,11 +33,11 @@ import com.github.adamantcheese.chan.core.pool.ChanLoaderFactory; import com.github.adamantcheese.chan.core.repository.BoardRepository; import com.github.adamantcheese.chan.core.repository.SavedThreadLoaderRepository; -import com.github.adamantcheese.chan.core.saf.FileManager; import com.github.adamantcheese.chan.core.settings.json.JsonSettings; import com.github.adamantcheese.chan.core.site.Site; import com.github.adamantcheese.chan.core.site.sites.chan4.Chan4; import com.github.adamantcheese.chan.utils.Logger; +import com.github.k1rakishou.fsaf.FileManager; import org.codejargon.feather.Provides; diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/NetModule.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/NetModule.java index ce81c1cace..2b7b4f4cfb 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/NetModule.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/NetModule.java @@ -21,10 +21,10 @@ import com.github.adamantcheese.chan.BuildConfig; import com.github.adamantcheese.chan.core.cache.FileCache; import com.github.adamantcheese.chan.core.net.ProxiedHurlStack; -import com.github.adamantcheese.chan.core.saf.FileManager; import com.github.adamantcheese.chan.core.settings.ChanSettings; import com.github.adamantcheese.chan.core.site.http.HttpCallManager; import com.github.adamantcheese.chan.utils.Logger; +import com.github.k1rakishou.fsaf.FileManager; import org.codejargon.feather.Provides; diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/RepositoryModule.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/RepositoryModule.java index 0aeeca3901..11f47458c0 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/RepositoryModule.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/RepositoryModule.java @@ -24,6 +24,7 @@ import com.github.adamantcheese.chan.core.repository.SavedThreadLoaderRepository; import com.github.adamantcheese.chan.core.repository.SiteRepository; import com.github.adamantcheese.chan.utils.Logger; +import com.github.k1rakishou.fsaf.FileManager; import com.google.gson.Gson; import org.codejargon.feather.Provides; @@ -37,10 +38,11 @@ public class RepositoryModule { public ImportExportRepository provideImportExportRepository( DatabaseManager databaseManager, DatabaseHelper databaseHelper, - Gson gson + Gson gson, + FileManager fileManager ) { Logger.d(AppModule.DI_TAG, "Import export repository"); - return new ImportExportRepository(databaseManager, databaseHelper, gson); + return new ImportExportRepository(databaseManager, databaseHelper, gson, fileManager); } @Provides @@ -71,8 +73,11 @@ public LastReplyRepository provideLastReplyRepository() { @Provides @Singleton - public SavedThreadLoaderRepository provideSavedThreadLoaderRepository(Gson gson) { + public SavedThreadLoaderRepository provideSavedThreadLoaderRepository( + Gson gson, + FileManager fileManager + ) { Logger.d(AppModule.DI_TAG, "Saved thread loader repository"); - return new SavedThreadLoaderRepository(gson); + return new SavedThreadLoaderRepository(gson, fileManager); } } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/image/ImageLoaderV2.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/image/ImageLoaderV2.java index 54af87cf51..22c46ffbe0 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/image/ImageLoaderV2.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/image/ImageLoaderV2.java @@ -10,11 +10,14 @@ import com.github.adamantcheese.chan.core.manager.ThreadSaveManager; import com.github.adamantcheese.chan.core.model.PostImage; import com.github.adamantcheese.chan.core.model.orm.Loadable; -import com.github.adamantcheese.chan.core.saf.FileManager; -import com.github.adamantcheese.chan.core.saf.file.AbstractFile; +import com.github.adamantcheese.chan.ui.settings.base_directory.LocalThreadsBaseDirectory; import com.github.adamantcheese.chan.utils.BackgroundUtils; import com.github.adamantcheese.chan.utils.Logger; import com.github.adamantcheese.chan.utils.StringUtils; +import com.github.k1rakishou.fsaf.FileManager; +import com.github.k1rakishou.fsaf.file.AbstractFile; +import com.github.k1rakishou.fsaf.file.DirectorySegment; +import com.github.k1rakishou.fsaf.file.FileSegment; import java.io.IOException; import java.io.InputStream; @@ -114,13 +117,17 @@ public ImageContainer getFromDisk( diskLoaderExecutor.execute(() -> { try { - if (!fileManager.baseLocalThreadsDirectoryExists()) { + if (!fileManager.baseDirectoryExists(LocalThreadsBaseDirectory.class)) { throw new IOException("Base local threads directory does not exist"); } - AbstractFile baseDirFile = fileManager.newLocalThreadFile(); + AbstractFile baseDirFile = fileManager.newBaseDirectoryFile( + LocalThreadsBaseDirectory.class + ); + if (baseDirFile == null) { - throw new IOException("getFromDisk() fileManager.newLocalThreadFile() returned null"); + throw new IOException("getFromDisk() " + + "fileManager.newLocalThreadFile() returned null"); } String imageDir; @@ -131,24 +138,25 @@ public ImageContainer getFromDisk( } AbstractFile imageOnDiskFile = baseDirFile - .appendSubDirSegment(imageDir) - .appendFileNameSegment(filename); + .clone(new DirectorySegment(imageDir), new FileSegment(filename)); + + boolean exists = fileManager.exists(imageOnDiskFile); + boolean isFile = fileManager.isFile(imageOnDiskFile); + boolean canRead = fileManager.canRead(imageOnDiskFile); - if (!imageOnDiskFile.exists() - || !imageOnDiskFile.isFile() - || !imageOnDiskFile.canRead()) { + if (!exists || !isFile || !canRead) { String errorMessage = "Could not load image from the disk: " + "(path = " + imageOnDiskFile.getFullPath() + - ", exists = " + imageOnDiskFile.exists() + - ", isFile = " + imageOnDiskFile.isFile() + - ", canRead = " + imageOnDiskFile.canRead() + ")"; + ", exists = " + exists + + ", isFile = " + isFile + + ", canRead = " + canRead + ")"; Logger.e(TAG, errorMessage); postError(container, errorMessage); return; } - try (InputStream inputStream = imageOnDiskFile.getInputStream()) { + try (InputStream inputStream = fileManager.getInputStream(imageOnDiskFile)) { // Image exists on the disk - try to load it and put in the cache BitmapFactory.Options bitmapOptions = new BitmapFactory.Options(); bitmapOptions.outWidth = width; diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/SavedThreadLoaderManager.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/SavedThreadLoaderManager.kt index 420d7a5b90..09a36ad76c 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/SavedThreadLoaderManager.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/SavedThreadLoaderManager.kt @@ -4,16 +4,19 @@ import com.github.adamantcheese.chan.core.mapper.ThreadMapper import com.github.adamantcheese.chan.core.model.ChanThread import com.github.adamantcheese.chan.core.model.orm.Loadable import com.github.adamantcheese.chan.core.repository.SavedThreadLoaderRepository -import com.github.adamantcheese.chan.core.saf.FileManager +import com.github.adamantcheese.chan.ui.settings.base_directory.LocalThreadsBaseDirectory import com.github.adamantcheese.chan.utils.BackgroundUtils import com.github.adamantcheese.chan.utils.Logger +import com.github.k1rakishou.fsaf.FileManager +import com.github.k1rakishou.fsaf.file.DirectorySegment +import com.github.k1rakishou.fsaf.file.FileSegment import java.io.IOException import javax.inject.Inject -class SavedThreadLoaderManager @Inject -constructor( +class SavedThreadLoaderManager @Inject constructor( private val savedThreadLoaderRepository: SavedThreadLoaderRepository, - private val fileManager: FileManager) { + private val fileManager: FileManager +) { fun loadSavedThread(loadable: Loadable): ChanThread? { if (BackgroundUtils.isMainThread()) { @@ -21,43 +24,53 @@ constructor( } val threadSubDir = ThreadSaveManager.getThreadSubDir(loadable) - val baseDir = fileManager.newLocalThreadFile() + val baseDir = fileManager.newBaseDirectoryFile(LocalThreadsBaseDirectory::class.java) if (baseDir == null) { Logger.e(TAG, "loadSavedThread() fileManager.newLocalThreadFile() returned null") return null } - val threadSaveDir = baseDir.appendSubDirSegment(threadSubDir) - if (!threadSaveDir.exists() || !threadSaveDir.isDirectory()) { + val threadSaveDir = baseDir + .clone(DirectorySegment(threadSubDir)) + + val threadSaveDirExists = fileManager.exists(threadSaveDir) + val threadSaveDirIsDirectory = fileManager.isDirectory(threadSaveDir) + + if (!threadSaveDirExists || !threadSaveDirIsDirectory) { Logger.e(TAG, "threadSaveDir does not exist or is not a directory: " + "(path = " + threadSaveDir.getFullPath() - + ", exists = " + threadSaveDir.exists() - + ", isDir = " + threadSaveDir.isDirectory() + ")") + + ", exists = " + threadSaveDirExists + + ", isDir = " + threadSaveDirIsDirectory + ")") return null } val threadFile = threadSaveDir - .clone() - .appendFileNameSegment(SavedThreadLoaderRepository.THREAD_FILE_NAME) + .clone(FileSegment(SavedThreadLoaderRepository.THREAD_FILE_NAME)) - if (!threadFile.exists() || !threadFile.isFile() || !threadFile.canRead()) { + val threadFileExists = fileManager.exists(threadFile) + val threadFileIsFile = fileManager.isFile(threadFile) + val threadFileCanRead = fileManager.canRead(threadFile) + + if (!threadFileExists || !threadFileIsFile || !threadFileCanRead) { Logger.e(TAG, "threadFile does not exist or not a file or cannot be read: " + "(path = " + threadFile.getFullPath() - + ", exists = " + threadFile.exists() - + ", isFile = " + threadFile.isFile() - + ", canRead = " + threadFile.canRead() + ")") + + ", exists = " + threadFileExists + + ", isFile = " + threadFileIsFile + + ", canRead = " + threadFileCanRead + ")") return null } val threadSaveDirImages = threadSaveDir - .clone() - .appendSubDirSegment("images") + .clone(DirectorySegment("images")) + + val imagesDirExists = fileManager.exists(threadSaveDirImages) + val imagesDirIsDir = fileManager.isDirectory(threadSaveDirImages) - if (!threadSaveDirImages.exists() || !threadSaveDirImages.isDirectory()) { + if (!imagesDirExists || !imagesDirIsDir) { Logger.e(TAG, "threadSaveDirImages does not exist or is not a directory: " + "(path = " + threadSaveDirImages.getFullPath() - + ", exists = " + threadSaveDirImages.exists() - + ", isDir = " + threadSaveDirImages.isDirectory() + ")") + + ", exists = " + imagesDirExists + + ", isDir = " + imagesDirIsDir + ")") return null } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/ThreadSaveManager.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/ThreadSaveManager.java index 87e8266207..b775bbc530 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/ThreadSaveManager.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/ThreadSaveManager.java @@ -13,13 +13,16 @@ import com.github.adamantcheese.chan.core.model.orm.Loadable; import com.github.adamantcheese.chan.core.model.save.SerializableThread; import com.github.adamantcheese.chan.core.repository.SavedThreadLoaderRepository; -import com.github.adamantcheese.chan.core.saf.FileManager; -import com.github.adamantcheese.chan.core.saf.file.AbstractFile; import com.github.adamantcheese.chan.core.settings.ChanSettings; +import com.github.adamantcheese.chan.ui.settings.base_directory.LocalThreadsBaseDirectory; import com.github.adamantcheese.chan.utils.BackgroundUtils; import com.github.adamantcheese.chan.utils.IOUtils; import com.github.adamantcheese.chan.utils.Logger; import com.github.adamantcheese.chan.utils.StringUtils; +import com.github.k1rakishou.fsaf.FileManager; +import com.github.k1rakishou.fsaf.file.AbstractFile; +import com.github.k1rakishou.fsaf.file.DirectorySegment; +import com.github.k1rakishou.fsaf.file.FileSegment; import java.io.File; import java.io.IOException; @@ -181,7 +184,7 @@ public boolean enqueueThreadToSave( throw new RuntimeException("Must be executed on the main thread"); } - if (!fileManager.baseLocalThreadsDirectoryExists()) { + if (!fileManager.baseDirectoryExists(LocalThreadsBaseDirectory.class)) { Logger.e(TAG, "Base local threads directory does not exist, can't start downloading"); return false; } @@ -315,7 +318,6 @@ private void onDownloadingError(Throwable error, Loadable loadable) { * @param loadable is a unique identifier of a thread we are saving. * @param postsToSave posts of a thread to be saved. */ - @SuppressLint("CheckResult") private Single saveThreadInternal(@NonNull Loadable loadable, List postsToSave) { return Single.fromCallable(() -> { if (BackgroundUtils.isMainThread()) { @@ -340,20 +342,20 @@ private Single saveThreadInternal(@NonNull Loadable loadable, List saveThreadInternal(@NonNull Loadable loadable, List downloadSpoilerImage( + downloadInternal( loadable, + threadSaveDirImages, boardSaveDir, - spoilerImageUrl) - ) - .flatMap((res) -> { - // For each post create a new inner rx stream (so they can be processed in parallel) - return Flowable.fromIterable(newPosts) - // Here we create a separate reactive stream for each image request. - // But we use an executor service with limited threads amount, so there - // will be only this much at a time. - // | - // / | \ - // / | \ - // / | \ - // V V V // Separate streams. - // | | | - // o o o // Download images in parallel. - // | | | - // V V V // Combine them back to a single stream. - // \ | / - // \ | / - // \ | / - // | - .flatMap((post) -> { - return downloadImages( - loadable, - threadSaveDirImages, - post, - currentImageDownloadIndex, - postsWithImages, - imageDownloadsWithIoError, - maxImageIoErrors); - }) - .toList() - .doOnSuccess((list) -> Logger.d(TAG, "PostImage download result list = " + list)); - }) - .flatMap((res) -> { - return Single.defer(() -> { - if (!isCurrentDownloadRunning(loadable)) { - if (isCurrentDownloadStopped(loadable)) { - Logger.d(TAG, "Thread downloading has been stopped " - + loadableToString(loadable)); - } else { - Logger.d(TAG, "Thread downloading has been canceled " - + loadableToString(loadable)); - } - - return Single.just(false); - } - - updateLastSavedPostNo(loadable, newPosts); - - Logger.d(TAG, "Successfully updated a thread " + loadableToString(loadable)); - return Single.just(true); - }); - }) - // Have to use blockingGet here. This is a place where all of the exception will come - // out from - .blockingGet(); + newPosts, + postsWithImages, + maxImageIoErrors, + spoilerImageUrl, + currentImageDownloadIndex, + imageDownloadsWithIoError); } finally { if (shouldDeleteDownloadedFiles(loadable)) { if (isCurrentDownloadStopped(loadable)) { @@ -470,49 +422,124 @@ private Single saveThreadInternal(@NonNull Loadable loadable, List newPosts, + int postsWithImages, + int maxImageIoErrors, + HttpUrl spoilerImageUrl, + AtomicInteger currentImageDownloadIndex, + AtomicInteger imageDownloadsWithIoError + ) { + Single.fromCallable(() -> downloadSpoilerImage( + loadable, + boardSaveDir, + spoilerImageUrl) + ) + .flatMap((res) -> { + // For each post create a new inner rx stream (so they can be processed in parallel) + return Flowable.fromIterable(newPosts) + // Here we create a separate reactive stream for each image request. + // But we use an executor service with limited threads amount, so there + // will be only this much at a time. + // | + // / | \ + // / | \ + // / | \ + // V V V // Separate streams. + // | | | + // o o o // Download images in parallel. + // | | | + // V V V // Combine them back to a single stream. + // \ | / + // \ | / + // \ | / + // | + .flatMap((post) -> { + return downloadImages( + loadable, + threadSaveDirImages, + post, + currentImageDownloadIndex, + postsWithImages, + imageDownloadsWithIoError, + maxImageIoErrors); + }) + .toList() + .doOnSuccess((list) -> Logger.d(TAG, "PostImage download result list = " + list)); + }) + .flatMap((res) -> { + return Single.defer(() -> { + return tryUpdateLastSavedPostNo(loadable, newPosts); + }); + }) + // Have to use blockingGet here. This is a place where all of the exception will come + // out from + .blockingGet(); + } + + private Single tryUpdateLastSavedPostNo(@NonNull Loadable loadable, List newPosts) { + if (!isCurrentDownloadRunning(loadable)) { + if (isCurrentDownloadStopped(loadable)) { + Logger.d(TAG, "Thread downloading has been stopped " + + loadableToString(loadable)); + } else { + Logger.d(TAG, "Thread downloading has been canceled " + + loadableToString(loadable)); + } + + return Single.just(false); + } + + updateLastSavedPostNo(loadable, newPosts); + + Logger.d(TAG, "Successfully updated a thread " + loadableToString(loadable)); + return Single.just(true); + } + private AbstractFile getBoardSaveDir(Loadable loadable) throws IOException { - if (!fileManager.baseLocalThreadsDirectoryExists()) { + if (!fileManager.baseDirectoryExists(LocalThreadsBaseDirectory.class)) { throw new IOException("getBoardSaveDir() Base local threads directory does not exist"); } - AbstractFile baseDir = fileManager.newLocalThreadFile(); + AbstractFile baseDir = fileManager.newBaseDirectoryFile(LocalThreadsBaseDirectory.class); if (baseDir == null) { throw new IOException("getBoardSaveDir() fileManager.newLocalThreadFile() returned null"); } - String boardSubDir = getBoardSubDir(loadable); return baseDir - .appendSubDirSegment(boardSubDir); + .clone(new DirectorySegment(getBoardSubDir(loadable))); } private AbstractFile getThreadSaveDir(Loadable loadable) throws IOException { - if (!fileManager.baseLocalThreadsDirectoryExists()) { + if (!fileManager.baseDirectoryExists(LocalThreadsBaseDirectory.class)) { throw new IOException("getThreadSaveDir() Base local threads directory does not exist"); } - AbstractFile baseDir = fileManager.newLocalThreadFile(); + AbstractFile baseDir = fileManager.newBaseDirectoryFile(LocalThreadsBaseDirectory.class); if (baseDir == null) { throw new IOException("getThreadSaveDir() fileManager.newLocalThreadFile() returned null"); } - String threadSubDir = getThreadSubDir(loadable); return baseDir - .appendSubDirSegment(threadSubDir); + .clone(new DirectorySegment(getThreadSubDir(loadable))); } private void dealWithMediaScanner(AbstractFile threadSaveDirImages) throws CouldNotCreateNoMediaFile { AbstractFile noMediaFile = threadSaveDirImages - .clone() - .appendFileNameSegment(NO_MEDIA_FILE_NAME); + .clone(new FileSegment(NO_MEDIA_FILE_NAME)); if (!ChanSettings.allowMediaScannerToScanLocalThreads.get()) { // .nomedia file being in the images directory "should" prevent media scanner from // scanning this directory - if (!noMediaFile.exists() && !noMediaFile.create()) { + if (!fileManager.exists(noMediaFile) && fileManager.create(noMediaFile) == null) { throw new CouldNotCreateNoMediaFile(threadSaveDirImages); } } else { - if (noMediaFile.exists() && !noMediaFile.delete()) { + if (fileManager.exists(noMediaFile) && !fileManager.delete(noMediaFile)) { Logger.e(TAG, "Could not delete .nomedia file from directory " + threadSaveDirImages.getFullPath()); } @@ -534,7 +561,9 @@ private int calculateMaxImageIoErrors(int postsWithImages) { private void updateLastSavedPostNo(Loadable loadable, List newPosts) { // Update the latests saved post id in the database int lastPostNo = newPosts.get(newPosts.size() - 1).no; - databaseManager.runTask(databaseSavedThreadManager.updateLastSavedPostNo(loadable.id, lastPostNo)); + databaseManager.runTask(databaseSavedThreadManager.updateLastSavedPostNo( + loadable.id, + lastPostNo)); } /** @@ -557,79 +586,95 @@ private int calculateAmountOfPostsWithImages(List newPosts) { * If a post has at least one image that has not been downloaded yet it will be * redownloaded again */ - private List filterAndSortPosts(AbstractFile threadSaveDirImages, Loadable loadable, List inputPosts) { - // Filter out already saved posts (by lastSavedPostNo) - int lastSavedPostNo = databaseManager.runTask(databaseSavedThreadManager.getLastSavedPostNo(loadable.id)); - - // Use HashSet to avoid duplicates - Set filteredPosts = new HashSet<>(inputPosts.size() / 2); - - // lastSavedPostNo == 0 means that we don't have this thread downloaded yet - if (lastSavedPostNo > 0) { - for (Post post : inputPosts) { - if (!checkWhetherAllPostImagesAreAlreadySaved(threadSaveDirImages, post)) { - // Some of the post's images could not be downloaded during the previous download - // so we need to download them now - if (VERBOSE_LOG) { - Logger.d(TAG, "Found not downloaded yet images for a post " + post.no + - ", for loadable " + loadableToString(loadable)); + // FIXME: VERY SLOW!!! + private List filterAndSortPosts( + AbstractFile threadSaveDirImages, + Loadable loadable, + List inputPosts + ) { + long start = System.currentTimeMillis(); + + try { + // Filter out already saved posts (by lastSavedPostNo) + int lastSavedPostNo = databaseManager.runTask( + databaseSavedThreadManager.getLastSavedPostNo(loadable.id)); + + // Use HashSet to avoid duplicates + Set filteredPosts = new HashSet<>(inputPosts.size() / 2); + + // lastSavedPostNo == 0 means that we don't have this thread downloaded yet + if (lastSavedPostNo > 0) { + for (Post post : inputPosts) { + if (!checkWhetherAllPostImagesAreAlreadySaved(threadSaveDirImages, post)) { + // Some of the post's images could not be downloaded during the previous download + // so we need to download them now + if (VERBOSE_LOG) { + Logger.d(TAG, "Found not downloaded yet images for a post " + post.no + + ", for loadable " + loadableToString(loadable)); + } + + filteredPosts.add(post); + continue; } - filteredPosts.add(post); - continue; + if (post.no > lastSavedPostNo) { + filteredPosts.add(post); + } } + } else { + filteredPosts.addAll(inputPosts); + } - if (post.no > lastSavedPostNo) { - filteredPosts.add(post); - } + if (filteredPosts.isEmpty()) { + return Collections.emptyList(); } - } else { - filteredPosts.addAll(inputPosts); - } - if (filteredPosts.isEmpty()) { - return Collections.emptyList(); - } + // And sort them + List posts = new ArrayList<>(filteredPosts); + Collections.sort(posts, postComparator); - // And sort them - List posts = new ArrayList<>(filteredPosts); - Collections.sort(posts, postComparator); + return posts; + } finally { + long delta = System.currentTimeMillis() - start; + String loadableString = loadableToString(loadable); - return posts; + Logger.d(TAG, "filterAndSortPosts() completed in " + + delta + "ms for loadable " + loadableString); + } } private boolean checkWhetherAllPostImagesAreAlreadySaved( AbstractFile threadSaveDirImages, - Post post) { + Post post + ) { for (PostImage postImage : post.images) { { String originalImageFilename = postImage.originalName + "_" + ORIGINAL_FILE_NAME + "." + postImage.extension; AbstractFile originalImage = threadSaveDirImages - .clone() - .appendFileNameSegment(originalImageFilename); + .clone(new FileSegment(originalImageFilename)); - if (!originalImage.exists()) { + if (!fileManager.exists(originalImage)) { return false; } - if (!originalImage.canRead()) { - if (!originalImage.delete()) { + if (!fileManager.canRead(originalImage)) { + if (!fileManager.delete(originalImage)) { Logger.e(TAG, "Could not delete originalImage with path " + originalImage.getFullPath()); } return false; } - long length = originalImage.getLength(); + long length = fileManager.getLength(originalImage); if (length == -1L) { throw new IllegalStateException("originalImage.getLength() returned -1, " + "originalImagePath = " + originalImage.getFullPath()); } if (length == 0L) { - if (!originalImage.delete()) { + if (!fileManager.delete(originalImage)) { Logger.e(TAG, "Could not delete originalImage with path " + originalImage.getFullPath()); } @@ -644,29 +689,28 @@ private boolean checkWhetherAllPostImagesAreAlreadySaved( + THUMBNAIL_FILE_NAME + "." + thumbnailExtension; AbstractFile thumbnailImage = threadSaveDirImages - .clone() - .appendFileNameSegment(thumbnailImageFilename); + .clone(new FileSegment(thumbnailImageFilename)); - if (!thumbnailImage.exists()) { + if (!fileManager.exists(thumbnailImage)) { return false; } - if (!thumbnailImage.canRead()) { - if (!thumbnailImage.delete()) { + if (!fileManager.canRead(thumbnailImage)) { + if (!fileManager.delete(thumbnailImage)) { Logger.e(TAG, "Could not delete thumbnailImage with path " + thumbnailImage.getFullPath()); } return false; } - long length = thumbnailImage.getLength(); + long length = fileManager.getLength(thumbnailImage); if (length == -1L) { throw new IllegalStateException("thumbnailImage.getLength() returned -1, " + "thumbnailImagePath = " + thumbnailImage.getFullPath()); } if (length == 0L) { - if (!thumbnailImage.delete()) { + if (!fileManager.delete(thumbnailImage)) { Logger.e(TAG, "Could not delete thumbnailImage with path " + thumbnailImage.getFullPath()); } @@ -681,7 +725,8 @@ private boolean checkWhetherAllPostImagesAreAlreadySaved( private boolean downloadSpoilerImage( Loadable loadable, AbstractFile threadSaveDirImages, - HttpUrl spoilerImageUrl) throws IOException { + HttpUrl spoilerImageUrl + ) throws IOException { // If the board uses spoiler image - download it if (loadable.board.spoilers && spoilerImageUrl != null) { String spoilerImageExtension = StringUtils.extractFileExtensionFromImageUrl( @@ -695,9 +740,8 @@ private boolean downloadSpoilerImage( String spoilerImageName = SPOILER_FILE_NAME + "." + spoilerImageExtension; AbstractFile spoilerImageFullPath = threadSaveDirImages - .clone() - .appendFileNameSegment(spoilerImageName); - if (spoilerImageFullPath.exists()) { + .clone(new FileSegment(spoilerImageName)); + if (fileManager.exists(spoilerImageFullPath)) { // Do nothing if already downloaded return false; } @@ -737,7 +781,8 @@ private Flowable downloadImages( AtomicInteger currentImageDownloadIndex, int postsWithImagesCount, AtomicInteger imageDownloadsWithIoError, - int maxImageIoErrors) { + int maxImageIoErrors + ) { if (post.images.isEmpty()) { if (VERBOSE_LOG) { Logger.d(TAG, "Post " + post.no + " contains no images"); @@ -863,7 +908,8 @@ private void addImageToAlreadyDeletedImage(Loadable loadable, String originalNam private void logThreadDownloadingProgress( Loadable loadable, AtomicInteger currentImageDownloadIndex, - int postsWithImagesCount) { + int postsWithImagesCount + ) { // postsWithImagesCount may be 0 so we need to avoid division by zero int count = postsWithImagesCount == 0 ? 1 : postsWithImagesCount; int index = currentImageDownloadIndex.incrementAndGet(); @@ -884,21 +930,19 @@ private void deleteImageCompletely( boolean error = false; AbstractFile originalFile = threadSaveDirImages - .clone() - .appendFileNameSegment(filename + "_" + ORIGINAL_FILE_NAME + "." + extension); + .clone(new FileSegment(filename + "_" + ORIGINAL_FILE_NAME + "." + extension)); - if (originalFile.exists()) { - if (!originalFile.delete()) { + if (fileManager.exists(originalFile)) { + if (!fileManager.delete(originalFile)) { error = true; } } AbstractFile thumbnailFile = threadSaveDirImages - .clone() - .appendFileNameSegment(filename + "_" + THUMBNAIL_FILE_NAME + "." + extension); + .clone(new FileSegment(filename + "_" + THUMBNAIL_FILE_NAME + "." + extension)); - if (thumbnailFile.exists()) { - if (!thumbnailFile.delete()) { + if (fileManager.exists(thumbnailFile)) { + if (!fileManager.delete(thumbnailFile)) { error = true; } } @@ -918,7 +962,8 @@ private void downloadImageIntoFile( String thumbnailExtension, HttpUrl imageUrl, HttpUrl thumbnailUrl, - Loadable loadable) throws IOException, ImageWasAlreadyDeletedException { + Loadable loadable + ) throws IOException, ImageWasAlreadyDeletedException { if (isImageAlreadyDeletedFromTheServer(loadable, filename)) { // We have already tried to download this image and got 404, so it was probably deleted // from the server so there is no point in trying to download it again @@ -977,10 +1022,9 @@ private void downloadImage( } AbstractFile imageFile = threadSaveDirImages - .clone() - .appendFileNameSegment(filename); + .clone(new FileSegment(filename)); - if (!imageFile.exists()) { + if (!fileManager.exists(imageFile)) { Request request = new Request.Builder().url(imageUrl).build(); try (Response response = okHttpClient.newCall(request).execute()) { @@ -1051,7 +1095,7 @@ private boolean isCurrentDownloadRunning(Loadable loadable) { private void storeImageToFile( AbstractFile imageFile, Response response) throws IOException { - if (!imageFile.create()) { + if (fileManager.create(imageFile) == null) { throw new IOException("Could not create a file to save an image to (path: " + imageFile.getFullPath() + ")"); } @@ -1066,7 +1110,7 @@ private void storeImageToFile( } try (InputStream is = body.byteStream()) { - try (OutputStream os = imageFile.getOutputStream()) { + try (OutputStream os = fileManager.getOutputStream(imageFile)) { if (os == null) { throw new IOException("Could not get OutputStream from imageFile, " + "imageFilePath = " + imageFile.getFullPath()); diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/UpdateManager.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/UpdateManager.java index bb13dc9f9e..83aac616b7 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/UpdateManager.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/UpdateManager.java @@ -39,12 +39,12 @@ import com.github.adamantcheese.chan.core.cache.FileCache; import com.github.adamantcheese.chan.core.cache.FileCacheListener; import com.github.adamantcheese.chan.core.net.UpdateApiRequest; -import com.github.adamantcheese.chan.core.saf.file.RawFile; import com.github.adamantcheese.chan.core.settings.ChanSettings; import com.github.adamantcheese.chan.ui.helper.RuntimePermissionsHelper; import com.github.adamantcheese.chan.utils.AndroidUtils; import com.github.adamantcheese.chan.utils.IOUtils; import com.github.adamantcheese.chan.utils.Logger; +import com.github.k1rakishou.fsaf.file.RawFile; import java.io.File; import java.io.IOException; diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/ImageViewerPresenter.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/ImageViewerPresenter.java index 7e4bc9f154..5dcd87b155 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/ImageViewerPresenter.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/ImageViewerPresenter.java @@ -384,7 +384,8 @@ private boolean imageAutoLoad(Loadable loadable, PostImage postImage) { } // Auto load the image when it is cached - return fileCache.exists(postImage.imageUrl.toString()) || shouldLoadForNetworkType(ChanSettings.imageAutoLoadNetwork.get()); + return fileCache.exists(postImage.imageUrl.toString()) + || shouldLoadForNetworkType(ChanSettings.imageAutoLoadNetwork.get()); } private boolean videoAutoLoad(Loadable loadable, PostImage postImage) { diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/ImportExportSettingsPresenter.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/ImportExportSettingsPresenter.java index 0d618197eb..a9358757bc 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/ImportExportSettingsPresenter.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/ImportExportSettingsPresenter.java @@ -20,7 +20,7 @@ import androidx.annotation.Nullable; import com.github.adamantcheese.chan.core.repository.ImportExportRepository; -import com.github.adamantcheese.chan.core.saf.file.ExternalFile; +import com.github.k1rakishou.fsaf.file.ExternalFile; import javax.inject.Inject; diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/ThreadPresenter.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/ThreadPresenter.java index d5729afc3e..41e42725d6 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/ThreadPresenter.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/ThreadPresenter.java @@ -21,7 +21,6 @@ import android.content.Context; import android.text.TextUtils; import android.view.LayoutInflater; -import android.widget.Toast; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; @@ -47,7 +46,6 @@ import com.github.adamantcheese.chan.core.model.orm.SavedReply; import com.github.adamantcheese.chan.core.model.orm.SavedThread; import com.github.adamantcheese.chan.core.pool.ChanLoaderFactory; -import com.github.adamantcheese.chan.core.saf.FileManager; import com.github.adamantcheese.chan.core.settings.ChanSettings; import com.github.adamantcheese.chan.core.site.Site; import com.github.adamantcheese.chan.core.site.SiteActions; @@ -64,11 +62,13 @@ import com.github.adamantcheese.chan.ui.helper.PostHelper; import com.github.adamantcheese.chan.ui.layout.ArchivesLayout; import com.github.adamantcheese.chan.ui.layout.ThreadListLayout; +import com.github.adamantcheese.chan.ui.settings.base_directory.LocalThreadsBaseDirectory; import com.github.adamantcheese.chan.ui.view.FloatingMenuItem; import com.github.adamantcheese.chan.ui.view.ThumbnailView; import com.github.adamantcheese.chan.utils.AndroidUtils; import com.github.adamantcheese.chan.utils.Logger; import com.github.adamantcheese.chan.utils.PostUtils; +import com.github.k1rakishou.fsaf.FileManager; import org.greenrobot.eventbus.EventBus; @@ -133,7 +133,8 @@ public ThreadPresenter(WatchManager watchManager, ChanLoaderFactory chanLoaderFactory, PageRequestManager pageRequestManager, ThreadSaveManager threadSaveManager, - FileManager fileManager) { + FileManager fileManager + ) { this.watchManager = watchManager; this.databaseManager = databaseManager; this.chanLoaderFactory = chanLoaderFactory; @@ -252,7 +253,7 @@ private void startSavingThreadIfItIsNotBeingSaved(Loadable loadable) { return; } - if (!fileManager.baseLocalThreadsDirectoryExists()) { + if (!fileManager.baseDirectoryExists(LocalThreadsBaseDirectory.class)) { // Base directory for local threads does not exist or was deleted return; } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/ImportExportRepository.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/ImportExportRepository.kt index 70ed745b2d..32ca9d2ef9 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/ImportExportRepository.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/ImportExportRepository.kt @@ -23,10 +23,11 @@ import com.github.adamantcheese.chan.core.database.DatabaseManager import com.github.adamantcheese.chan.core.model.export.* import com.github.adamantcheese.chan.core.model.json.site.SiteConfig import com.github.adamantcheese.chan.core.model.orm.* -import com.github.adamantcheese.chan.core.saf.file.ExternalFile -import com.github.adamantcheese.chan.core.saf.file.FileDescriptorMode import com.github.adamantcheese.chan.core.settings.ChanSettings import com.github.adamantcheese.chan.utils.Logger +import com.github.k1rakishou.fsaf.FileManager +import com.github.k1rakishou.fsaf.file.ExternalFile +import com.github.k1rakishou.fsaf.file.FileDescriptorMode import com.google.gson.Gson import java.io.FileReader import java.io.FileWriter @@ -39,7 +40,8 @@ class ImportExportRepository @Inject constructor( private val databaseManager: DatabaseManager, private val databaseHelper: DatabaseHelper, - private val gson: Gson + private val gson: Gson, + private val fileManager: FileManager ) { fun exportTo(settingsFile: ExternalFile, isNewFile: Boolean, callbacks: ImportExportCallbacks) { @@ -53,7 +55,7 @@ constructor( val json = gson.toJson(appSettings) - if (!settingsFile.exists() || !settingsFile.canWrite()) { + if (!fileManager.exists(settingsFile) || !fileManager.canWrite(settingsFile)) { throw IOException( "Something wrong with export file (Can't write or it doesn't exist) " + settingsFile.getFullPath() @@ -68,7 +70,7 @@ constructor( fdm = FileDescriptorMode.Write } - val result = settingsFile.withFileDescriptor(fdm) { fileDescriptor -> + fileManager.withFileDescriptor(settingsFile, fdm) { fileDescriptor -> FileWriter(fileDescriptor).use { writer -> writer.write(json) writer.flush() @@ -78,10 +80,6 @@ constructor( callbacks.onSuccess(ImportExport.Export) } - if (result.isFailure) { - throw result.exceptionOrNull()!! - } - } catch (error: Throwable) { Logger.e(TAG, "Error while trying to export settings", error) @@ -93,21 +91,24 @@ constructor( fun importFrom(settingsFile: ExternalFile, callbacks: ImportExportCallbacks) { databaseManager.runTask { try { - if (!settingsFile.exists()) { + if (!fileManager.exists(settingsFile)) { Logger.i(TAG, "There is nothing to import, importFile does not exist " + settingsFile.getFullPath()) callbacks.onNothingToImportExport(ImportExport.Import) return@runTask } - if (!settingsFile.canRead()) { + if (!fileManager.canRead(settingsFile)) { throw IOException( "Something wrong with import file (Can't read or it doesn't exist) " + settingsFile.getFullPath() ) } - val result = settingsFile.withFileDescriptor(FileDescriptorMode.Read) { fileDescriptor -> + fileManager.withFileDescriptor( + settingsFile, + FileDescriptorMode.Read + ) { fileDescriptor -> FileReader(fileDescriptor).use { reader -> val appSettings = gson.fromJson(reader, ExportedAppSettings::class.java) @@ -124,10 +125,6 @@ constructor( } } - if (result.isFailure) { - throw result.exceptionOrNull()!! - } - } catch (error: Throwable) { Logger.e(TAG, "Error while trying to import settings", error) callbacks.onError(error, ImportExport.Import) diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/SavedThreadLoaderRepository.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/SavedThreadLoaderRepository.kt index e4d6f15ad7..df8e529831 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/SavedThreadLoaderRepository.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/SavedThreadLoaderRepository.kt @@ -3,10 +3,12 @@ package com.github.adamantcheese.chan.core.repository import com.github.adamantcheese.chan.core.mapper.ThreadMapper import com.github.adamantcheese.chan.core.model.Post import com.github.adamantcheese.chan.core.model.save.SerializableThread -import com.github.adamantcheese.chan.core.saf.file.AbstractFile -import com.github.adamantcheese.chan.core.saf.file.ExternalFile import com.github.adamantcheese.chan.utils.BackgroundUtils import com.github.adamantcheese.chan.utils.Logger +import com.github.k1rakishou.fsaf.FileManager +import com.github.k1rakishou.fsaf.file.AbstractFile +import com.github.k1rakishou.fsaf.file.ExternalFile +import com.github.k1rakishou.fsaf.file.FileSegment import com.google.gson.Gson import java.io.DataInputStream import java.io.DataOutputStream @@ -22,25 +24,28 @@ class SavedThreadLoaderRepository * uninstall/app data clearing. */ @Inject -constructor(private val gson: Gson) { +constructor( + private val gson: Gson, + private val fileManager: FileManager +) { @Throws(IOException::class) fun loadOldThreadFromJsonFile( - threadSaveDir: AbstractFile): SerializableThread? { + threadSaveDir: AbstractFile + ): SerializableThread? { if (BackgroundUtils.isMainThread()) { throw RuntimeException("Cannot be executed on the main thread!") } val threadFile = threadSaveDir - .clone() - .appendFileNameSegment(THREAD_FILE_NAME) + .clone(FileSegment(THREAD_FILE_NAME)) - if (!threadFile.exists()) { + if (!fileManager.exists(threadFile)) { Logger.d(TAG, "threadFile does not exist, threadFilePath = " + threadFile.getFullPath()) return null } - return threadFile.getInputStream()?.use { inputStream -> + return fileManager.getInputStream(threadFile)?.use { inputStream -> return@use DataInputStream(inputStream).use { dis -> val json = String(dis.readBytes(), StandardCharsets.UTF_8) @@ -53,24 +58,27 @@ constructor(private val gson: Gson) { @Throws(IOException::class, CouldNotCreateThreadFile::class, - CouldNotGetParcelFileDescriptor::class) + CouldNotGetParcelFileDescriptor::class + ) fun savePostsToJsonFile( oldSerializableThread: SerializableThread?, posts: List, - threadSaveDir: AbstractFile) { + threadSaveDir: AbstractFile + ) { if (BackgroundUtils.isMainThread()) { throw RuntimeException("Cannot be executed on the main thread!") } val threadFile = threadSaveDir - .clone() - .appendFileNameSegment(THREAD_FILE_NAME) + .clone(FileSegment(THREAD_FILE_NAME)) - if (!threadFile.exists() && !threadFile.create()) { + val createdThreadFile = fileManager.create(threadFile) + + if (!fileManager.exists(threadFile) || createdThreadFile == null) { throw CouldNotCreateThreadFile(threadFile) } - threadFile.getOutputStream()?.use { outputStream -> + fileManager.getOutputStream(createdThreadFile)?.use { outputStream -> // Update the thread file return@use DataOutputStream(outputStream).use { dos -> val serializableThread = if (oldSerializableThread != null) { @@ -87,10 +95,10 @@ constructor(private val gson: Gson) { dos.write(bytes) dos.flush() - return@use Unit + return@use } } ?: throw IOException("threadFile.getOutputStream() returned null, threadFile = " - + threadFile.getFullPath()) + + createdThreadFile.getFullPath()) } inner class CouldNotGetParcelFileDescriptor(threadFile: ExternalFile) diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileChooser.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileChooser.kt deleted file mode 100644 index 4b9d7e191e..0000000000 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileChooser.kt +++ /dev/null @@ -1,309 +0,0 @@ -package com.github.adamantcheese.chan.core.saf - -import android.app.Activity -import android.content.Context -import android.content.Intent -import android.provider.DocumentsContract -import android.webkit.MimeTypeMap -import com.github.adamantcheese.chan.core.getMimeFromFilename -import com.github.adamantcheese.chan.core.saf.callback.* -import com.github.adamantcheese.chan.utils.Logger -import java.lang.Exception -import java.lang.IllegalArgumentException - -internal class FileChooser( - private val appContext: Context -) { - private val callbacksMap = hashMapOf() - private val mimeTypeMap = MimeTypeMap.getSingleton() - - private var requestCode = 10000 - private var startActivityCallbacks: StartActivityCallbacks? = null - - internal fun setCallbacks(startActivityCallbacks: StartActivityCallbacks) { - this.startActivityCallbacks = startActivityCallbacks - } - - internal fun removeCallbacks() { - this.startActivityCallbacks = null - } - - internal fun openChooseDirectoryDialog(directoryChooserCallback: DirectoryChooserCallback) { - startActivityCallbacks?.let { callbacks -> - val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) - intent.putExtra("android.content.extra.SHOW_ADVANCED", true) - - intent.addFlags( - Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or - Intent.FLAG_GRANT_READ_URI_PERMISSION or - Intent.FLAG_GRANT_WRITE_URI_PERMISSION - ) - - val nextRequestCode = ++requestCode - callbacksMap[nextRequestCode] = directoryChooserCallback as ChooserCallback - - try { - callbacks.myStartActivityForResult(intent, nextRequestCode) - } catch (e: Exception) { - callbacksMap.remove(nextRequestCode) - directoryChooserCallback.onCancel(e.message - ?: "openChooseDirectoryDialog() Unknown error") - } - } - } - - internal fun openChooseFileDialog(fileChooserCallback: FileChooserCallback) { - startActivityCallbacks?.let { callbacks -> - val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) - intent.addFlags( - Intent.FLAG_GRANT_READ_URI_PERMISSION or - Intent.FLAG_GRANT_WRITE_URI_PERMISSION - ) - - intent.addCategory(Intent.CATEGORY_OPENABLE) - intent.type = "*/*" - - val nextRequestCode = ++requestCode - callbacksMap[nextRequestCode] = fileChooserCallback as ChooserCallback - - try { - callbacks.myStartActivityForResult(intent, nextRequestCode) - } catch (e: Exception) { - callbacksMap.remove(nextRequestCode) - fileChooserCallback.onCancel(e.message ?: "openChooseFileDialog() Unknown error") - } - } - } - - internal fun openCreateFileDialog( - fileName: String, - fileCreateCallback: FileCreateCallback - ) { - startActivityCallbacks?.let { callbacks -> - val intent = Intent(Intent.ACTION_CREATE_DOCUMENT) - intent.addFlags( - Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or - Intent.FLAG_GRANT_READ_URI_PERMISSION or - Intent.FLAG_GRANT_WRITE_URI_PERMISSION - ) - - intent.addCategory(Intent.CATEGORY_OPENABLE) - - if (fileName != null) { - intent.type = mimeTypeMap.getMimeFromFilename(fileName) - intent.putExtra(Intent.EXTRA_TITLE, fileName) - } - - val nextRequestCode = ++requestCode - callbacksMap[nextRequestCode] = fileCreateCallback as ChooserCallback - - try { - callbacks.myStartActivityForResult(intent, nextRequestCode) - } catch (e: Exception) { - callbacksMap.remove(nextRequestCode) - fileCreateCallback.onCancel(e.message ?: "openCreateFileDialog() Unknown error") - } - } - } - - internal fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { - val callback = callbacksMap[requestCode] - if (callback == null) { - Logger.d(TAG, "Callback is already removed from the map, resultCode = $requestCode") - return false - } - - try { - if (startActivityCallbacks == null) { - // Skip all requests when the callback is not set - Logger.d(TAG, "Callback is not attached") - return false - } - - when (callback) { - is DirectoryChooserCallback -> { - handleDirectoryChooserCallback(callback, resultCode, data) - } - is FileChooserCallback -> { - handleFileChooserCallback(callback, resultCode, data) - } - is FileCreateCallback -> { - handleFileCreateCallback(callback, resultCode, data) - } - else -> throw IllegalArgumentException("Not implemented for ${callback.javaClass.name}") - } - - return true - } finally { - callbacksMap.remove(requestCode) - } - } - - private fun handleFileCreateCallback( - callback: FileCreateCallback, - resultCode: Int, - intent: Intent?) { - if (resultCode != Activity.RESULT_OK) { - val msg = "handleFileCreateCallback() Non OK result ($resultCode)" - - Logger.e(TAG, msg) - callback.onCancel(msg) - return - } - - if (intent == null) { - val msg = "handleFileCreateCallback() Intent is null" - - Logger.e(TAG, msg) - callback.onCancel(msg) - return - } - - val read = (intent.flags and Intent.FLAG_GRANT_READ_URI_PERMISSION) != 0 - val write = (intent.flags and Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0 - - if (!read) { - val msg = "handleFileCreateCallback() No grant read uri permission given" - - Logger.e(TAG, msg) - callback.onCancel(msg) - return - } - - if (!write) { - val msg = "handleFileCreateCallback() No grant write uri permission given" - - Logger.e(TAG, msg) - callback.onCancel(msg) - return - } - - val uri = intent.data - if (uri == null) { - val msg = "handleFileCreateCallback() intent.getData() == null" - - Logger.e(TAG, msg) - callback.onCancel(msg) - return - } - - callback.onResult(uri) - } - - private fun handleFileChooserCallback( - callback: FileChooserCallback, - resultCode: Int, - intent: Intent? - ) { - if (resultCode != Activity.RESULT_OK) { - val msg = "handleFileChooserCallback() Non OK result ($resultCode)" - - Logger.e(TAG, msg) - callback.onCancel(msg) - return - } - - if (intent == null) { - val msg = "handleFileChooserCallback() Intent is null" - - Logger.e(TAG, msg) - callback.onCancel(msg) - return - } - - val read = (intent.flags and Intent.FLAG_GRANT_READ_URI_PERMISSION) != 0 - val write = (intent.flags and Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0 - - if (!read) { - val msg = "handleFileChooserCallback() No grant read uri permission given" - - Logger.e(TAG, msg) - callback.onCancel(msg) - return - } - - if (!write) { - val msg = "handleFileChooserCallback() No grant write uri permission given" - - Logger.e(TAG, msg) - callback.onCancel(msg) - return - } - - val uri = intent.data - if (uri == null) { - val msg = "handleFileChooserCallback() intent.getData() == null" - - Logger.e(TAG, msg) - callback.onCancel(msg) - return - } - - callback.onResult(uri) - } - - private fun handleDirectoryChooserCallback( - callback: DirectoryChooserCallback, - resultCode: Int, - intent: Intent? - ) { - if (resultCode != Activity.RESULT_OK) { - val msg = "handleDirectoryChooserCallback() Non OK result ($resultCode)" - - Logger.e(TAG, msg) - callback.onCancel(msg) - return - } - - if (intent == null) { - val msg = "handleDirectoryChooserCallback() Intent is null" - - Logger.e(TAG, msg) - callback.onCancel(msg) - return - } - - val read = (intent.flags and Intent.FLAG_GRANT_READ_URI_PERMISSION) != 0 - val write = (intent.flags and Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0 - - if (!read) { - val msg = "handleDirectoryChooserCallback() No grant read uri permission given" - - Logger.e(TAG, msg) - callback.onCancel(msg) - return - } - - if (!write) { - val msg = "handleDirectoryChooserCallback() No grant write uri permission given" - - Logger.e(TAG, msg) - callback.onCancel(msg) - return - } - - val uri = intent.data - if (uri == null) { - val msg = "handleDirectoryChooserCallback() intent.getData() == null" - - Logger.e(TAG, msg) - callback.onCancel(msg) - return - } - - val documentId = DocumentsContract.getTreeDocumentId(uri) - val treeDocumentUri = DocumentsContract.buildDocumentUriUsingTree(uri, documentId) - - val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or - Intent.FLAG_GRANT_WRITE_URI_PERMISSION - - val contentResolver = appContext.contentResolver - contentResolver.takePersistableUriPermission(uri, flags) - - callback.onResult(treeDocumentUri) - } - - companion object { - private const val TAG = "FileChooser" - } -} \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt deleted file mode 100644 index 800669460b..0000000000 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/FileManager.kt +++ /dev/null @@ -1,362 +0,0 @@ -package com.github.adamantcheese.chan.core.saf - -import android.content.Context -import android.content.Intent -import android.net.Uri -import android.provider.DocumentsContract -import android.util.Log -import androidx.documentfile.provider.DocumentFile -import com.github.adamantcheese.chan.core.saf.callback.DirectoryChooserCallback -import com.github.adamantcheese.chan.core.saf.callback.FileChooserCallback -import com.github.adamantcheese.chan.core.saf.callback.FileCreateCallback -import com.github.adamantcheese.chan.core.saf.callback.StartActivityCallbacks -import com.github.adamantcheese.chan.core.saf.file.AbstractFile -import com.github.adamantcheese.chan.core.saf.file.ExternalFile -import com.github.adamantcheese.chan.core.saf.file.RawFile -import com.github.adamantcheese.chan.core.settings.ChanSettings -import com.github.adamantcheese.chan.utils.IOUtils -import com.github.adamantcheese.chan.utils.Logger -import java.io.File -import java.io.IOException -import java.lang.IllegalStateException -import java.lang.RuntimeException -import java.util.* - -class FileManager( - private val appContext: Context -) { - private val fileChooser = FileChooser(appContext) - - /** - * Used for calling Android File picker - * */ - fun setCallbacks(startActivityCallbacks: StartActivityCallbacks) { - fileChooser.setCallbacks(startActivityCallbacks) - } - - fun removeCallbacks() { - fileChooser.removeCallbacks() - } - - //======================================================= - // Api to open file/directory chooser and handling the result - //======================================================= - - fun openChooseDirectoryDialog(callback: DirectoryChooserCallback) { - fileChooser.openChooseDirectoryDialog(callback) - } - - fun openChooseFileDialog(callback: FileChooserCallback) { - fileChooser.openChooseFileDialog(callback) - } - - fun openCreateFileDialog(filename: String, callback: FileCreateCallback) { - fileChooser.openCreateFileDialog(filename, callback) - } - - fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { - return fileChooser.onActivityResult(requestCode, resultCode, data) - } - - //======================================================= - // Api to convert native file/documentFile classes into our own abstractions - //======================================================= - - fun baseSaveLocalDirectoryExists(): Boolean { - val baseDirFile = newSaveLocationFile() - if (baseDirFile == null) { - return false - } - - if (!baseDirFile.exists()) { - return false - } - - return true - } - - fun baseLocalThreadsDirectoryExists(): Boolean { - val baseDirFile = newLocalThreadFile() - if (baseDirFile == null) { - return false - } - - if (!baseDirFile.exists()) { - return false - } - - return true - } - - /** - * Create a raw file from a path. - * Use this method to convert a java File by this path into an AbstractFile. - * Does not create file on the disk automatically! - * */ - fun fromPath(path: String): RawFile { - return fromRawFile(File(path)) - } - - /** - * Create RawFile from Java File. - * Use this method to convert a java File into an AbstractFile. - * Does not create file on the disk automatically! - * */ - fun fromRawFile(file: File): RawFile { - if (file.isFile) { - return RawFile(AbstractFile.Root.FileRoot(file, file.name)) - } - - return RawFile(AbstractFile.Root.DirRoot(file)) - } - - /** - * Create an external file from Uri. - * Use this method to convert external file uri (file that may be located at sd card) into an - * AbstractFile. If a file does not exist null is returned. - * Does not create file on the disk automatically! - * */ - fun fromUri(uri: Uri): ExternalFile? { - val documentFile = toDocumentFile(uri) - if (documentFile == null) { - return null - } - - return if (documentFile.isFile) { - val filename = documentFile.name - if (filename == null) { - throw IllegalStateException("fromUri() queryTreeName() returned null") - } - - ExternalFile(appContext, AbstractFile.Root.FileRoot(documentFile, filename)) - } else { - ExternalFile(appContext, AbstractFile.Root.DirRoot(documentFile)) - } - } - - /** - * Instantiates a new AbstractFile with the root being in the local threads directory. - * Does not create file on the disk automatically! - * */ - fun newLocalThreadFile(): AbstractFile? { - if (ChanSettings.localThreadLocation.get().isEmpty() - && ChanSettings.localThreadsLocationUri.get().isEmpty()) { - // wtf? - throw RuntimeException("Both local thread save locations are empty! " + - "Something went terribly wrong.") - } - - val uri = ChanSettings.localThreadsLocationUri.get() - if (uri.isNotEmpty()) { - // When we change localThreadsLocation we also set localThreadsLocationUri to an - // empty string, so we need to check whether the localThreadsLocationUri is empty or not, - // because saveLocation is never empty - val rootDirectory = DocumentFile.fromTreeUri(appContext, Uri.parse(uri)) - if (rootDirectory == null) { - return null - } - - return ExternalFile( - appContext, - AbstractFile.Root.DirRoot(rootDirectory)) - } - - val path = ChanSettings.localThreadLocation.get() - return RawFile(AbstractFile.Root.DirRoot(File(path))) - } - - /** - * Instantiates a new AbstractFile with the root being in the app's base directory (either the Kuroba - * directory in case of using raw file api or the user's selected directory in case of using SAF). - * Does not create file on the disk automatically! - * */ - fun newSaveLocationFile(): AbstractFile? { - if (ChanSettings.saveLocation.get().isEmpty() && ChanSettings.saveLocationUri.get().isEmpty()) { - // wtf? - throw RuntimeException("Both save locations are empty! Something went terribly wrong.") - } - - val uri = ChanSettings.saveLocationUri.get() - if (uri.isNotEmpty()) { - // When we change saveLocation we also set saveLocationUri to an empty string, so we need - // to check whether the saveLocationUri is empty or not, because saveLocation is never - // empty - val rootDirectory = DocumentFile.fromTreeUri(appContext, Uri.parse(uri)) - if (rootDirectory == null) { - return null - } - - return ExternalFile( - appContext, - AbstractFile.Root.DirRoot(rootDirectory)) - } - - val path = ChanSettings.saveLocation.get() - return RawFile(AbstractFile.Root.DirRoot(File(path))) - } - - /** - * Copy one file's contents into another - * */ - fun copyFileContents(source: AbstractFile, destination: AbstractFile): Boolean { - return try { - source.getInputStream()?.use { inputStream -> - destination.getOutputStream()?.use { outputStream -> - IOUtils.copy(inputStream, outputStream) - true - } - } ?: false - } catch (e: IOException) { - Logger.e(TAG, "IOException while copying one file into another", e) - false - } - } - - // TODO: maybe it is a better idea to not copy old local threads when changing base directory - // at all? - /** - * VERY SLOW!!! DO NOT EVEN THINK RUNNING THIS ON THE MAIN THREAD!!! - * */ - fun copyDirectoryWithContent(sourceDir: AbstractFile, destDir: AbstractFile): Boolean { - if (!sourceDir.exists()) { - Logger.e(TAG, "Source directory does not exists, path = ${sourceDir.getFullPath()}") - return false - } - - if (sourceDir.listFiles().isEmpty()) { - Logger.d(TAG, "Source directory is empty, nothing to copy") - return true - } - - if (!destDir.exists()) { - Logger.e(TAG, "Destination directory does not exists, path = ${sourceDir.getFullPath()}") - return false - } - - if (!sourceDir.isDirectory()) { - Logger.e(TAG, "Source directory is not a directory, path = ${sourceDir.getFullPath()}") - return false - } - - if (!destDir.isDirectory()) { - Logger.e(TAG, "Destination directory is not a directory, path = ${destDir.getFullPath()}") - return false - } - - val queue = LinkedList() - val files = mutableListOf() - queue.offer(sourceDir) - - // Collect all of the inner files in the source directory - while (queue.isNotEmpty()) { - val file = queue.poll() - if (file.isDirectory()) { - file.listFiles().forEach { queue.offer(it) } - } else { - files.add(file) - } - } - - val prefix = sourceDir.getFullPath() - - for (file in files) { - // Holy shit this hack is so fucking disgusting and may break literally any minute. - // If this shit breaks then blame google for providing such a retarded fucking API. - - // Basically we have a directory, let's say /123 and we want to copy all - // of it's files into /456. So we collect every file in /123 then we iterate every - // collected file, remove base directory prefix (/123 in this case) and recreate this - // file with the same directory structure in another base directory (/456 in this case). - // Let's was we have the following files: - // - // /123/1.txt - // /123/111/2.txt - // /123/222/3.txt - // - // After calling this method we will have these files copied into /456: - // - // /456/1.txt - // /456/111/2.txt - // /456/222/3.txt - // - val fileInNewDirectory = newLocalThreadFile() - ?.appendFileNameSegment(file.getFullPath().removePrefix(prefix)) - ?.createNew() - - if (fileInNewDirectory == null) { - Logger.e(TAG, "Couldn't create inner file with name ${file.getName()}") - return false - } - - if (!copyFileContents(file, fileInNewDirectory)) { - Logger.e(TAG, "Couldn't copy one file into another") - return false - } - } - - return true - } - - fun forgetSAFTree(directory: AbstractFile): Boolean { - if (directory !is ExternalFile) { - // Only ExternalFile is being used with SAF - return true - } - - val uri = Uri.parse(directory.getFullPath()) - - if (!directory.exists()) { - Logger.e(TAG, "Couldn't revoke permissions from directory because it does not exist, path = $uri") - return false - } - - if (!directory.isDirectory()) { - Logger.e(TAG, "Couldn't revoke permissions from directory it is not a directory, path = $uri") - return false - } - - return try { - val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or - Intent.FLAG_GRANT_WRITE_URI_PERMISSION - - appContext.contentResolver.releasePersistableUriPermission(uri, flags) - appContext.revokeUriPermission(uri, flags) - - Logger.d(TAG, "Revoke old path permissions success on $uri") - true - } catch (err: Exception) { - Logger.e(TAG, "Error revoking old path permissions on $uri", err) - false - } - } - - private fun toDocumentFile(uri: Uri): DocumentFile? { - if (!DocumentFile.isDocumentUri(appContext, uri)) { - Logger.e(TAG, "Not a DocumentFile, uri = $uri") - return null - } - - val treeUri = try { - // Will throw an exception if uri is not a treeUri. Hacky as fuck but I don't know - // another way to check it. - DocumentFile.fromTreeUri(appContext, uri) - } catch (ignored: IllegalArgumentException) { - null - } - - if (treeUri != null) { - return treeUri - } - - return try { - DocumentFile.fromSingleUri(appContext, uri) - } catch (e: IllegalArgumentException) { - Logger.e(TAG, "Provided uri is neither a treeUri nor singleUri, uri = $uri") - null - } - } - - companion object { - private const val TAG = "FileManager" - } -} \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/annotation/ImmutableMethod.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/annotation/ImmutableMethod.kt deleted file mode 100644 index e8755cc8a8..0000000000 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/annotation/ImmutableMethod.kt +++ /dev/null @@ -1,4 +0,0 @@ -package com.github.adamantcheese.chan.core.saf.annotation - -@Retention(AnnotationRetention.SOURCE) -annotation class ImmutableMethod \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/annotation/MutableMethod.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/annotation/MutableMethod.kt deleted file mode 100644 index 638d51c87b..0000000000 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/annotation/MutableMethod.kt +++ /dev/null @@ -1,4 +0,0 @@ -package com.github.adamantcheese.chan.core.saf.annotation - -@Retention(AnnotationRetention.SOURCE) -annotation class MutableMethod \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/callback/ChooserCallback.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/callback/ChooserCallback.kt deleted file mode 100644 index 472b72b98c..0000000000 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/callback/ChooserCallback.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.github.adamantcheese.chan.core.saf.callback - -import android.net.Uri - -interface ChooserCallback { - fun onResult(uri: Uri) - fun onCancel(reason: String) -} \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/callback/DirectoryChooserCallback.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/callback/DirectoryChooserCallback.kt deleted file mode 100644 index 32338a5360..0000000000 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/callback/DirectoryChooserCallback.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.github.adamantcheese.chan.core.saf.callback - -abstract class DirectoryChooserCallback : ChooserCallback \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/callback/FileChooserCallback.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/callback/FileChooserCallback.kt deleted file mode 100644 index c889c53a52..0000000000 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/callback/FileChooserCallback.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.github.adamantcheese.chan.core.saf.callback - -abstract class FileChooserCallback : ChooserCallback \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/callback/FileCreateCallback.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/callback/FileCreateCallback.kt deleted file mode 100644 index a2d27021c4..0000000000 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/callback/FileCreateCallback.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.github.adamantcheese.chan.core.saf.callback - -abstract class FileCreateCallback : ChooserCallback \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/callback/StartActivityCallbacks.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/callback/StartActivityCallbacks.kt deleted file mode 100644 index 9f6db98d1f..0000000000 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/callback/StartActivityCallbacks.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.github.adamantcheese.chan.core.saf.callback - -import android.content.Intent - -interface StartActivityCallbacks { - fun myStartActivityForResult(intent: Intent, requestCode: Int) -} \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt deleted file mode 100644 index 6e2f6a9f5c..0000000000 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/AbstractFile.kt +++ /dev/null @@ -1,302 +0,0 @@ -package com.github.adamantcheese.chan.core.saf.file - -import com.github.adamantcheese.chan.core.extension -import com.github.adamantcheese.chan.core.saf.annotation.ImmutableMethod -import com.github.adamantcheese.chan.core.saf.annotation.MutableMethod -import java.io.* - -/** - * An abstraction class over both the Java File and the new Storage Access Framework DocumentFile. - * - * Some methods are marked with [MutableMethod] annotation. This means that such methods are gonna - * mutate the inner data of the [AbstractFile] (such as root or segments). Sometimes this behavior is - * not desirable. For example, when you have an AbstractFile representing some directory that may - * not even exists on the disk and you want to check whether it exists and if it does check some - * additional files inside that directory. In such case you may want to preserve the [AbstractFile] - * that represents that directory in it's original state. To do this you have to call the [clone] - * method on the file that represents the directory. It will create a copy of the file that you can - * safely work without worry that the original file may change. - * - * Other methods are marked with [ImmutableMethod] annotation. This means that those methods create a - * copy of the [AbstractFile] internally and are safe to use without calling [clone] - * - * Examples. - * - * Usually you want to create an [AbstractFile] pointing to some directory (like the Kuroba base dir) - * and then create either subdirectories or files inside that directory. You can start with one of the - * following methods: - * - * // Creates an [AbstractFile] from base SAF directory. Be aware that the Uri must not be created - * // manually! This won't work with SAF since one file may have it's Uri changed when Android decides to - * // do so. Usually you want to call file or directory chooser via SAF API (there are methods for - * // that in the [FileManager] class) which will return an Uri that you can then pass into fromUri() - * // method. But usually you don't even need to do this since we usually do this once to get the - * // Kuroba base directory and then just do our work inside of that directory. - * AbstractFile baseDir = fileManager.fromUri(Uri.parse(ChanSettings.saveLocationUri.get())); - * - * // Creates an [AbstractFile] from base raw directory - * AbstractFile baseDir = fileManager.fromRawFile(new File(ChanSettings.saveLocation.get())); - * - * // Same as above - * AbstractFile baseDir = fileManager.fromPath(ChanSettings.saveLocation.get()); - * - * - * Then you can start appending subdirectories or a filename: - * - * // This will create a "test.txt" file located at /dir1/dir2/dir3, i.e. - * // /dir1/dir2/dir3/test.txt - * AbstractFile newFile = baseDir - * .appendSubDirSegment("dir1") - * .appendSubDirSegment("dir2") - * .appendSubDirSegment("dir3") - * .appendFileNameSegment("test.txt") - * .createNew(); - * - * Then you can call methods that are similar to the standard Java File API, e.g. [exists], - * [getName], [getLength], [isFile], [isDirectory], [canRead], [canWrite] etc. - * - * If you want to work with multiple files in a directory (or sub directories) you may want - * to [clone] the file that represents that directory, e.g: - * - * AbstractFile clonedFile = baseDir.clone(); - * - * AbstractFile f1 = clonedFile - * .appendFileNameSegment("f1.txt") - * .createNew(); - * AbstractFile f2 = clonedFile - * .appendFileNameSegment("f2.txt") - * .createNew(); - * AbstractFile f3 = clonedFile - * .appendFileNameSegment("f3.txt") - * .createNew(); - * - * You have to do this because some methods may mutate the internal state of the [AbstractFile], so - * after calling, let's say: - * - * AbstractFile f1 = baseDir - * .appendFileNameSegment("f1.txt") - * .createNew(); - * - * Without cloning it first baseDir will be start pointing to /f1.txt instead of - * just . The same thing applies to any method marked with [MutableMethod] annotation. - * methods marked with [ImmutableMethod] do this stuff internally so they are safe to use without - * cloning. - * - * Sometimes you don't know which external directory to choose to store a new file (the SAF or the - * old raw Java File external directory). In this case you can use: - * - * AbstractFile baseDir = fileManager.newSaveLocationFile(); - * - * Method which will create an [AbstractFile] with root pointing to either Kuroba SAF base directory - * (if user has set it) or if he didn't then to the default external directory (Backed by raw - * Java File) or - * - * AbstractFile baseDir = fileManager.newLocalThreadFile(); - * - * Method which will create an [AbstractFile] with root pointing to either user's selected local - * threads directory or to the default external local threads directory - * - * */ -abstract class AbstractFile( - /** - * /test/123/test2/filename.txt -> 4 segments - * */ - protected val segments: MutableList -) { - /** - * Appends a new subdirectory (or couple of subdirectories, e.g. "dir1/dir2/dir3") - * to the root directory - * */ - @MutableMethod - abstract fun appendSubDirSegment(name: String): AbstractFile - - /** - * Appends a file name to the root directory (or couple subdirectories with filename at the end, - * e.g. "dir1/dir2/dir3/test.txt" - * */ - @MutableMethod - abstract fun appendFileNameSegment(name: String): AbstractFile - - /** - * Creates a new file that consists of the root directory and segments (sub dirs or the file name) - * Behave similarly to Java's mkdirs() method but work not only with directories but files as well. - * */ - @ImmutableMethod - abstract fun createNew(): AbstractFile? - - @ImmutableMethod - fun create(): Boolean { - return createNew() != null - } - - /** - * When doing something with an [AbstractFile] (like appending a subdir or a filename) the - * [AbstractFile] will change because it's mutable. So if you don't want to change the original - * [AbstractFile] you need to make a copy via this method (like, if you want to search for - * a couple of files in the same directory you would want to clone the directory - * [AbstractFile] and then append the filename to those copies) - * */ - abstract fun clone(): AbstractFile - - @ImmutableMethod - abstract fun exists(): Boolean - - @ImmutableMethod - abstract fun isFile(): Boolean - - @ImmutableMethod - abstract fun isDirectory(): Boolean - - @ImmutableMethod - abstract fun canRead(): Boolean - - @ImmutableMethod - abstract fun canWrite(): Boolean - - @ImmutableMethod - abstract fun getFullPath(): String - - @ImmutableMethod - abstract fun delete(): Boolean - - @ImmutableMethod - abstract fun getInputStream(): InputStream? - - @ImmutableMethod - abstract fun getOutputStream(): OutputStream? - - @ImmutableMethod - abstract fun getName(): String - - @ImmutableMethod - abstract fun findFile(fileName: String): AbstractFile? - - @ImmutableMethod - abstract fun getLength(): Long - - @ImmutableMethod - abstract fun withFileDescriptor( - fileDescriptorMode: FileDescriptorMode, - func: (FileDescriptor) -> T): Result - - @ImmutableMethod - abstract fun listFiles(): List - - @ImmutableMethod - abstract fun lastModified(): Long - - protected fun appendSubDirSegmentInner(name: String): AbstractFile { - check(!isFilenameAppended()) { "Cannot append anything after file name has been appended" } - require(!name.isBlank()) { "Bad name: $name" } - - val nameList = if (name.contains(File.separatorChar) || name.contains(ENCODED_SEPARATOR)) { - name - // First of all split by the "/" symbol - .split(File.separatorChar) - // Then try to split every part again by this time by the "%2F" symbol - .flatMap { names -> names.split(ENCODED_SEPARATOR) } - } else { - listOf(name) - } - - nameList - .map { splitName -> Segment(splitName) } - .forEach { segment -> segments += segment } - - return this - } - - protected fun appendFileNameSegmentInner(name: String): AbstractFile { - check(!isFilenameAppended()) { "Cannot append anything after file name has been appended" } - require(!name.isBlank()) { "Bad name: $name" } - - val nameList = if (name.contains(File.separatorChar) || name.contains(ENCODED_SEPARATOR)) { - val split = name - // First of all split by the "/" symbol - .split(File.separatorChar) - // Then try to split every part again by this time by the "%2F" symbol - .flatMap { names -> names.split(ENCODED_SEPARATOR) } - check(split.size >= 2) { "Should have at least two entries, name = $name" } - - split - } else { - listOf(name) - } - - require(nameList.last().extension() != null) { "Last segment must be a filename" } - - for ((index, splitName) in nameList.withIndex()) { - require(!(splitName.extension() != null && index != nameList.lastIndex)) { - "Only the last split segment may have a file name, " + - "bad segment index = ${index}/${nameList.lastIndex}, bad name = $splitName" - } - - val isFileName = index == nameList.lastIndex - segments += Segment(splitName, isFileName) - } - - return this - } - - private fun isFilenameAppended(): Boolean = segments.lastOrNull()?.isFileName ?: false - - override fun toString(): String { - return getFullPath() - } - - /** - * We can have the root to be a directory or a file. - * If it's a directory, that means that we can append sub directories to it. - * If it's a file we can't do that so usually when attempting to append something to the FileRoot - * an exception will be thrown - * - * @param holder either DocumentFile or File. - * */ - sealed class Root(val holder: T) { - - fun name(): String? { - if (this is FileRoot) { - return this.fileName - } - - return null - } - - fun clone(): Root { - return when (this) { - is DirRoot<*> -> DirRoot(holder) - is FileRoot<*> -> FileRoot(holder, fileName) - } - } - - /** - * /test/123/test2 - * or - * /test/123/test2/5/6/7/8/112233 - * */ - class DirRoot(holder: T) : Root(holder) - - /** - * /test/123/test2/filename.txt - * where holder = /test/123/test2/filename.txt (Uri), - * fileName = filename.txt (may have no extension) - * */ - class FileRoot(holder: T, val fileName: String) : Root(holder) - } - - /** - * Segment represents a sub directory or a file name, e.g: - * /test/123/test2/filename.txt - * ^ ^ ^ ^ - * | | | +--- File name segment (name = filename.txt, isFileName == true) - * +---+----+-- Directory segments (names = [test, 123, test2], isFileName == false) - * */ - class Segment( - val name: String, - val isFileName: Boolean = false - ) - - companion object { - const val ENCODED_SEPARATOR = "%2F" - } -} \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt deleted file mode 100644 index 5b4da7d8b9..0000000000 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/ExternalFile.kt +++ /dev/null @@ -1,355 +0,0 @@ -package com.github.adamantcheese.chan.core.saf.file - -import android.content.Context -import android.net.Uri -import android.os.ParcelFileDescriptor -import android.provider.DocumentsContract -import android.provider.DocumentsContract.Document.COLUMN_DISPLAY_NAME -import android.provider.DocumentsContract.Document.COLUMN_DOCUMENT_ID -import android.webkit.MimeTypeMap -import androidx.documentfile.provider.DocumentFile -import com.github.adamantcheese.chan.core.appendManyEncoded -import com.github.adamantcheese.chan.core.getMimeFromFilename -import com.github.adamantcheese.chan.utils.Logger -import java.io.FileDescriptor -import java.io.InputStream -import java.io.OutputStream -import java.util.* - -class ExternalFile( - private val appContext: Context, - private val root: Root, - segments: MutableList = mutableListOf() -) : AbstractFile(segments) { - private val mimeTypeMap = MimeTypeMap.getSingleton() - - override fun appendSubDirSegment(name: String): ExternalFile { - check(root !is Root.FileRoot) { "root is already FileRoot, cannot append anything anymore" } - return super.appendSubDirSegmentInner(name) as ExternalFile - } - - override fun appendFileNameSegment(name: String): ExternalFile { - check(root !is Root.FileRoot) { "root is already FileRoot, cannot append anything anymore" } - return super.appendFileNameSegmentInner(name) as ExternalFile - } - - override fun createNew(): ExternalFile? { - check(root !is Root.FileRoot) { - "root is already FileRoot, cannot append anything anymore" - } - - if (segments.isEmpty()) { - // Root is probably already exists and there is no point in creating it again so just - // return null here - return null - } - - var newFile: DocumentFile? = null - - for (segment in segments) { - val file = newFile ?: root.holder - - val prevFile = file.findFile(segment.name) - if (prevFile != null) { - // File already exists, no need to create it again (and we won't be able) - newFile = prevFile - continue - } - - if (!segment.isFileName) { - newFile = file.createDirectory(segment.name) - if (newFile == null) { - Logger.e(TAG, "createNew() file.createDirectory() returned null, file.uri = ${file.uri}, " + - "segment.name = ${segment.name}") - return null - } - } else { - newFile = file.createFile(mimeTypeMap.getMimeFromFilename(segment.name), segment.name) - if (newFile == null) { - Logger.e(TAG, "createNew() file.createFile returned null, file.uri = ${file.uri}, " + - "segment.name = ${segment.name}") - return null - } - - // Ignore any left segments (which we shouldn't have) after encountering fileName - // segment - return ExternalFile(appContext, Root.FileRoot(newFile, segment.name)) - } - } - - if (newFile == null) { - Logger.e(TAG, "result file is null") - return null - } - - if (segments.size < 1) { - Logger.e(TAG, "Must be at least one segment!") - return null - } - - val lastSegment = segments.last() - val isLastSegmentFilename = lastSegment.isFileName - - val root = if (isLastSegmentFilename) { - Root.FileRoot(newFile, lastSegment.name) - } else { - Root.DirRoot(newFile) - } - - return ExternalFile(appContext, root) - } - - override fun clone(): ExternalFile = ExternalFile( - appContext, - root.clone(), - segments.toMutableList()) - - override fun exists(): Boolean = clone().toDocumentFile()?.exists() ?: false - override fun isFile(): Boolean = clone().toDocumentFile()?.isFile ?: false - override fun isDirectory(): Boolean = clone().toDocumentFile()?.isDirectory ?: false - override fun canRead(): Boolean = clone().toDocumentFile()?.canRead() ?: false - override fun canWrite(): Boolean = clone().toDocumentFile()?.canWrite() ?: false - - override fun getFullPath(): String { - return Uri.parse(root.holder.uri.toString()).buildUpon() - .appendManyEncoded(segments.map { segment -> segment.name }) - .build() - .toString() - } - - override fun delete(): Boolean { - return clone().toDocumentFile()?.delete() ?: false - } - - override fun getInputStream(): InputStream? { - val contentResolver = appContext.contentResolver - val documentFile = clone().toDocumentFile() - - if (documentFile == null) { - Logger.e(TAG, "getInputStream() toDocumentFile() returned null") - return null - } - - if (!documentFile.exists()) { - Logger.e(TAG, "getInputStream() documentFile does not exist, uri = ${documentFile.uri}") - return null - } - - if (!documentFile.isFile) { - Logger.e(TAG, "getInputStream() documentFile is not a file, uri = ${documentFile.uri}") - return null - } - - if (!documentFile.canRead()) { - Logger.e(TAG, "getInputStream() cannot read from documentFile, uri = ${documentFile.uri}") - return null - } - - return contentResolver.openInputStream(documentFile.uri) - } - - override fun getOutputStream(): OutputStream? { - val contentResolver = appContext.contentResolver - val documentFile = clone().toDocumentFile() - - if (documentFile == null) { - Logger.e(TAG, "getOutputStream() toDocumentFile() returned null") - return null - } - - if (!documentFile.exists()) { - Logger.e(TAG, "getOutputStream() documentFile does not exist, uri = ${documentFile.uri}") - return null - } - - if (!documentFile.isFile) { - Logger.e(TAG, "getOutputStream() documentFile is not a file, uri = ${documentFile.uri}") - return null - } - - if (!documentFile.canWrite()) { - Logger.e(TAG, "getOutputStream() cannot write to documentFile, uri = ${documentFile.uri}") - return null - } - - return contentResolver.openOutputStream(documentFile.uri) - } - - override fun getName(): String { - if (segments.isNotEmpty() && segments.last().isFileName) { - return segments.last().name - } - - val documentFile = clone().toDocumentFile() - if (documentFile == null) { - throw IllegalStateException("getName() toDocumentFile() returned null") - } - - return documentFile.name - ?: throw IllegalStateException("Could not extract file name from document file") - } - - override fun findFile(fileName: String): ExternalFile? { - check(root !is Root.FileRoot) { "Cannot use FileRoot as directory" } - - val filteredSegments = segments - .map { it.name } - - var dirTree = root.holder - - for (segment in filteredSegments) { - // FIXME: SLOW!!! - for (documentFile in dirTree.listFiles()) { - if (documentFile.name != null && documentFile.name == segment) { - dirTree = documentFile - break - } - } - } - - // FIXME: SLOW!!! - for (documentFile in dirTree.listFiles()) { - if (documentFile.name != null && documentFile.name == fileName) { - val root = if (documentFile.isFile) { - Root.FileRoot(documentFile, documentFile.name!!) - } else { - Root.DirRoot(documentFile) - } - - return ExternalFile( - appContext, - root) - } - } - - if (dirTree.name == fileName) { - val root = if (dirTree.isFile) { - Root.FileRoot(dirTree, dirTree.name!!) - } else { - Root.DirRoot(dirTree) - } - - return ExternalFile( - appContext, - root) - } - - // Not found - return null - } - - override fun getLength(): Long = clone().toDocumentFile()?.length() ?: -1L - - override fun withFileDescriptor( - fileDescriptorMode: FileDescriptorMode, - func: (FileDescriptor) -> T): Result { - return runCatching { - getParcelFileDescriptor(fileDescriptorMode)?.use { pfd -> - func(pfd.fileDescriptor) - } ?: throw IllegalStateException("Could not get ParcelFileDescriptor " + - "from root with uri = ${root.holder.uri}") - } - } - - override fun listFiles(): List { - check(root !is Root.FileRoot) { "Cannot use listFiles with FileRoot" } - - return clone() - .toDocumentFile() - ?.listFiles() - ?.map { documentFile -> ExternalFile(appContext, Root.DirRoot(documentFile)) } - ?: emptyList() - } - - override fun lastModified(): Long { - return clone().toDocumentFile()?.lastModified() ?: 0L - } - - private fun getParcelFileDescriptor(fileDescriptorMode: FileDescriptorMode): ParcelFileDescriptor? { - return appContext.contentResolver.openFileDescriptor( - root.holder.uri, - fileDescriptorMode.mode) - } - - private fun toDocumentFile(): DocumentFile? { - if (segments.isEmpty()) { - return root.holder - } - - var documentFile: DocumentFile = root.holder - var index = 0 - - for (i in 0 until segments.size) { - val segment = segments[i] - - val file = fastFindFile(documentFile, segment) - if (file == null) { - break - } - - documentFile = file - ++index - } - - if (index != segments.size) { - return createDocumentFileFromUri(documentFile.uri, index) - } - - return documentFile - } - - private fun fastFindFile(root: DocumentFile, segment: Segment): DocumentFile? { - val name = 0 - val documentId = 1 - val selection = "$COLUMN_DISPLAY_NAME = ?" - val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree( - root.uri, - DocumentsContract.getDocumentId(root.uri)) - val projection = arrayOf(COLUMN_DISPLAY_NAME, COLUMN_DOCUMENT_ID) - val contentResolver = appContext.contentResolver - val lowerCaseFilename = segment.name.toLowerCase(Locale.US) - - return contentResolver.query( - childrenUri, - projection, - selection, - arrayOf(lowerCaseFilename), - null - )?.use { cursor -> - while (cursor.moveToNext()) { - if (cursor.isNull(name)) { - continue - } - - val foundFileName = cursor.getString(name) - ?: continue - - if (!foundFileName.toLowerCase(Locale.US).startsWith(lowerCaseFilename)) { - continue - } - - val uri = DocumentsContract.buildDocumentUriUsingTree( - root.uri, - cursor.getString(documentId)) - - return@use DocumentFile.fromSingleUri(appContext, uri) - } - - return@use null - } - } - - private fun createDocumentFileFromUri(uri: Uri, index: Int): DocumentFile? { - val builder = uri.buildUpon() - - for (i in index until segments.size) { - builder.appendPath(segments[i].name) - } - - return DocumentFile.fromSingleUri(appContext, builder.build()) - } - - companion object { - private const val TAG = "FileManager" - } -} \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/FileDescriptorMode.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/FileDescriptorMode.kt deleted file mode 100644 index 4259624ee8..0000000000 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/FileDescriptorMode.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.github.adamantcheese.chan.core.saf.file - -enum class FileDescriptorMode(val mode: String) { - Read("r"), - Write("w"), - // When overwriting an existing file it is a really good ide to use truncate mode, - // because otherwise if a new file's length is less than the old one's then there will be - // old file's data left at the end of the file. Truncate flags will make sure that the file - // is truncated at the end to fit the new length. - WriteTruncate("wt") - - // ReadWrite and ReadWriteTruncate are not supported! -} \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt deleted file mode 100644 index 3b704aa5f7..0000000000 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saf/file/RawFile.kt +++ /dev/null @@ -1,209 +0,0 @@ -package com.github.adamantcheese.chan.core.saf.file - -import com.github.adamantcheese.chan.core.appendMany -import com.github.adamantcheese.chan.utils.Logger -import java.io.* - -class RawFile( - private val root: Root, - segments: MutableList = mutableListOf() -) : AbstractFile(segments) { - - override fun appendSubDirSegment(name: String): RawFile { - check(root !is Root.FileRoot) { "root is already FileRoot, cannot append anything anymore" } - return super.appendSubDirSegmentInner(name) as RawFile - } - - override fun appendFileNameSegment(name: String): RawFile { - check(root !is Root.FileRoot) { "root is already FileRoot, cannot append anything anymore" } - return super.appendFileNameSegmentInner(name) as RawFile - } - - override fun createNew(): RawFile? { - check(root !is Root.FileRoot) { - "root is already FileRoot, cannot append anything anymore" - } - - if (segments.isEmpty()) { - if (!root.holder.exists()) { - if (root.holder.isFile) { - if (!root.holder.createNewFile()) { - Logger.e(TAG, "Couldn't create file, path = ${root.holder.absolutePath}") - return null - } - } else { - if (!root.holder.mkdirs()) { - Logger.e(TAG, "Couldn't create directory, path = ${root.holder.absolutePath}") - return null - } - } - } - - return this - } - - var newFile = root.holder - - for (segment in segments) { - newFile = File(newFile, segment.name) - - if (segment.isFileName) { - if (!newFile.exists() && !newFile.createNewFile()) { - Logger.e(TAG, "Could not create a new file, path = " + newFile.absolutePath) - return null - } - } else { - if (!newFile.exists() && !newFile.mkdir()) { - Logger.e(TAG, "Could not create a new directory, path = " + newFile.absolutePath) - return null - } - } - - if (segment.isFileName) { - return RawFile(Root.FileRoot(newFile, segment.name)) - } - } - - return RawFile(Root.DirRoot(newFile)) - } - - override fun clone(): RawFile = RawFile( - root.clone(), - segments.toMutableList()) - - override fun exists(): Boolean = clone().toFile().exists() - override fun isFile(): Boolean = clone().toFile().isFile - override fun isDirectory(): Boolean = clone().toFile().isDirectory - override fun canRead(): Boolean = clone().toFile().canRead() - override fun canWrite(): Boolean = clone().toFile().canWrite() - - override fun getFullPath(): String { - return File(root.holder.absolutePath) - .appendMany(segments.map { segment -> segment.name }) - .absolutePath - } - - override fun delete(): Boolean { - return clone().toFile().delete() - } - - override fun getInputStream(): InputStream? { - val file = clone().toFile() - - if (!file.exists()) { - Logger.e(TAG, "getInputStream() file does not exist, path = ${file.absolutePath}") - return null - } - - if (!file.isFile) { - Logger.e(TAG, "getInputStream() file is not a file, path = ${file.absolutePath}") - return null - } - - if (!file.canRead()) { - Logger.e(TAG, "getInputStream() cannot read from file, path = ${file.absolutePath}") - return null - } - - return file.inputStream() - } - - override fun getOutputStream(): OutputStream? { - val file = clone().toFile() - - if (!file.exists()) { - Logger.e(TAG, "getOutputStream() file does not exist, path = ${file.absolutePath}") - return null - } - - if (!file.isFile) { - Logger.e(TAG, "getOutputStream() file is not a file, path = ${file.absolutePath}") - return null - } - - if (!file.canWrite()) { - Logger.e(TAG, "getOutputStream() cannot write to file, path = ${file.absolutePath}") - return null - } - - return file.outputStream() - } - - override fun getName(): String { - return clone().toFile().name - } - - override fun findFile(fileName: String): RawFile? { - check(root !is Root.FileRoot) { "Cannot use FileRoot as directory" } - - val copy = File(root.holder.absolutePath) - if (segments.isNotEmpty()) { - copy.appendMany(segments.map { segment -> segment.name }) - } - - val resultFile = File(copy.absolutePath, fileName) - if (!resultFile.exists()) { - return null - } - - val newRoot = if (resultFile.isFile) { - Root.FileRoot(resultFile, resultFile.name) - } else { - Root.DirRoot(resultFile) - } - - return RawFile(newRoot) - } - - override fun getLength(): Long = clone().toFile().length() - - override fun withFileDescriptor( - fileDescriptorMode: FileDescriptorMode, - func: (FileDescriptor) -> T): Result { - return runCatching { - val fileCopy = clone().toFile() - - when (fileDescriptorMode) { - FileDescriptorMode.Read -> FileInputStream(fileCopy).use { fis -> func(fis.fd) } - FileDescriptorMode.Write, - FileDescriptorMode.WriteTruncate -> { - val fileOutputStream = when (fileDescriptorMode) { - FileDescriptorMode.Write -> FileOutputStream(fileCopy, false) - FileDescriptorMode.WriteTruncate -> FileOutputStream(fileCopy, true) - else -> throw NotImplementedError("Not implemented for " + - "fileDescriptorMode = ${fileDescriptorMode.name}") - } - - fileOutputStream.use { fos -> func(fos.fd) } - } - else -> throw NotImplementedError("Not implemented for " + - "fileDescriptorMode = ${fileDescriptorMode.name}") - } - } - } - - override fun lastModified(): Long { - return clone().toFile().lastModified() - } - - override fun listFiles(): List { - check(root !is Root.FileRoot) { "Cannot use listFiles with FileRoot" } - - return clone() - .toFile() - .listFiles() - .map { file -> RawFile(Root.DirRoot(file)) } - } - - private fun toFile(): File { - return if (segments.isEmpty()) { - root.holder - } else { - root.holder.appendMany(segments.map { segment -> segment.name }) - } - } - - companion object { - private const val TAG = "RawFile" - } -} \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaveTask.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaveTask.java index c524904264..393573867c 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaveTask.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaveTask.java @@ -17,18 +17,17 @@ package com.github.adamantcheese.chan.core.saver; import android.content.Intent; -import android.media.MediaScannerConnection; import android.net.Uri; import com.github.adamantcheese.chan.core.cache.FileCache; import com.github.adamantcheese.chan.core.cache.FileCacheListener; import com.github.adamantcheese.chan.core.model.PostImage; import com.github.adamantcheese.chan.core.model.orm.Loadable; -import com.github.adamantcheese.chan.core.saf.FileManager; -import com.github.adamantcheese.chan.core.saf.file.AbstractFile; -import com.github.adamantcheese.chan.core.saf.file.RawFile; import com.github.adamantcheese.chan.utils.AndroidUtils; import com.github.adamantcheese.chan.utils.Logger; +import com.github.k1rakishou.fsaf.FileManager; +import com.github.k1rakishou.fsaf.file.AbstractFile; +import com.github.k1rakishou.fsaf.file.RawFile; import java.io.File; import java.io.IOException; @@ -36,7 +35,6 @@ import javax.inject.Inject; import static com.github.adamantcheese.chan.Chan.inject; -import static com.github.adamantcheese.chan.utils.AndroidUtils.getAppContext; public class ImageSaveTask extends FileCacheListener implements Runnable { private static final String TAG = "ImageSaveTask"; @@ -96,7 +94,7 @@ public boolean getShare() { @Override public void run() { try { - if (destination.exists()) { + if (fileManager.exists(destination)) { onDestination(); // Manually call postFinished() postFinished(success); @@ -123,8 +121,8 @@ public void onEnd() { } private void deleteDestination() { - if (destination.exists()) { - if (!destination.delete()) { + if (fileManager.exists(destination)) { + if (!fileManager.delete(destination)) { Logger.e(TAG, "Could not delete destination after an interrupt"); } } @@ -146,24 +144,12 @@ private boolean copyToDestination(File source) { boolean result = false; try { - AbstractFile createdDestinationFile = destination.createNew(); + AbstractFile createdDestinationFile = fileManager.create(destination); if (createdDestinationFile == null) { throw new IOException("Could not create destination file, path = " + destination.getFullPath()); } - // TODO: check whether this change broke something - // If destination is a raw file then we need to check whether the parent directory exists. - // Otherwise we don't -// if (createdDestinationFile instanceof RawFile) { -// AbstractFile parent = createdDestinationFile -// .clone() // TODO: do we need to clone this file? -// .getParent(); -// if (parent == null || (!parent.create() && !parent.isDirectory())) { -// throw new IOException("Could not create parent directory"); -// } -// } - - if (createdDestinationFile.isDirectory()) { + if (fileManager.isDirectory(createdDestinationFile)) { throw new IOException("Destination file is already a directory"); } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaver.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaver.java index c135a971db..995b03c522 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaver.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaver.java @@ -27,12 +27,15 @@ import com.github.adamantcheese.chan.R; import com.github.adamantcheese.chan.StartActivity; import com.github.adamantcheese.chan.core.model.PostImage; -import com.github.adamantcheese.chan.core.saf.FileManager; -import com.github.adamantcheese.chan.core.saf.file.AbstractFile; import com.github.adamantcheese.chan.core.settings.ChanSettings; import com.github.adamantcheese.chan.ui.helper.RuntimePermissionsHelper; import com.github.adamantcheese.chan.ui.service.SavingNotification; +import com.github.adamantcheese.chan.ui.settings.base_directory.FilesBaseDirectory; import com.github.adamantcheese.chan.utils.Logger; +import com.github.k1rakishou.fsaf.FileManager; +import com.github.k1rakishou.fsaf.file.AbstractFile; +import com.github.k1rakishou.fsaf.file.DirectorySegment; +import com.github.k1rakishou.fsaf.file.FileSegment; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; @@ -97,18 +100,16 @@ private void startDownloadTaskInternal( String fileName = filterName(name + "." + postImage.extension); AbstractFile saveFile = saveLocation - .clone() - .appendFileNameSegment(fileName); + .clone(new FileSegment(fileName)); - while (saveFile.exists()) { + while (fileManager.exists(saveFile)) { String resultFileName = name + "_" + Long.toString(SystemClock.elapsedRealtimeNanos(), Character.MAX_RADIX) + "." + postImage.extension; fileName = filterName(resultFileName); saveFile = saveLocation - .clone() - .appendFileNameSegment(fileName); + .clone(new FileSegment(fileName)); } task.setDestination(saveFile); @@ -147,25 +148,27 @@ public String getSubFolder(String name) { @Nullable public AbstractFile getSaveLocation(ImageSaveTask task) { - AbstractFile baseSaveDir = fileManager.newSaveLocationFile(); + AbstractFile baseSaveDir = fileManager.newBaseDirectoryFile(FilesBaseDirectory.class); if (baseSaveDir == null) { Logger.e(TAG, "getSaveLocation() fileManager.newSaveLocationFile() returned null"); return null; } - if (!baseSaveDir.exists() && !baseSaveDir.create()) { + AbstractFile createdBaseSaveDir = fileManager.create(baseSaveDir); + + if (!fileManager.exists(baseSaveDir) || createdBaseSaveDir == null) { Logger.e(TAG, "Couldn't create base image save directory"); return null; } - if (!fileManager.baseSaveLocalDirectoryExists()) { + if (!fileManager.baseDirectoryExists(FilesBaseDirectory.class)) { Logger.e(TAG, "Base save local directory does not exist"); return null; } String subFolder = task.getSubFolder(); if (subFolder != null) { - baseSaveDir.appendSubDirSegment(subFolder); + baseSaveDir.clone(new DirectorySegment(subFolder)); } return baseSaveDir; @@ -211,8 +214,7 @@ private boolean startBundledTaskInternal(String subFolder, List t } AbstractFile destinationFile = saveLocation - .appendSubDirSegment(subFolder) - .appendFileNameSegment(fileName); + .clone(new DirectorySegment(subFolder), new FileSegment(fileName)); task.setDestination(destinationFile); startTask(task); @@ -277,7 +279,7 @@ private String getText(ImageSaveTask task, boolean success, boolean wasAlbumSave } else { text = getAppContext().getString( R.string.image_save_as, - task.getDestination().getName()); + fileManager.getName(task.getDestination())); } } else { text = getString(R.string.image_save_failed); diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ImportExportSettingsController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ImportExportSettingsController.java index 257734f304..00106d440a 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ImportExportSettingsController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ImportExportSettingsController.java @@ -28,15 +28,16 @@ import com.github.adamantcheese.chan.StartActivity; import com.github.adamantcheese.chan.core.presenter.ImportExportSettingsPresenter; import com.github.adamantcheese.chan.core.repository.ImportExportRepository; -import com.github.adamantcheese.chan.core.saf.FileManager; -import com.github.adamantcheese.chan.core.saf.callback.FileChooserCallback; -import com.github.adamantcheese.chan.core.saf.callback.FileCreateCallback; -import com.github.adamantcheese.chan.core.saf.file.ExternalFile; import com.github.adamantcheese.chan.ui.settings.LinkSettingView; import com.github.adamantcheese.chan.ui.settings.SettingsController; import com.github.adamantcheese.chan.ui.settings.SettingsGroup; import com.github.adamantcheese.chan.utils.AndroidUtils; import com.github.adamantcheese.chan.utils.Logger; +import com.github.k1rakishou.fsaf.FileChooser; +import com.github.k1rakishou.fsaf.FileManager; +import com.github.k1rakishou.fsaf.callback.FileChooserCallback; +import com.github.k1rakishou.fsaf.callback.FileCreateCallback; +import com.github.k1rakishou.fsaf.file.ExternalFile; import org.jetbrains.annotations.NotNull; @@ -52,6 +53,8 @@ public class ImportExportSettingsController extends SettingsController implement @Inject FileManager fileManager; + @Inject + FileChooser fileChooser; private ImportExportSettingsPresenter presenter; @@ -141,7 +144,7 @@ private void onExportClicked() { * Opens an existing file (any file) for overwriting with the settings. * */ private void overwriteExisting() { - fileManager.openChooseFileDialog(new FileChooserCallback() { + fileChooser.openChooseFileDialog(new FileChooserCallback() { @Override public void onResult(@NotNull Uri uri) { onFileChosen(uri, false); @@ -160,7 +163,7 @@ public void onCancel(@NotNull String reason) { * with appended "(1)" at the end will appear, e.g. "test (1).txt") * */ private void createNew() { - fileManager.openCreateFileDialog(EXPORT_FILE_NAME, new FileCreateCallback() { + fileChooser.openCreateFileDialog(EXPORT_FILE_NAME, new FileCreateCallback() { @Override public void onResult(@NotNull Uri uri) { onFileChosen(uri, true); @@ -190,7 +193,7 @@ private void onFileChosen(Uri uri, boolean isNewFile) { } private void onImportClicked() { - fileManager.openChooseFileDialog(new FileChooserCallback() { + fileChooser.openChooseFileDialog(new FileChooserCallback() { @Override public void onResult(@NotNull Uri uri) { ExternalFile externalFile = fileManager.fromUri(uri); diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/LoadingViewController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/LoadingViewController.java index 0d9eeb81bd..317ecb22ff 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/LoadingViewController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/LoadingViewController.java @@ -42,6 +42,18 @@ public void updateProgress(int percent) { textView.setText(String.valueOf(percent)); } + public void updateWithText(String text) { + if (indeterminate) { + throw new IllegalStateException("Cannot be used with indeterminate flag"); + } + + if (textView.getVisibility() != View.VISIBLE) { + textView.setVisibility(View.VISIBLE); + } + + textView.setText(text); + } + @Override protected int getLayoutId() { return R.layout.controller_loading_view; diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java index 6a785084dd..d564d61634 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java @@ -21,15 +21,9 @@ import android.net.Uri; import android.widget.Toast; -import androidx.documentfile.provider.DocumentFile; +import androidx.annotation.NonNull; import com.github.adamantcheese.chan.R; -import com.github.adamantcheese.chan.core.database.DatabaseManager; -import com.github.adamantcheese.chan.core.saf.FileManager; -import com.github.adamantcheese.chan.core.saf.callback.DirectoryChooserCallback; -import com.github.adamantcheese.chan.core.saf.file.AbstractFile; -import com.github.adamantcheese.chan.core.saf.file.ExternalFile; -import com.github.adamantcheese.chan.core.saf.file.FileDescriptorMode; import com.github.adamantcheese.chan.core.settings.ChanSettings; import com.github.adamantcheese.chan.ui.settings.BooleanSettingView; import com.github.adamantcheese.chan.ui.settings.LinkSettingView; @@ -37,21 +31,22 @@ import com.github.adamantcheese.chan.ui.settings.SettingView; import com.github.adamantcheese.chan.ui.settings.SettingsController; import com.github.adamantcheese.chan.ui.settings.SettingsGroup; +import com.github.adamantcheese.chan.ui.settings.base_directory.LocalThreadsBaseDirectory; import com.github.adamantcheese.chan.utils.Logger; +import com.github.k1rakishou.fsaf.FileChooser; +import com.github.k1rakishou.fsaf.FileManager; +import com.github.k1rakishou.fsaf.callback.DirectoryChooserCallback; +import com.github.k1rakishou.fsaf.document_file.CachingDocumentFile; +import com.github.k1rakishou.fsaf.file.AbstractFile; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStreamWriter; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.concurrent.Executor; import java.util.concurrent.Executors; @@ -78,9 +73,8 @@ public class MediaSettingsController extends SettingsController { @Inject FileManager fileManager; - @Inject - DatabaseManager databaseManager; + FileChooser fileChooser; public MediaSettingsController(Context context) { super(context); @@ -266,6 +260,7 @@ private void showUseSAFOrOldAPIForLocalThreadsLocationDialog() { }) .setNegativeButton(R.string.use_saf_dialog_negative_button_text, (dialog, which) -> { onLocalThreadsLocationUseOldApiClicked(); + dialog.dismiss(); }) .create(); @@ -280,7 +275,9 @@ private void onLocalThreadsLocationUseOldApiClicked() { context, SaveLocationController.SaveLocationControllerMode.LocalThreadsSaveLocation, dirPath -> { - AbstractFile oldLocalThreadsDirectory = fileManager.newLocalThreadFile(); + AbstractFile oldLocalThreadsDirectory = fileManager.newBaseDirectoryFile( + LocalThreadsBaseDirectory.class + ); Logger.d(TAG, "SaveLocationController with LocalThreadsSaveLocation mode returned dir " + dirPath); @@ -289,7 +286,10 @@ private void onLocalThreadsLocationUseOldApiClicked() { ChanSettings.localThreadLocation.setSync(""); ChanSettings.localThreadLocation.setSync(dirPath); - AbstractFile newLocalThreadsDirectory = fileManager.newLocalThreadFile(); + AbstractFile newLocalThreadsDirectory = fileManager.newBaseDirectoryFile( + LocalThreadsBaseDirectory.class + ); + askUserIfTheyWantToMoveOldThreadsToTheNewDirectory( oldLocalThreadsDirectory, newLocalThreadsDirectory); @@ -302,10 +302,14 @@ private void onLocalThreadsLocationUseOldApiClicked() { * Select a directory where local threads will be stored via the SAF */ private void onLocalThreadsLocationUseSAFClicked() { - fileManager.openChooseDirectoryDialog(new DirectoryChooserCallback() { + fileChooser.openChooseDirectoryDialog(new DirectoryChooserCallback() { @Override public void onResult(@NotNull Uri uri) { - AbstractFile oldLocalThreadsDirectory = fileManager.newLocalThreadFile(); + // TODO: check that there are no files in the directory and warn the user that something + // might go wrong in this case + AbstractFile oldLocalThreadsDirectory = fileManager.newBaseDirectoryFile( + LocalThreadsBaseDirectory.class + ); ChanSettings.localThreadsLocationUri.set(uri.toString()); String defaultDir = ChanSettings.getDefaultLocalThreadsLocation(); @@ -313,7 +317,10 @@ public void onResult(@NotNull Uri uri) { ChanSettings.localThreadLocation.setNoUpdate(defaultDir); localThreadsLocation.setDescription(uri.toString()); - AbstractFile newLocalThreadsDirectory = fileManager.newLocalThreadFile(); + AbstractFile newLocalThreadsDirectory = fileManager.newBaseDirectoryFile( + LocalThreadsBaseDirectory.class + ); + askUserIfTheyWantToMoveOldThreadsToTheNewDirectory( oldLocalThreadsDirectory, newLocalThreadsDirectory); @@ -337,7 +344,7 @@ private void askUserIfTheyWantToMoveOldThreadsToTheNewDirectory( .setPositiveButton("Move", (dialog, which) -> { moveOldFilesToTheNewDirectory(oldLocalThreadsDirectory, newLocalThreadsDirectory); }) - .setNegativeButton("Do not move", (dialog, which) -> {}) + .setNegativeButton("Do not", (dialog, which) -> dialog.dismiss()) .create(); alertDialog.show(); @@ -356,41 +363,84 @@ private void moveOldFilesToTheNewDirectory( Logger.d(TAG, "oldLocalThreadsDirectory = " + oldLocalThreadsDirectory.getFullPath() + ", newLocalThreadsDirectory = " + newLocalThreadsDirectory.getFullPath()); - navigationController.pushController(new LoadingViewController(context, true)); + int filesCount = fileManager.listFiles(oldLocalThreadsDirectory).size(); + if (filesCount == 0) { + Toast.makeText(context, "No files to copy", Toast.LENGTH_SHORT).show(); + return; + } - fileCopyingExecutor.execute(() -> { - boolean result = fileManager.copyDirectoryWithContent( - oldLocalThreadsDirectory, - newLocalThreadsDirectory); + // TODO: Strings + AlertDialog alertDialog = new AlertDialog.Builder(context) + .setTitle("Copy files") + .setMessage("Do you want to copy " + filesCount + " from an old directory to the new one?") + .setPositiveButton("Copy", ((dialog, which) -> { + LoadingViewController loadingViewController = new LoadingViewController( + context, + false); + navigationController.pushController(loadingViewController); + + fileCopyingExecutor.execute(() -> { + moveFilesInternal( + oldLocalThreadsDirectory, + newLocalThreadsDirectory, + loadingViewController); + }); + })) + .setNegativeButton("Do not", ((dialog, which) -> dialog.dismiss())) + .create(); - runOnUiThread(() -> { - navigationController.popController(); + alertDialog.show(); + } - if (!result) { - // TODO: strings - Toast.makeText( - context, - "Could not copy one directory's file into another one", - Toast.LENGTH_LONG - ).show(); - } else { - if (!fileManager.forgetSAFTree(oldLocalThreadsDirectory)) { - // TODO: strings - Toast.makeText( - context, - "Files were copied but couldn't remove SAF permissions from the old directory", - Toast.LENGTH_SHORT - ).show(); - } else { + private void moveFilesInternal( + @NonNull AbstractFile oldLocalThreadsDirectory, + @NonNull AbstractFile newLocalThreadsDirectory, + LoadingViewController loadingViewController) { + boolean result = fileManager.copyDirectoryWithContent( + oldLocalThreadsDirectory, + newLocalThreadsDirectory, + false, + (fileIndex, totalFilesCount) -> { + runOnUiThread(() -> { // TODO: strings - Toast.makeText( - context, - "Successfully copied files", - Toast.LENGTH_LONG - ).show(); - } - } - }); + String text = String.format( + Locale.US, + // TODO: strings + "Copying file %d out of %d", + fileIndex, + totalFilesCount); + + loadingViewController.updateWithText(text); + }); + + return Unit.INSTANCE; + }); + + runOnUiThread(() -> { + navigationController.popController(); + + if (!result) { + // TODO: strings + Toast.makeText( + context, + "Could not copy one directory's file into another one", + Toast.LENGTH_LONG + ).show(); + } else { + Uri safTreeuri = oldLocalThreadsDirectory + .getFileRoot().getHolder().getUri(); + + fileChooser.forgetSAFTree(safTreeuri); + + // TODO: delete old directory dialog + + // TODO: strings + Toast.makeText( + context, + "Successfully copied files", + Toast.LENGTH_LONG + ).show(); + } }); } @@ -403,6 +453,7 @@ private void showUseSAFOrOldAPIForSaveLocationDialog() { }) .setNegativeButton(R.string.use_saf_dialog_negative_button_text, (dialog, which) -> { onSaveLocationUseOldApiClicked(); + dialog.dismiss(); }) .create(); @@ -432,15 +483,16 @@ private void onSaveLocationUseOldApiClicked() { * Select a directory where saved images will be stored via the SAF */ private void onSaveLocationUseSAFClicked() { - fileManager.openChooseDirectoryDialog(new DirectoryChooserCallback() { + fileChooser.openChooseDirectoryDialog(new DirectoryChooserCallback() { @Override public void onResult(@NotNull Uri uri) { + // TODO: check that there are no files in the directory at warn user that something + // might go wrong in this case ChanSettings.saveLocationUri.set(uri.toString()); String defaultDir = ChanSettings.getDefaultSaveLocationDir(); ChanSettings.saveLocation.setNoUpdate(defaultDir); saveLocation.setDescription(uri.toString()); - } @Override @@ -450,175 +502,6 @@ public void onCancel(@NotNull String reason) { }); } - private void testMethod(@NotNull Uri uri) { - { - ExternalFile externalFile = fileManager.fromUri(uri) - .appendSubDirSegment("123") - .appendSubDirSegment("456") - .appendSubDirSegment("789") - .appendFileNameSegment("test123.txt") - .createNew(); - - if (!externalFile.isFile()) { - throw new RuntimeException("test123.txt is not a file"); - } - - if (externalFile.isDirectory()) { - throw new RuntimeException("test123.txt is a directory"); - } - - if (externalFile == null || !externalFile.exists()) { - throw new RuntimeException("Couldn't create test123.txt"); - } - - if (!externalFile.getName().equals("test123.txt")) { - throw new RuntimeException("externalFile name != test123.txt"); - } - - boolean externalFile2Exists = fileManager.fromUri(uri) - .appendSubDirSegment("123") - .appendSubDirSegment("456") - .appendSubDirSegment("789") - .exists(); - - if (!externalFile2Exists) { - throw new RuntimeException("789 directory does not exist"); - } - - if (!externalFile.delete() && externalFile.exists()) { - throw new RuntimeException("Couldn't delete test123.txt"); - } - } - - { - AbstractFile externalFile = fileManager.newSaveLocationFile() - .appendSubDirSegment("1234") - .appendSubDirSegment("4566") - .appendFileNameSegment("filename.json") - .createNew(); - - if (externalFile == null || !externalFile.exists()) { - throw new RuntimeException("Couldn't create filename.json"); - } - - if (!externalFile.isFile()) { - throw new RuntimeException("filename.json is not a file"); - } - - if (externalFile.isDirectory()) { - throw new RuntimeException("filename.json is not a directory"); - } - - if (!externalFile.getName().equals("filename.json")) { - throw new RuntimeException("externalFile1 name != filename.json"); - } - - AbstractFile dir = fileManager.newSaveLocationFile() - .appendSubDirSegment("1234") - .appendSubDirSegment("4566"); - - if (!dir.getName().equals("4566")) { - throw new RuntimeException("dir.name != 4566, name = " + dir.getName()); - } - - AbstractFile foundFile = dir.findFile("filename.json"); - if (foundFile == null || !foundFile.exists()) { - throw new RuntimeException("Couldn't find filename.json"); - } - - // Write string to the file - String testString = "Hello world"; - - foundFile.withFileDescriptor(FileDescriptorMode.WriteTruncate, (fd) -> { - try (OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(fd))) { - osw.write(testString); - osw.flush(); - } catch (IOException e) { - e.printStackTrace(); - } - - return Unit.INSTANCE; - }); - - if (foundFile.getLength() != testString.length()) { - throw new RuntimeException("file length != testString.length(), file length = " - + foundFile.getLength()); - } - - foundFile.withFileDescriptor(FileDescriptorMode.Read, (fd) -> { - try (InputStreamReader isr = new InputStreamReader(new FileInputStream(fd))) { - char[] stringBytes = new char[testString.length()]; - int read = isr.read(stringBytes); - - if (read != testString.length()) { - throw new RuntimeException("read bytes != testString.length(), read = " + read); - } - - String resultString = new String(stringBytes); - if (!resultString.equals(testString)) { - throw new RuntimeException("resultString != testString, resultString = " - + resultString); - } - - } catch (IOException e) { - e.printStackTrace(); - } - - return Unit.INSTANCE; - }); - - // Write another string that is shorter than the previous string - String testString2 = "Hello"; - - foundFile.withFileDescriptor(FileDescriptorMode.WriteTruncate, (fd) -> { - try (OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(fd))) { - osw.write(testString2); - osw.flush(); - } catch (IOException e) { - e.printStackTrace(); - } - - return Unit.INSTANCE; - }); - - if (foundFile.getLength() != testString2.length()) { - throw new RuntimeException("file length != testString.length(), file length = " - + foundFile.getLength()); - } - - foundFile.withFileDescriptor(FileDescriptorMode.Read, (fd) -> { - try (InputStreamReader isr = new InputStreamReader(new FileInputStream(fd))) { - char[] stringBytes = new char[testString2.length()]; - int read = isr.read(stringBytes); - - if (read != testString2.length()) { - throw new RuntimeException("read bytes != testString2.length(), read = " + read); - } - - String resultString = new String(stringBytes); - if (!resultString.equals(testString2)) { - throw new RuntimeException("resultString != testString2, resultString = " - + resultString); - } - - } catch (IOException e) { - e.printStackTrace(); - } - - return Unit.INSTANCE; - }); - } - - { - ExternalFile externalFile = fileManager.fromUri(uri); - if (!externalFile.getName().equals("Test")) { - throw new RuntimeException("externalFile.name != Test, name = " + externalFile.getName()); - } - } - - System.out.println("All tests passed!"); - } - private void setupMediaLoadTypesSetting(SettingsGroup loading) { List imageAutoLoadTypes = new ArrayList<>(); List videoAutoLoadTypes = new ArrayList<>(); diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ViewThreadController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ViewThreadController.java index 4c719aa5f7..3ed28223c7 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ViewThreadController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ViewThreadController.java @@ -44,27 +44,26 @@ import com.github.adamantcheese.chan.core.model.orm.PinType; import com.github.adamantcheese.chan.core.model.orm.SavedThread; import com.github.adamantcheese.chan.core.presenter.ThreadPresenter; -import com.github.adamantcheese.chan.core.saf.FileManager; -import com.github.adamantcheese.chan.core.saf.file.AbstractFile; import com.github.adamantcheese.chan.core.settings.ChanSettings; import com.github.adamantcheese.chan.ui.helper.HintPopup; import com.github.adamantcheese.chan.ui.helper.RuntimePermissionsHelper; import com.github.adamantcheese.chan.ui.layout.ArchivesLayout; import com.github.adamantcheese.chan.ui.layout.ThreadLayout; +import com.github.adamantcheese.chan.ui.settings.base_directory.LocalThreadsBaseDirectory; import com.github.adamantcheese.chan.ui.toolbar.NavigationItem; import com.github.adamantcheese.chan.ui.toolbar.Toolbar; -import com.github.adamantcheese.chan.ui.toolbar.ToolbarMenu; import com.github.adamantcheese.chan.ui.toolbar.ToolbarMenuItem; import com.github.adamantcheese.chan.ui.toolbar.ToolbarMenuSubItem; import com.github.adamantcheese.chan.utils.AndroidUtils; import com.github.adamantcheese.chan.utils.AnimationUtils; import com.github.adamantcheese.chan.utils.Logger; +import com.github.k1rakishou.fsaf.FileManager; +import com.github.k1rakishou.fsaf.file.AbstractFile; import org.greenrobot.eventbus.Subscribe; import java.util.ArrayDeque; import java.util.Deque; -import java.util.List; import javax.inject.Inject; @@ -226,7 +225,10 @@ private void saveClicked(ToolbarMenuItem item) { } private void saveClickedInternal() { - AbstractFile baseLocalThreadsDir = fileManager.newLocalThreadFile(); + AbstractFile baseLocalThreadsDir = fileManager.newBaseDirectoryFile( + LocalThreadsBaseDirectory.class + ); + if (baseLocalThreadsDir == null) { Logger.e(TAG, "saveClickedInternal() fileManager.newLocalThreadFile() returned null"); Toast.makeText( @@ -236,7 +238,8 @@ private void saveClickedInternal() { return; } - if (!baseLocalThreadsDir.exists() && !baseLocalThreadsDir.create()) { + if (!fileManager.exists(baseLocalThreadsDir) + && fileManager.create(baseLocalThreadsDir) == null) { Logger.e(TAG, "saveClickedInternal() Couldn't create baseLocalThreadsDir"); Toast.makeText( context, @@ -245,7 +248,7 @@ private void saveClickedInternal() { return; } - if (!fileManager.baseLocalThreadsDirectoryExists()) { + if (!fileManager.baseDirectoryExists(LocalThreadsBaseDirectory.class)) { Logger.e(TAG, "Base local threads directory does not exist"); Toast.makeText( context, diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/helper/ImagePickDelegate.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/helper/ImagePickDelegate.java index 3abe934cea..63c5d5b3cd 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/helper/ImagePickDelegate.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/helper/ImagePickDelegate.java @@ -31,10 +31,10 @@ import com.github.adamantcheese.chan.core.cache.FileCache; import com.github.adamantcheese.chan.core.cache.FileCacheListener; import com.github.adamantcheese.chan.core.manager.ReplyManager; -import com.github.adamantcheese.chan.core.saf.FileManager; -import com.github.adamantcheese.chan.core.saf.file.RawFile; import com.github.adamantcheese.chan.utils.IOUtils; import com.github.adamantcheese.chan.utils.Logger; +import com.github.k1rakishou.fsaf.FileManager; +import com.github.k1rakishou.fsaf.file.RawFile; import java.io.File; import java.io.FileInputStream; @@ -187,7 +187,7 @@ public void run() { } is = new FileInputStream(fileDescriptor.getFileDescriptor()); - os = cacheFile.getOutputStream(); + os = fileManager.getOutputStream(cacheFile); if (os == null) { throw new IOException("Could not get OutputStream from the cacheFile, " + @@ -206,7 +206,7 @@ public void run() { } if (!success) { - if (!cacheFile.delete()) { + if (!fileManager.delete(cacheFile)) { Logger.e(TAG, "Could not delete picked_file after copy fail"); } } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/settings/base_directory/FilesBaseDirectory.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/settings/base_directory/FilesBaseDirectory.kt new file mode 100644 index 0000000000..2d28476e9e --- /dev/null +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/settings/base_directory/FilesBaseDirectory.kt @@ -0,0 +1,10 @@ +package com.github.adamantcheese.chan.ui.settings.base_directory + +import android.net.Uri +import com.github.k1rakishou.fsaf.manager.base_directory.BaseDirectory +import java.io.File + +class FilesBaseDirectory( + dirUri: Uri?, + dirFile: File? +) : BaseDirectory(dirUri, dirFile) \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/settings/base_directory/LocalThreadsBaseDirectory.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/settings/base_directory/LocalThreadsBaseDirectory.kt new file mode 100644 index 0000000000..fa2092bb91 --- /dev/null +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/settings/base_directory/LocalThreadsBaseDirectory.kt @@ -0,0 +1,10 @@ +package com.github.adamantcheese.chan.ui.settings.base_directory + +import android.net.Uri +import com.github.k1rakishou.fsaf.manager.base_directory.BaseDirectory +import java.io.File + +class LocalThreadsBaseDirectory( + dirUri: Uri?, + dirFile: File? +) : BaseDirectory(dirUri, dirFile) \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/view/MultiImageView.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/view/MultiImageView.java index 442327c055..5ff15107fd 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/view/MultiImageView.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/view/MultiImageView.java @@ -29,7 +29,6 @@ import android.widget.ImageView; import android.widget.Toast; -import androidx.annotation.NonNull; import androidx.core.content.FileProvider; import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleObserver; @@ -49,11 +48,10 @@ import com.github.adamantcheese.chan.core.image.ImageLoaderV2; import com.github.adamantcheese.chan.core.model.PostImage; import com.github.adamantcheese.chan.core.model.orm.Loadable; -import com.github.adamantcheese.chan.core.saf.file.AbstractFile; -import com.github.adamantcheese.chan.core.saf.file.RawFile; import com.github.adamantcheese.chan.core.settings.ChanSettings; import com.github.adamantcheese.chan.utils.AndroidUtils; import com.github.adamantcheese.chan.utils.Logger; +import com.github.k1rakishou.fsaf.file.RawFile; import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; @@ -67,7 +65,6 @@ import java.io.File; import java.io.IOException; -import java.io.InputStream; import javax.inject.Inject; diff --git a/Kuroba/build.gradle b/Kuroba/build.gradle index 12844ece62..ead519acaa 100644 --- a/Kuroba/build.gradle +++ b/Kuroba/build.gradle @@ -1,14 +1,15 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.3.41' + ext.kotlin_version = '1.3.50' repositories { google() jcenter() + maven { url 'https://jitpack.io' } } dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath 'com.android.tools.build:gradle:3.5.0' + classpath 'com.android.tools.build:gradle:3.5.1' } } @@ -16,5 +17,6 @@ allprojects { repositories { google() jcenter() + maven { url 'https://jitpack.io' } } } From b2ce0b92f8bc329c6dafcc76f0d9effd2b16c71e Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 20 Oct 2019 17:03:43 +0300 Subject: [PATCH 173/184] (#172) Significantly improved speed of local threads downloading --- .../adamantcheese/chan/core/Extensions.kt | 90 ----- .../chan/core/cache/FileCache.java | 11 +- .../database/DatabaseSavedThreadManager.java | 59 +-- .../adamantcheese/chan/core/di/AppModule.java | 48 +-- .../chan/core/image/ImageLoaderV2.java | 19 +- .../core/manager/SavedThreadLoaderManager.kt | 3 +- .../chan/core/manager/ThreadSaveManager.java | 282 +++++++------ .../MediaSettingsControllerPresenter.kt | 303 ++++++++++++++ .../ExperimentalSettingsController.java | 5 +- .../controller/MediaSettingsController.java | 374 ++++++++---------- .../base_directory/FilesBaseDirectory.kt | 26 +- .../LocalThreadsBaseDirectory.kt | 26 +- .../chan/utils/BackgroundUtils.java | 12 + .../adamantcheese/chan/utils/IOUtils.java | 13 - .../chan/core/ExtensionsKtTest.kt | 29 -- 15 files changed, 742 insertions(+), 558 deletions(-) delete mode 100644 Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/Extensions.kt create mode 100644 Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/MediaSettingsControllerPresenter.kt delete mode 100644 Kuroba/app/src/test/java/com/github/adamantcheese/chan/core/ExtensionsKtTest.kt diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/Extensions.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/Extensions.kt deleted file mode 100644 index 056f1edd95..0000000000 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/Extensions.kt +++ /dev/null @@ -1,90 +0,0 @@ -package com.github.adamantcheese.chan.core - -import android.net.Uri -import android.webkit.MimeTypeMap -import java.io.File - -private const val BINARY_FILE_MIME_TYPE = "application/octet-stream" - -fun String.extension(): String? { - val index = this.indexOfLast { ch -> ch == '.' } - if (index == -1) { - return null - } - - if (index == this.lastIndex) { - // The dot is at the very end of the string, so there is no extension - return null - } - - return this.substring(index + 1) -} - -fun Uri.Builder.appendManyEncoded(segments: List): Uri.Builder { - for (segment in segments) { - this.appendPath(segment) - } - - return this -} - -fun Uri.removeLastSegment(): Uri? { - if (this.pathSegments.size <= 1) { - // I think we shouldn't return "/" directory here since android won't let us access it anyway - return null - } - - val newSegments = this.pathSegments - .subList(0, pathSegments.lastIndex) - - return Uri.Builder() - .appendManyEncoded(newSegments) - .build() -} - -fun MimeTypeMap.getMimeFromFilename(filename: String): String { - val extension = filename.extension() - if (extension == null) { - return BINARY_FILE_MIME_TYPE - } - - val mimeType = this.getMimeTypeFromExtension(extension) - if (mimeType == null || mimeType.isEmpty()) { - return BINARY_FILE_MIME_TYPE - } - - return mimeType -} - -fun File.appendMany(segments: List): File { - var newFile = File(this.absolutePath) - - for (segment in segments) { - newFile = File(newFile, segment) - } - - return newFile -} - -fun Int.toCharArray(): CharArray { - val charArray = CharArray(4) - - charArray[0] = ((this shr 24) and 0x000000FF).toChar() - charArray[1] = ((this shr 16) and 0x000000FF).toChar() - charArray[2] = ((this shr 8) and 0x000000FF).toChar() - charArray[3] = ((this) and 0x000000FF).toChar() - - return charArray -} - -fun CharArray.toInt(): Int { - check(this.size == 4) { "CharArray must have length of exactly 4 bytes" } - - var value: Int = 0 - value = value or (this[0].toInt() shl 24) - value = value or (this[1].toInt() shl 16) - value = value or (this[2].toInt() shl 8) - value = value or (this[3].toInt()) - - return value -} \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/FileCache.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/FileCache.java index 93258697e5..f5655ca7f8 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/FileCache.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/cache/FileCache.java @@ -26,9 +26,9 @@ import com.github.adamantcheese.chan.utils.Logger; import com.github.k1rakishou.fsaf.FileManager; import com.github.k1rakishou.fsaf.file.AbstractFile; -import com.github.k1rakishou.fsaf.file.DirectorySegment; import com.github.k1rakishou.fsaf.file.FileSegment; import com.github.k1rakishou.fsaf.file.RawFile; +import com.github.k1rakishou.fsaf.file.Segment; import java.io.File; import java.io.IOException; @@ -88,12 +88,11 @@ public FileCacheDownloader downloadFile( return null; } - String imageDir = ThreadSaveManager.getImagesSubDir(loadable); + // TODO: double check, may not work as expected + List segments = new ArrayList<>(ThreadSaveManager.getImagesSubDir(loadable)); + segments.add(new FileSegment(filename)); - AbstractFile imageOnDiskFile = baseDirFile.clone( - new DirectorySegment(imageDir), - new FileSegment(filename) - ); + AbstractFile imageOnDiskFile = baseDirFile.clone(segments); if (fileManager.exists(imageOnDiskFile) && fileManager.isFile(imageOnDiskFile) diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/database/DatabaseSavedThreadManager.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/database/DatabaseSavedThreadManager.java index 2bfa0fd230..51cca6f2f2 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/database/DatabaseSavedThreadManager.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/database/DatabaseSavedThreadManager.java @@ -3,16 +3,12 @@ import com.github.adamantcheese.chan.core.manager.ThreadSaveManager; import com.github.adamantcheese.chan.core.model.orm.Loadable; import com.github.adamantcheese.chan.core.model.orm.SavedThread; -import com.github.adamantcheese.chan.core.settings.ChanSettings; import com.github.adamantcheese.chan.ui.settings.base_directory.LocalThreadsBaseDirectory; -import com.github.adamantcheese.chan.utils.IOUtils; import com.github.adamantcheese.chan.utils.Logger; import com.github.k1rakishou.fsaf.FileManager; import com.github.k1rakishou.fsaf.file.AbstractFile; -import com.github.k1rakishou.fsaf.file.DirectorySegment; import com.j256.ormlite.stmt.DeleteBuilder; -import java.io.File; import java.util.List; import java.util.concurrent.Callable; @@ -174,48 +170,35 @@ public Callable deleteSavedThread(Loadable loadable) { db.where().eq(SavedThread.LOADABLE_ID, loadable.id); db.delete(); - deleteThreadFromDisk(loadable, ChanSettings.isLocalThreadsDirUsesSAF()); + deleteThreadFromDisk(loadable); return null; }; } - public void deleteThreadFromDisk(Loadable loadable, boolean usesSAF) { - if (usesSAF) { - String threadSubDir = ThreadSaveManager.getThreadSubDir(loadable); - - AbstractFile localThreadsDir = fileManager.newBaseDirectoryFile( - LocalThreadsBaseDirectory.class - ); - - if (localThreadsDir == null - || !fileManager.exists(localThreadsDir) - || !fileManager.isDirectory(localThreadsDir)) { - // Probably already deleted - return; - } - - AbstractFile threadDir = localThreadsDir - .clone(new DirectorySegment(threadSubDir)); + // TODO: may not work, but in theory it should + public void deleteThreadFromDisk(Loadable loadable) { + AbstractFile localThreadsDir = fileManager.newBaseDirectoryFile( + LocalThreadsBaseDirectory.class + ); - if (!fileManager.exists(threadDir) || !fileManager.isDirectory(threadDir)) { - // Probably already deleted - return; - } + if (localThreadsDir == null + || !fileManager.exists(localThreadsDir) + || !fileManager.isDirectory(localThreadsDir)) { + // Probably already deleted + return; + } - if (!fileManager.delete(threadDir)) { - Logger.d(TAG, "deleteThreadFromDisk() Could not delete SAF directory " - + threadDir.getFullPath()); - } - } else { - String threadSubDir = ThreadSaveManager.getThreadSubDir(loadable); - File threadSaveDir = new File(ChanSettings.localThreadLocation.get(), threadSubDir); + AbstractFile threadDir = localThreadsDir + .clone(ThreadSaveManager.getThreadSubDir(loadable)); - if (!threadSaveDir.exists() || !threadSaveDir.isDirectory()) { - // Probably already deleted - return; - } + if (!fileManager.exists(threadDir) || !fileManager.isDirectory(threadDir)) { + // Probably already deleted + return; + } - IOUtils.deleteDirWithContents(threadSaveDir); + if (!fileManager.delete(threadDir)) { + Logger.d(TAG, "deleteThreadFromDisk() Could not delete SAF directory " + + threadDir.getFullPath()); } } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/AppModule.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/AppModule.java index 79c23995e4..95ec76dc2e 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/AppModule.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/AppModule.java @@ -18,16 +18,12 @@ import android.app.NotificationManager; import android.content.Context; -import android.net.Uri; - -import androidx.annotation.NonNull; import com.android.volley.RequestQueue; import com.android.volley.toolbox.ImageLoader; import com.github.adamantcheese.chan.core.image.ImageLoaderV2; import com.github.adamantcheese.chan.core.net.BitmapLruImageCache; import com.github.adamantcheese.chan.core.saver.ImageSaver; -import com.github.adamantcheese.chan.core.settings.ChanSettings; import com.github.adamantcheese.chan.ui.captcha.CaptchaHolder; import com.github.adamantcheese.chan.ui.settings.base_directory.FilesBaseDirectory; import com.github.adamantcheese.chan.ui.settings.base_directory.LocalThreadsBaseDirectory; @@ -41,8 +37,6 @@ import org.codejargon.feather.Provides; -import java.io.File; - import javax.inject.Singleton; import static android.content.Context.NOTIFICATION_SERVICE; @@ -117,10 +111,9 @@ public FileManager provideFileManager() { ); RawFileManager rawFileManager = new RawFileManager(); - LocalThreadsBaseDirectory localThreadsBaseDirectory = buildLocalThreadsBaseDirectory(); - FilesBaseDirectory filesBaseDirectory = buildImagesBaseDirectory(); - - // Add your base directories here + // Add new base directories here + LocalThreadsBaseDirectory localThreadsBaseDirectory = new LocalThreadsBaseDirectory(); + FilesBaseDirectory filesBaseDirectory = new FilesBaseDirectory(); FileManager fileManager = new FileManager( applicationContext, @@ -146,39 +139,4 @@ public FileManager provideFileManager() { public FileChooser provideFileChooser() { return new FileChooser(applicationContext); } - - private FilesBaseDirectory buildImagesBaseDirectory() { - Uri dirUri = null; - if (ChanSettings.saveLocationUri.get().length() > 0) { - dirUri = Uri.parse(ChanSettings.saveLocationUri.get()); - } - - File dirFile = null; - if (ChanSettings.saveLocation.get().length() > 0) { - dirFile = new File(ChanSettings.saveLocation.get()); - } - - return new FilesBaseDirectory( - dirUri, - dirFile - ); - } - - @NonNull - private LocalThreadsBaseDirectory buildLocalThreadsBaseDirectory() { - Uri dirUri = null; - if (ChanSettings.localThreadsLocationUri.get().length() > 0) { - dirUri = Uri.parse(ChanSettings.localThreadsLocationUri.get()); - } - - File dirFile = null; - if (ChanSettings.localThreadLocation.get().length() > 0) { - dirFile = new File(ChanSettings.localThreadLocation.get()); - } - - return new LocalThreadsBaseDirectory( - dirUri, - dirFile - ); - } } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/image/ImageLoaderV2.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/image/ImageLoaderV2.java index 22c46ffbe0..b350f4e2ca 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/image/ImageLoaderV2.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/image/ImageLoaderV2.java @@ -16,11 +16,13 @@ import com.github.adamantcheese.chan.utils.StringUtils; import com.github.k1rakishou.fsaf.FileManager; import com.github.k1rakishou.fsaf.file.AbstractFile; -import com.github.k1rakishou.fsaf.file.DirectorySegment; import com.github.k1rakishou.fsaf.file.FileSegment; +import com.github.k1rakishou.fsaf.file.Segment; import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.Executors; @@ -130,15 +132,16 @@ public ImageContainer getFromDisk( "fileManager.newLocalThreadFile() returned null"); } - String imageDir; + List segments = new ArrayList<>(); + if (isSpoiler) { - imageDir = ThreadSaveManager.getBoardSubDir(loadable); + segments.addAll(ThreadSaveManager.getBoardSubDir(loadable)); } else { - imageDir = ThreadSaveManager.getImagesSubDir(loadable); + segments.addAll(ThreadSaveManager.getImagesSubDir(loadable)); } - AbstractFile imageOnDiskFile = baseDirFile - .clone(new DirectorySegment(imageDir), new FileSegment(filename)); + segments.add(new FileSegment(filename)); + AbstractFile imageOnDiskFile = baseDirFile.clone(segments); boolean exists = fileManager.exists(imageOnDiskFile); boolean isFile = fileManager.isFile(imageOnDiskFile); @@ -175,7 +178,9 @@ public ImageContainer getFromDisk( mainThreadHandler.post(() -> { container.setBitmap(bitmap); - container.setRequestUrl(imageDir); + + // TODO: may not work + container.setRequestUrl(imageOnDiskFile.getFullPath()); if (container.getListener() != null) { container.getListener().onResponse(container, true); diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/SavedThreadLoaderManager.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/SavedThreadLoaderManager.kt index 09a36ad76c..47c62c25ed 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/SavedThreadLoaderManager.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/SavedThreadLoaderManager.kt @@ -23,7 +23,6 @@ class SavedThreadLoaderManager @Inject constructor( throw RuntimeException("Cannot be executed on the main thread!") } - val threadSubDir = ThreadSaveManager.getThreadSubDir(loadable) val baseDir = fileManager.newBaseDirectoryFile(LocalThreadsBaseDirectory::class.java) if (baseDir == null) { Logger.e(TAG, "loadSavedThread() fileManager.newLocalThreadFile() returned null") @@ -31,7 +30,7 @@ class SavedThreadLoaderManager @Inject constructor( } val threadSaveDir = baseDir - .clone(DirectorySegment(threadSubDir)) + .clone(ThreadSaveManager.getThreadSubDir(loadable)) val threadSaveDirExists = fileManager.exists(threadSaveDir) val threadSaveDirIsDirectory = fileManager.isDirectory(threadSaveDir) diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/ThreadSaveManager.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/ThreadSaveManager.java index b775bbc530..0c69e255d8 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/ThreadSaveManager.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/ThreadSaveManager.java @@ -23,12 +23,13 @@ import com.github.k1rakishou.fsaf.file.AbstractFile; import com.github.k1rakishou.fsaf.file.DirectorySegment; import com.github.k1rakishou.fsaf.file.FileSegment; +import com.github.k1rakishou.fsaf.file.Segment; -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; @@ -58,6 +59,7 @@ public class ThreadSaveManager { private static final String TAG = "ThreadSaveManager"; private static final int OKHTTP_TIMEOUT_SECONDS = 30; + private static final int REQUEST_BUFFERING_TIME_SECONDS = 30; private static final int MAX_RETRY_ATTEMPTS = 3; private static final boolean VERBOSE_LOG = false; @@ -125,53 +127,103 @@ private void initRxWorkerQueue() { // Just buffer everything in the internal queue when the consumers are slow (and they are // always slow because they have to download images, but we check whether a download request // is already enqueued so it's okay for us to rely on the buffering) - workerQueue.onBackpressureBuffer().concatMapSingle((loadable) -> { - SaveThreadParameters parameters; - List postsToSave = new ArrayList<>(); - - synchronized (activeDownloads) { - Logger.d(TAG, "New downloading request started " + loadableToString(loadable) - + ", activeDownloads count = " + activeDownloads.size()); - parameters = activeDownloads.get(loadable); - - if (parameters != null) { - // Use a copy of the list to avoid ConcurrentModificationExceptions - postsToSave.addAll(parameters.postsToSave); - } - } + workerQueue + .onBackpressureBuffer() + // Collect all the request over some time + .buffer(REQUEST_BUFFERING_TIME_SECONDS, TimeUnit.SECONDS) + .concatMap(this::processCollectedRequests) + .subscribe( + (res) -> { + // OK + }, + (error) -> Logger.e(TAG, + "Uncaught exception!!! workerQueue is in error state now!!! " + + "This should not happen!!!", error), + () -> Logger.e(TAG, + "workerQueue stream has completed!!! This should not happen!!!")); + } - if (parameters == null) { - Logger.e(TAG, "Could not find download parameters for loadable " - + loadableToString(loadable)); - return Single.just(false); - } + private Flowable processCollectedRequests(List loadableList) { + if (loadableList.isEmpty()) { + return Flowable.just(true); + } - return saveThreadInternal(loadable, postsToSave) - // Use the executor's thread to process the queue elements. Everything above - // will executed on this executor's threads. - .subscribeOn(Schedulers.from(executorService)) - // Everything below will be executed on the main thread - .observeOn(AndroidSchedulers.mainThread()) - // Handle errors - .doOnError((error) -> onDownloadingError(error, loadable)) - // Handle results - .doOnSuccess((result) -> onDownloadingCompleted(result, loadable)) - .doOnEvent((result, error) -> { - synchronized (activeDownloads) { - Logger.d(TAG, "Downloading request has completed for loadable " - + loadableToString(loadable) + - ", activeDownloads count = " - + activeDownloads.size()); + Logger.d(TAG, "Collected " + loadableList.size() + " local thread download requests"); + + AbstractFile baseLocalThreadsDirectory = fileManager.newBaseDirectoryFile( + LocalThreadsBaseDirectory.class + ); + + if (baseLocalThreadsDirectory == null) { + Logger.e(TAG, "LocalThreadsBaseDirectory is not registered!"); + return Flowable.just(false); + } + + /** + * Create an in-memory snapshot of a directory with files and sub directories with their + * files. This will SIGNIFICANTLY improve the files operations speed until this snapshot is + * released. For this reason we collect the request so that we can create a snapshot process + * all of the collected request in one big batch and then release the snapshot. + * */ + Logger.d(TAG, "Snapshot created"); + fileManager.createSnapshot(baseLocalThreadsDirectory, true); + + return Flowable.fromIterable(loadableList) + .concatMap((loadable) -> { + SaveThreadParameters parameters; + List postsToSave = new ArrayList<>(); + + synchronized (activeDownloads) { + Logger.d(TAG, "New downloading request started " + loadableToString(loadable) + + ", activeDownloads count = " + activeDownloads.size()); + parameters = activeDownloads.get(loadable); + + if (parameters != null) { + // Use a copy of the list to avoid ConcurrentModificationExceptions + postsToSave.addAll(parameters.postsToSave); } - }) - // Suppress all of the exceptions so that the stream does not complete - .onErrorReturnItem(false); - }).subscribe( - (res) -> { - }, - (error) -> Logger.e(TAG, "Uncaught exception!!! workerQueue is in error state now!!! " + - "This should not happen!!!", error), - () -> Logger.e(TAG, "workerQueue stream has completed!!! This should not happen!!!")); + } + + if (parameters == null) { + Logger.e(TAG, "Could not find download parameters for loadable " + + loadableToString(loadable)); + return Flowable.just(false); + } + + return saveThreadInternal(loadable, postsToSave) + // Use the executor's thread to process the queue elements. Everything above + // will executed on this executor's threads. + .subscribeOn(Schedulers.from(executorService)) + // Everything below will be executed on the main thread + .observeOn(AndroidSchedulers.mainThread()) + // Handle errors + .doOnError((error) -> onDownloadingError(error, loadable)) + // Handle results + .doOnSuccess((result) -> onDownloadingCompleted(result, loadable)) + .doOnEvent((result, error) -> { + synchronized (activeDownloads) { + Logger.d(TAG, "Downloading request has completed for loadable " + + loadableToString(loadable) + + ", activeDownloads count = " + + activeDownloads.size()); + } + }) + // Suppress all of the exceptions so that the stream does not complete + .onErrorReturnItem(false) + .toFlowable(); + }) + .doOnTerminate(() -> { + /** + * Release the snapshot. It is important to do this. Otherwise the cached files + * will stay in memory and if one of the files will get deleted from the disk + * by the user the snapshot will become dirty meaning that it doesn't contain + * the actual information of the directory. And this may lead to unexpected bugs. + * + * !!! Always release snapshots when you have executed all of your file operations. !!! + * */ + Logger.d(TAG, "Snapshot released"); + fileManager.releaseSnapshot(baseLocalThreadsDirectory); + }); } /** @@ -439,46 +491,46 @@ private void downloadInternal( boardSaveDir, spoilerImageUrl) ) - .flatMap((res) -> { - // For each post create a new inner rx stream (so they can be processed in parallel) - return Flowable.fromIterable(newPosts) - // Here we create a separate reactive stream for each image request. - // But we use an executor service with limited threads amount, so there - // will be only this much at a time. - // | - // / | \ - // / | \ - // / | \ - // V V V // Separate streams. - // | | | - // o o o // Download images in parallel. - // | | | - // V V V // Combine them back to a single stream. - // \ | / - // \ | / - // \ | / - // | - .flatMap((post) -> { - return downloadImages( - loadable, - threadSaveDirImages, - post, - currentImageDownloadIndex, - postsWithImages, - imageDownloadsWithIoError, - maxImageIoErrors); - }) - .toList() - .doOnSuccess((list) -> Logger.d(TAG, "PostImage download result list = " + list)); - }) - .flatMap((res) -> { - return Single.defer(() -> { - return tryUpdateLastSavedPostNo(loadable, newPosts); - }); - }) - // Have to use blockingGet here. This is a place where all of the exception will come - // out from - .blockingGet(); + .flatMap((res) -> { + // For each post create a new inner rx stream (so they can be processed in parallel) + return Flowable.fromIterable(newPosts) + // Here we create a separate reactive stream for each image request. + // But we use an executor service with limited threads amount, so there + // will be only this much at a time. + // | + // / | \ + // / | \ + // / | \ + // V V V // Separate streams. + // | | | + // o o o // Download images in parallel. + // | | | + // V V V // Combine them back to a single stream. + // \ | / + // \ | / + // \ | / + // | + .flatMap((post) -> { + return downloadImages( + loadable, + threadSaveDirImages, + post, + currentImageDownloadIndex, + postsWithImages, + imageDownloadsWithIoError, + maxImageIoErrors); + }) + .toList() + .doOnSuccess((list) -> Logger.d(TAG, "PostImage download result list = " + list)); + }) + .flatMap((res) -> { + return Single.defer(() -> { + return tryUpdateLastSavedPostNo(loadable, newPosts); + }); + }) + // Have to use blockingGet here. This is a place where all of the exception will come + // out from + .blockingGet(); } private Single tryUpdateLastSavedPostNo(@NonNull Loadable loadable, List newPosts) { @@ -511,7 +563,7 @@ private AbstractFile getBoardSaveDir(Loadable loadable) throws IOException { } return baseDir - .clone(new DirectorySegment(getBoardSubDir(loadable))); + .clone(getBoardSubDir(loadable)); } private AbstractFile getThreadSaveDir(Loadable loadable) throws IOException { @@ -525,7 +577,7 @@ private AbstractFile getThreadSaveDir(Loadable loadable) throws IOException { } return baseDir - .clone(new DirectorySegment(getThreadSubDir(loadable))); + .clone(getThreadSubDir(loadable)); } private void dealWithMediaScanner(AbstractFile threadSaveDirImages) throws CouldNotCreateNoMediaFile { @@ -586,7 +638,6 @@ private int calculateAmountOfPostsWithImages(List newPosts) { * If a post has at least one image that has not been downloaded yet it will be * redownloaded again */ - // FIXME: VERY SLOW!!! private List filterAndSortPosts( AbstractFile threadSaveDirImages, Loadable loadable, @@ -639,7 +690,8 @@ private List filterAndSortPosts( String loadableString = loadableToString(loadable); Logger.d(TAG, "filterAndSortPosts() completed in " - + delta + "ms for loadable " + loadableString); + + delta + "ms for loadable " + loadableString + + " with " + inputPosts.size() + " posts"); } } @@ -669,8 +721,9 @@ private boolean checkWhetherAllPostImagesAreAlreadySaved( long length = fileManager.getLength(originalImage); if (length == -1L) { - throw new IllegalStateException("originalImage.getLength() returned -1, " + + Logger.e(TAG, "originalImage.getLength() returned -1, " + "originalImagePath = " + originalImage.getFullPath()); + return false; } if (length == 0L) { @@ -705,8 +758,9 @@ private boolean checkWhetherAllPostImagesAreAlreadySaved( long length = fileManager.getLength(thumbnailImage); if (length == -1L) { - throw new IllegalStateException("thumbnailImage.getLength() returned -1, " + + Logger.e(TAG, "thumbnailImage.getLength() returned -1, " + "thumbnailImagePath = " + thumbnailImage.getFullPath()); + return false; } if (length == 0L) { @@ -1135,14 +1189,20 @@ private boolean isFatalException(Throwable error) { * When user cancels a download we need to delete the thread from the disk as well */ private void deleteThreadFilesFromDisk(Loadable loadable) { - String subDirs = getThreadSubDir(loadable); + AbstractFile baseDirectory = fileManager.newBaseDirectoryFile( + LocalThreadsBaseDirectory.class + ); + + if (baseDirectory == null) { + throw new IllegalStateException("LocalThreadsBaseDirectory is not registered!"); + } - File threadSaveDir = new File(ChanSettings.saveLocation.get(), subDirs); - if (!threadSaveDir.exists()) { + AbstractFile threadSaveDir = baseDirectory.clone(getThreadSubDir(loadable)); + if (!fileManager.exists(threadSaveDir)) { return; } - IOUtils.deleteDirWithContents(threadSaveDir); + fileManager.delete(threadSaveDir); } /** @@ -1165,28 +1225,30 @@ public static String formatSpoilerImageName(String extension) { return SPOILER_FILE_NAME + "." + extension; } - public static String getThreadSubDir(Loadable loadable) { + public static List getThreadSubDir(Loadable loadable) { // 4chan/g/11223344 - return loadable.site.name() + - File.separator + - loadable.boardCode + - File.separator + loadable.no; + return Arrays.asList( + new DirectorySegment(loadable.site.name()), + new DirectorySegment(loadable.boardCode), + new DirectorySegment(String.valueOf(loadable.no))); } - public static String getImagesSubDir(Loadable loadable) { + public static List getImagesSubDir(Loadable loadable) { // 4chan/g/11223344/images - return loadable.site.name() + - File.separator + - loadable.boardCode + - File.separator + loadable.no + - File.separator + IMAGES_DIR_NAME; + return Arrays.asList( + new DirectorySegment(loadable.site.name()), + new DirectorySegment(loadable.boardCode), + new DirectorySegment(String.valueOf(loadable.no)), + new DirectorySegment(IMAGES_DIR_NAME) + ); } - public static String getBoardSubDir(Loadable loadable) { + public static List getBoardSubDir(Loadable loadable) { // 4chan/g - return loadable.site.name() + - File.separator + - loadable.boardCode; + return Arrays.asList( + new DirectorySegment(loadable.site.name()), + new DirectorySegment(loadable.boardCode) + ); } /** diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/MediaSettingsControllerPresenter.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/MediaSettingsControllerPresenter.kt new file mode 100644 index 0000000000..1ca847bed3 --- /dev/null +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/MediaSettingsControllerPresenter.kt @@ -0,0 +1,303 @@ +package com.github.adamantcheese.chan.core.presenter + +import android.net.Uri +import android.widget.Toast +import com.github.adamantcheese.chan.core.settings.ChanSettings +import com.github.adamantcheese.chan.ui.settings.base_directory.FilesBaseDirectory +import com.github.adamantcheese.chan.ui.settings.base_directory.LocalThreadsBaseDirectory +import com.github.adamantcheese.chan.utils.AndroidUtils.runOnUiThread +import com.github.adamantcheese.chan.utils.Logger +import com.github.k1rakishou.fsaf.FileChooser +import com.github.k1rakishou.fsaf.FileManager +import com.github.k1rakishou.fsaf.callback.DirectoryChooserCallback +import com.github.k1rakishou.fsaf.file.AbstractFile +import java.util.* +import java.util.concurrent.Executors + +class MediaSettingsControllerPresenter( + private val fileManager: FileManager, + private val fileChooser: FileChooser, + private var callbacks: MediaSettingsControllerCallbacks? +) { + private val fileCopyingExecutor = Executors.newSingleThreadExecutor() + + fun onDestroy() { + callbacks = null + fileCopyingExecutor.shutdown() + } + + /** + * Select a directory where local threads will be stored via the SAF + */ + fun onLocalThreadsLocationUseSAFClicked() { + fileChooser.openChooseDirectoryDialog(object : DirectoryChooserCallback() { + override fun onResult(uri: Uri) { + val oldLocalThreadsDirectory = fileManager.newBaseDirectoryFile( + LocalThreadsBaseDirectory::class.java + ) + + if (oldLocalThreadsDirectory == null) { + withCallbacks { + // TODO: string + showToast("Old local threads base directory is " + + "probably not registered (newBaseDirectoryFile returned null)") + } + + return + } + + ChanSettings.localThreadsLocationUri.set(uri.toString()) + val defaultDir = ChanSettings.getDefaultLocalThreadsLocation() + + ChanSettings.localThreadLocation.setNoUpdate(defaultDir) + + withCallbacks { + // TODO LocalThreadsLocation.setDescription() + updateLocalThreadsLocation(uri.toString()) + } + + val newLocalThreadsDirectory = fileManager.newBaseDirectoryFile( + LocalThreadsBaseDirectory::class.java + ) + + if (newLocalThreadsDirectory == null) { + withCallbacks { + // TODO: strings + showToast("New local threads base directory is probably not registered") + } + + return + } + + withCallbacks { + askUserIfTheyWantToMoveOldThreadsToTheNewDirectory( + oldLocalThreadsDirectory, + newLocalThreadsDirectory + ) + } + } + + override fun onCancel(reason: String) { + withCallbacks { + showToast(reason, Toast.LENGTH_LONG) + } + } + }) + } + + fun onLocalThreadsLocationChosen(dirPath: String) { + val oldLocalThreadsDirectory = fileManager.newBaseDirectoryFile( + LocalThreadsBaseDirectory::class.java + ) + + if (oldLocalThreadsDirectory == null) { + withCallbacks { + // TODO: String + showToast("Old local threads base directory is " + + "probably not registered (newBaseDirectoryFile returned null)") + } + + return + } + + Logger.d(TAG, "SaveLocationController with LocalThreadsSaveLocation mode returned dir $dirPath") + + // Supa hack to get the callback called + ChanSettings.localThreadLocation.setSync("") + ChanSettings.localThreadLocation.setSync(dirPath) + + val newLocalThreadsDirectory = fileManager.newBaseDirectoryFile( + LocalThreadsBaseDirectory::class.java + ) + + if (newLocalThreadsDirectory == null) { + withCallbacks { + // TODO: strings + showToast("New local threads base directory is probably not registered") + } + + return + } + + withCallbacks { + askUserIfTheyWantToMoveOldThreadsToTheNewDirectory( + oldLocalThreadsDirectory, + newLocalThreadsDirectory + ) + } + } + + fun onSaveLocationChosen(dirPath: String) { + val oldSaveFilesDirectory = fileManager.newBaseDirectoryFile( + FilesBaseDirectory::class.java + ) + + if (oldSaveFilesDirectory == null) { + withCallbacks { + // TODO: String + showToast("Old save files base directory is " + + "probably not registered (newBaseDirectoryFile returned null)") + } + + return + } + + Logger.d(TAG, "SaveLocationController with ImageSaveLocation mode returned dir $dirPath") + + // Supa hack to get the callback called + ChanSettings.saveLocation.setSync("") + ChanSettings.saveLocation.setSync(dirPath) + + val newSaveFilesDirectory = fileManager.newBaseDirectoryFile( + FilesBaseDirectory::class.java + ) + + if (newSaveFilesDirectory == null) { + withCallbacks { + // TODO: strings + showToast("New save files base directory is probably not registered") + } + + return + } + + withCallbacks { + askUserIfTheyWantToMoveOldThreadsToTheNewDirectory( + oldSaveFilesDirectory, + newSaveFilesDirectory + ) + } + } + + /** + * Select a directory where saved images will be stored via the SAF + */ + fun onSaveLocationUseSAFClicked() { + fileChooser.openChooseDirectoryDialog(object : DirectoryChooserCallback() { + override fun onResult(uri: Uri) { + ChanSettings.saveLocationUri.set(uri.toString()) + + val defaultDir = ChanSettings.getDefaultSaveLocationDir() + ChanSettings.saveLocation.setNoUpdate(defaultDir) + + withCallbacks { + updateSaveLocationViewText(uri.toString()) + } + } + + override fun onCancel(reason: String) { + withCallbacks { + showToast(reason, Toast.LENGTH_LONG) + } + } + }) + } + + + fun moveOldFilesToTheNewDirectory( + oldBaseDirectory: AbstractFile?, + newBaseDirectory: AbstractFile? + ) { + if (oldBaseDirectory == null || newBaseDirectory == null) { + Logger.e(TAG, "One of the directories is null, cannot copy " + + "(oldBaseDirectory is null == " + (oldBaseDirectory == null) + ")" + + ", newLocalThreadsDirectory is null == " + (newBaseDirectory == null) + ")") + return + } + + Logger.d(TAG, + "oldLocalThreadsDirectory = " + oldBaseDirectory.getFullPath() + + ", newLocalThreadsDirectory = " + newBaseDirectory.getFullPath() + ) + + val filesCount = fileManager.listFiles(oldBaseDirectory).size + if (filesCount == 0) { + withCallbacks { + // TODO: strings + showToast("No files to copy") + } + + return + } + + withCallbacks { + showCopyFilesDialog(oldBaseDirectory, newBaseDirectory) + } + } + + fun moveFilesInternal( + oldBaseDirectory: AbstractFile, + newBaseDirectory: AbstractFile + ) { + fileCopyingExecutor.execute { + val result = fileManager.copyDirectoryWithContent( + oldBaseDirectory, + newBaseDirectory, + false + ) { fileIndex, totalFilesCount -> + if (callbacks == null) { + // User left the MediaSettings screen, we need to cancel the file copying + return@copyDirectoryWithContent true + } + + withCallbacks { + // TODO: strings + val text = String.format( + Locale.US, + // TODO: strings + "Copying file %d out of %d", + fileIndex, + totalFilesCount) + + updateLoadingViewText(text) + } + + return@copyDirectoryWithContent false + } + + withCallbacks { + onCopyDirectoryEnded( + oldBaseDirectory, + newBaseDirectory, + result + ) + } + } + } + + private fun withCallbacks(func: MediaSettingsControllerCallbacks.() -> Unit) { + callbacks?.let { + runOnUiThread { + func(it) + } + } + } + + interface MediaSettingsControllerCallbacks { + fun showToast(message: String, length: Int = Toast.LENGTH_SHORT) + fun updateLocalThreadsLocation(newLocation: String) + + fun askUserIfTheyWantToMoveOldThreadsToTheNewDirectory( + oldBaseDirectory: AbstractFile, + newBaseDirectory: AbstractFile + ) + + fun updateLoadingViewText(newLocation: String) + fun updateSaveLocationViewText(newLocation: String) + + fun showCopyFilesDialog( + oldBaseDirectory: AbstractFile, + newBaseDirectory: AbstractFile + ) + + fun onCopyDirectoryEnded( + oldBaseDirectory: AbstractFile, + newBaseDirectory: AbstractFile, + result: Boolean + ) + } + + companion object { + private const val TAG = "MediaSettingsControllerPresenter" + } +} \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ExperimentalSettingsController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ExperimentalSettingsController.java index a2f6718a2c..d04d6adfa2 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ExperimentalSettingsController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ExperimentalSettingsController.java @@ -92,12 +92,11 @@ private void cancelAllDownloads() { } databaseManager.getDatabasePinManager().updatePins(downloadPins).call(); - boolean usesSAF = ChanSettings.isLocalThreadsDirUsesSAF(); for (Pin pin : downloadPins) { databaseManager.getDatabaseSavedThreadManager().deleteThreadFromDisk( - pin.loadable, - usesSAF); + pin.loadable + ); } return null; diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java index d564d61634..49c88cb830 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java @@ -24,6 +24,7 @@ import androidx.annotation.NonNull; import com.github.adamantcheese.chan.R; +import com.github.adamantcheese.chan.core.presenter.MediaSettingsControllerPresenter; import com.github.adamantcheese.chan.core.settings.ChanSettings; import com.github.adamantcheese.chan.ui.settings.BooleanSettingView; import com.github.adamantcheese.chan.ui.settings.LinkSettingView; @@ -31,34 +32,29 @@ import com.github.adamantcheese.chan.ui.settings.SettingView; import com.github.adamantcheese.chan.ui.settings.SettingsController; import com.github.adamantcheese.chan.ui.settings.SettingsGroup; -import com.github.adamantcheese.chan.ui.settings.base_directory.LocalThreadsBaseDirectory; +import com.github.adamantcheese.chan.utils.BackgroundUtils; import com.github.adamantcheese.chan.utils.Logger; import com.github.k1rakishou.fsaf.FileChooser; import com.github.k1rakishou.fsaf.FileManager; -import com.github.k1rakishou.fsaf.callback.DirectoryChooserCallback; import com.github.k1rakishou.fsaf.document_file.CachingDocumentFile; import com.github.k1rakishou.fsaf.file.AbstractFile; +import com.github.k1rakishou.fsaf.file.ExternalFile; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; -import java.util.Locale; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; import javax.inject.Inject; -import kotlin.Unit; - import static com.github.adamantcheese.chan.Chan.inject; import static com.github.adamantcheese.chan.utils.AndroidUtils.getString; -import static com.github.adamantcheese.chan.utils.AndroidUtils.runOnUiThread; -public class MediaSettingsController extends SettingsController { +public class MediaSettingsController + extends SettingsController + implements MediaSettingsControllerPresenter.MediaSettingsControllerCallbacks { private static final String TAG = "MediaSettingsController"; // Special setting views @@ -69,7 +65,9 @@ public class MediaSettingsController extends SettingsController { private ListSettingView imageAutoLoadView; private ListSettingView videoAutoLoadView; - private Executor fileCopyingExecutor = Executors.newSingleThreadExecutor(); + private LoadingViewController loadingViewController; + private MediaSettingsControllerPresenter presenter; + @Inject FileManager fileManager; @@ -86,9 +84,10 @@ public void onCreate() { inject(this); EventBus.getDefault().register(this); - navigation.setTitle(R.string.settings_screen_media); + presenter = new MediaSettingsControllerPresenter(fileManager, fileChooser, this); + setupLayout(); populatePreferences(); buildPreferences(); @@ -102,6 +101,7 @@ public void onCreate() { public void onDestroy() { super.onDestroy(); + presenter.onDestroy(); EventBus.getDefault().unregister(this); } @@ -209,17 +209,24 @@ private void populatePreferences() { } } + /** + * ============================================== + * Setup Local Threads location + * ============================================== + * */ + private void setupLocalThreadLocationSetting(SettingsGroup media) { if (!ChanSettings.incrementalThreadDownloadingEnabled.get()) { - Logger.d(TAG, "setupLocalThreadLocationSetting() incrementalThreadDownloadingEnabled is disabled"); + Logger.d(TAG, "setupLocalThreadLocationSetting() " + + "incrementalThreadDownloadingEnabled is disabled"); return; } LinkSettingView localThreadsLocationSetting = new LinkSettingView(this, R.string.media_settings_local_threads_location_title, 0, - v -> showUseSAFOrOldAPIForLocalThreadsLocationDialog()); - + v -> showUseSAFOrOldAPIForLocalThreadsLocationDialog() + ); localThreadsLocation = (LinkSettingView) media.add(localThreadsLocationSetting); localThreadsLocation.setDescription(getLocalThreadsLocation()); @@ -233,6 +240,42 @@ private String getLocalThreadsLocation() { return ChanSettings.localThreadLocation.get(); } + private void showUseSAFOrOldAPIForLocalThreadsLocationDialog() { + AlertDialog alertDialog = new AlertDialog.Builder(context) + .setTitle(R.string.use_saf_for_local_threads_location_dialog_title) + .setMessage(R.string.use_saf_for_local_threads_location_dialog_message) + .setPositiveButton(R.string.use_saf_dialog_positive_button_text, (dialog, which) -> { + presenter.onLocalThreadsLocationUseSAFClicked(); + }) + .setNegativeButton(R.string.use_saf_dialog_negative_button_text, (dialog, which) -> { + onLocalThreadsLocationUseOldApiClicked(); + dialog.dismiss(); + }) + .create(); + + alertDialog.show(); + } + + /** + * Select a directory where local threads will be stored via the old Java File API + */ + private void onLocalThreadsLocationUseOldApiClicked() { + SaveLocationController saveLocationController = new SaveLocationController( + context, + SaveLocationController.SaveLocationControllerMode.LocalThreadsSaveLocation, + dirPath -> { + presenter.onLocalThreadsLocationChosen(dirPath); + }); + + navigationController.pushController(saveLocationController); + } + + /** + * ============================================== + * Setup Save Files location + * ============================================== + * */ + private void setupSaveLocationSetting(SettingsGroup media) { LinkSettingView chooseSaveLocationSetting = new LinkSettingView(this, R.string.save_location_screen, @@ -251,15 +294,15 @@ private String getSaveLocation() { return ChanSettings.saveLocation.get(); } - private void showUseSAFOrOldAPIForLocalThreadsLocationDialog() { + private void showUseSAFOrOldAPIForSaveLocationDialog() { AlertDialog alertDialog = new AlertDialog.Builder(context) - .setTitle(R.string.use_saf_for_local_threads_location_dialog_title) - .setMessage(R.string.use_saf_for_local_threads_location_dialog_message) + .setTitle(R.string.use_saf_for_save_location_dialog_title) + .setMessage(R.string.use_saf_for_save_location_dialog_message) .setPositiveButton(R.string.use_saf_dialog_positive_button_text, (dialog, which) -> { - onLocalThreadsLocationUseSAFClicked(); + presenter.onSaveLocationUseSAFClicked(); }) .setNegativeButton(R.string.use_saf_dialog_negative_button_text, (dialog, which) -> { - onLocalThreadsLocationUseOldApiClicked(); + onSaveLocationUseOldApiClicked(); dialog.dismiss(); }) .create(); @@ -268,81 +311,40 @@ private void showUseSAFOrOldAPIForLocalThreadsLocationDialog() { } /** - * Select a directory where local threads will be stored via the old Java File API + * Select a directory where saved images will be stored via the old Java File API */ - private void onLocalThreadsLocationUseOldApiClicked() { + private void onSaveLocationUseOldApiClicked() { SaveLocationController saveLocationController = new SaveLocationController( context, - SaveLocationController.SaveLocationControllerMode.LocalThreadsSaveLocation, + SaveLocationController.SaveLocationControllerMode.ImageSaveLocation, dirPath -> { - AbstractFile oldLocalThreadsDirectory = fileManager.newBaseDirectoryFile( - LocalThreadsBaseDirectory.class - ); - - Logger.d(TAG, "SaveLocationController with LocalThreadsSaveLocation mode returned dir " - + dirPath); - - // Supa hack to get the callback called - ChanSettings.localThreadLocation.setSync(""); - ChanSettings.localThreadLocation.setSync(dirPath); - - AbstractFile newLocalThreadsDirectory = fileManager.newBaseDirectoryFile( - LocalThreadsBaseDirectory.class - ); - - askUserIfTheyWantToMoveOldThreadsToTheNewDirectory( - oldLocalThreadsDirectory, - newLocalThreadsDirectory); + presenter.onSaveLocationChosen(dirPath); }); navigationController.pushController(saveLocationController); } /** - * Select a directory where local threads will be stored via the SAF - */ - private void onLocalThreadsLocationUseSAFClicked() { - fileChooser.openChooseDirectoryDialog(new DirectoryChooserCallback() { - @Override - public void onResult(@NotNull Uri uri) { - // TODO: check that there are no files in the directory and warn the user that something - // might go wrong in this case - AbstractFile oldLocalThreadsDirectory = fileManager.newBaseDirectoryFile( - LocalThreadsBaseDirectory.class - ); - - ChanSettings.localThreadsLocationUri.set(uri.toString()); - String defaultDir = ChanSettings.getDefaultLocalThreadsLocation(); - - ChanSettings.localThreadLocation.setNoUpdate(defaultDir); - localThreadsLocation.setDescription(uri.toString()); - - AbstractFile newLocalThreadsDirectory = fileManager.newBaseDirectoryFile( - LocalThreadsBaseDirectory.class - ); - - askUserIfTheyWantToMoveOldThreadsToTheNewDirectory( - oldLocalThreadsDirectory, - newLocalThreadsDirectory); - } - - @Override - public void onCancel(@NotNull String reason) { - Toast.makeText(context, reason, Toast.LENGTH_LONG).show(); - } - }); - } + * ============================================== + * Presenter callbacks + * ============================================== + * */ - private void askUserIfTheyWantToMoveOldThreadsToTheNewDirectory( - AbstractFile oldLocalThreadsDirectory, - AbstractFile newLocalThreadsDirectory) { + @Override + public void askUserIfTheyWantToMoveOldThreadsToTheNewDirectory( + @NonNull AbstractFile oldBaseDirectory, + @NonNull AbstractFile newBaseDirectory + ) { // TODO: strings AlertDialog alertDialog = new AlertDialog.Builder(context) .setTitle("Move old local threads to the new directory?") - .setMessage("This operation may take quite some time. Once started this operation shouldn't be canceled, otherwise something may break") + .setMessage("This operation may take quite some time. Once started this operation shouldn't be canceled") .setPositiveButton("Move", (dialog, which) -> { - moveOldFilesToTheNewDirectory(oldLocalThreadsDirectory, newLocalThreadsDirectory); + presenter.moveOldFilesToTheNewDirectory( + oldBaseDirectory, + newBaseDirectory + ); }) .setNegativeButton("Do not", (dialog, which) -> dialog.dismiss()) .create(); @@ -350,156 +352,110 @@ private void askUserIfTheyWantToMoveOldThreadsToTheNewDirectory( alertDialog.show(); } - private void moveOldFilesToTheNewDirectory( - @Nullable AbstractFile oldLocalThreadsDirectory, - @Nullable AbstractFile newLocalThreadsDirectory) { - if (oldLocalThreadsDirectory == null || newLocalThreadsDirectory == null) { - Logger.e(TAG, "One of the directories is null, cannot copy " + - "(oldLocalThreadsDirectory is null == " + (oldLocalThreadsDirectory == null) + ")" + - ", newLocalThreadsDirectory is null == " + (newLocalThreadsDirectory == null) + ")"); - return; - } + @Override + public void onCopyDirectoryEnded( + @NonNull AbstractFile oldBaseDirectory, + @NonNull AbstractFile newBaseDirectory, + boolean result + ) { + BackgroundUtils.ensureMainThread(); + + navigationController.popController(); + loadingViewController = null; + + if (!result) { + // TODO: strings + showToast("Could not copy one directory's file into another one", Toast.LENGTH_LONG); + } else { + if (oldBaseDirectory instanceof ExternalFile) { + forgetPreviousExternalBaseDirectory(oldBaseDirectory); + } - Logger.d(TAG, "oldLocalThreadsDirectory = " + oldLocalThreadsDirectory.getFullPath() - + ", newLocalThreadsDirectory = " + newLocalThreadsDirectory.getFullPath()); + // TODO: delete old directory dialog - int filesCount = fileManager.listFiles(oldLocalThreadsDirectory).size(); - if (filesCount == 0) { - Toast.makeText(context, "No files to copy", Toast.LENGTH_SHORT).show(); - return; + // TODO: strings + showToast("Successfully copied files", Toast.LENGTH_LONG); } + } - // TODO: Strings - AlertDialog alertDialog = new AlertDialog.Builder(context) - .setTitle("Copy files") - .setMessage("Do you want to copy " + filesCount + " from an old directory to the new one?") - .setPositiveButton("Copy", ((dialog, which) -> { - LoadingViewController loadingViewController = new LoadingViewController( - context, - false); - navigationController.pushController(loadingViewController); + @Override + public void updateLoadingViewText(@NotNull String text) { + BackgroundUtils.ensureMainThread(); - fileCopyingExecutor.execute(() -> { - moveFilesInternal( - oldLocalThreadsDirectory, - newLocalThreadsDirectory, - loadingViewController); - }); - })) - .setNegativeButton("Do not", ((dialog, which) -> dialog.dismiss())) - .create(); + if (loadingViewController != null) { + loadingViewController.updateWithText(text); + } + } - alertDialog.show(); + @Override + public void updateSaveLocationViewText(@NotNull String newLocation) { + BackgroundUtils.ensureMainThread(); + saveLocation.setDescription(newLocation); } - private void moveFilesInternal( - @NonNull AbstractFile oldLocalThreadsDirectory, - @NonNull AbstractFile newLocalThreadsDirectory, - LoadingViewController loadingViewController) { - boolean result = fileManager.copyDirectoryWithContent( - oldLocalThreadsDirectory, - newLocalThreadsDirectory, - false, - (fileIndex, totalFilesCount) -> { - runOnUiThread(() -> { - // TODO: strings - String text = String.format( - Locale.US, - // TODO: strings - "Copying file %d out of %d", - fileIndex, - totalFilesCount); - - loadingViewController.updateWithText(text); - }); - - return Unit.INSTANCE; - }); + @Override + public void showToast(@NotNull String message, int length) { + BackgroundUtils.ensureMainThread(); + Toast.makeText(context, message, length).show(); + } - runOnUiThread(() -> { - navigationController.popController(); - - if (!result) { - // TODO: strings - Toast.makeText( - context, - "Could not copy one directory's file into another one", - Toast.LENGTH_LONG - ).show(); - } else { - Uri safTreeuri = oldLocalThreadsDirectory - .getFileRoot().getHolder().getUri(); - - fileChooser.forgetSAFTree(safTreeuri); - - // TODO: delete old directory dialog - - // TODO: strings - Toast.makeText( - context, - "Successfully copied files", - Toast.LENGTH_LONG - ).show(); - } - }); + @Override + public void updateLocalThreadsLocation(@NotNull String newLocation) { + BackgroundUtils.ensureMainThread(); + localThreadsLocation.setDescription(newLocation); } - private void showUseSAFOrOldAPIForSaveLocationDialog() { + @Override + public void showCopyFilesDialog( + @NotNull AbstractFile oldBaseDirectory, + @NotNull AbstractFile newBaseDirectory + ) { + BackgroundUtils.ensureMainThread(); + + // TODO: strings AlertDialog alertDialog = new AlertDialog.Builder(context) - .setTitle(R.string.use_saf_for_save_location_dialog_title) - .setMessage(R.string.use_saf_for_save_location_dialog_message) - .setPositiveButton(R.string.use_saf_dialog_positive_button_text, (dialog, which) -> { - onSaveLocationUseSAFClicked(); - }) - .setNegativeButton(R.string.use_saf_dialog_negative_button_text, (dialog, which) -> { - onSaveLocationUseOldApiClicked(); - dialog.dismiss(); + .setTitle("Copy files") + .setMessage("Do you want to copy $filesCount from an old directory to the new one?") + .setPositiveButton("Copy", (dialog, which) -> { + if (loadingViewController != null) { + throw new IllegalStateException( + "Previous loadingViewController was not destroyed" + ); + } + + loadingViewController = new LoadingViewController( + context, + false + ); + + navigationController.pushController(loadingViewController); + + presenter.moveFilesInternal( + oldBaseDirectory, + newBaseDirectory + ); }) + .setNegativeButton("Do not", (dialog, which) -> dialog.dismiss()) .create(); alertDialog.show(); } /** - * Select a directory where saved images will be stored via the old Java File API - */ - private void onSaveLocationUseOldApiClicked() { - SaveLocationController saveLocationController = new SaveLocationController( - context, - SaveLocationController.SaveLocationControllerMode.ImageSaveLocation, - dirPath -> { - Logger.d(TAG, "SaveLocationController with ImageSaveLocation mode returned dir " - + dirPath); - - // Supa hack to get the callback called - ChanSettings.saveLocation.setSync(""); - ChanSettings.saveLocation.setSync(dirPath); - }); - - navigationController.pushController(saveLocationController); - } - - /** - * Select a directory where saved images will be stored via the SAF - */ - private void onSaveLocationUseSAFClicked() { - fileChooser.openChooseDirectoryDialog(new DirectoryChooserCallback() { - @Override - public void onResult(@NotNull Uri uri) { - // TODO: check that there are no files in the directory at warn user that something - // might go wrong in this case - ChanSettings.saveLocationUri.set(uri.toString()); - - String defaultDir = ChanSettings.getDefaultSaveLocationDir(); - ChanSettings.saveLocation.setNoUpdate(defaultDir); - saveLocation.setDescription(uri.toString()); + * ============================================== + * Other methods + * ============================================== + * */ + + private void forgetPreviousExternalBaseDirectory(@NonNull AbstractFile oldLocalThreadsDirectory) { + if (oldLocalThreadsDirectory instanceof ExternalFile) { + Uri safTreeuri = oldLocalThreadsDirectory + .getFileRoot().getHolder().uri(); + + if (!fileChooser.forgetSAFTree(safTreeuri)) { + showToast("Couldn't release uri permissions", Toast.LENGTH_SHORT); } - - @Override - public void onCancel(@NotNull String reason) { - Toast.makeText(context, reason, Toast.LENGTH_LONG).show(); - } - }); + } } private void setupMediaLoadTypesSetting(SettingsGroup loading) { diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/settings/base_directory/FilesBaseDirectory.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/settings/base_directory/FilesBaseDirectory.kt index 2d28476e9e..0e6be8ac4d 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/settings/base_directory/FilesBaseDirectory.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/settings/base_directory/FilesBaseDirectory.kt @@ -1,10 +1,30 @@ package com.github.adamantcheese.chan.ui.settings.base_directory import android.net.Uri +import com.github.adamantcheese.chan.BuildConfig +import com.github.adamantcheese.chan.core.settings.ChanSettings import com.github.k1rakishou.fsaf.manager.base_directory.BaseDirectory import java.io.File class FilesBaseDirectory( - dirUri: Uri?, - dirFile: File? -) : BaseDirectory(dirUri, dirFile) \ No newline at end of file +) : BaseDirectory(BuildConfig.DEBUG) { + + override fun getDirFile(): File? { + val saveLocationPath = ChanSettings.saveLocation.get() + if (saveLocationPath.isEmpty()) { + return null + } + + return File(saveLocationPath) + } + + override fun getDirUri(): Uri? { + val saveLocationSafPath = ChanSettings.saveLocationUri.get() + if (saveLocationSafPath.isEmpty()) { + return null + } + + return Uri.parse(saveLocationSafPath) + } + +} \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/settings/base_directory/LocalThreadsBaseDirectory.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/settings/base_directory/LocalThreadsBaseDirectory.kt index fa2092bb91..674f9c6bae 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/settings/base_directory/LocalThreadsBaseDirectory.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/settings/base_directory/LocalThreadsBaseDirectory.kt @@ -1,10 +1,30 @@ package com.github.adamantcheese.chan.ui.settings.base_directory import android.net.Uri +import com.github.adamantcheese.chan.BuildConfig +import com.github.adamantcheese.chan.core.settings.ChanSettings import com.github.k1rakishou.fsaf.manager.base_directory.BaseDirectory import java.io.File class LocalThreadsBaseDirectory( - dirUri: Uri?, - dirFile: File? -) : BaseDirectory(dirUri, dirFile) \ No newline at end of file +) : BaseDirectory(BuildConfig.DEBUG) { + + override fun getDirFile(): File? { + val localThreadsPath = ChanSettings.localThreadLocation.get() + if (localThreadsPath.isEmpty()) { + return null + } + + return File(localThreadsPath) + } + + override fun getDirUri(): Uri? { + val localThreadsSafPath = ChanSettings.localThreadsLocationUri.get() + if (localThreadsSafPath.isEmpty()) { + return null + } + + return Uri.parse(localThreadsSafPath) + } + +} \ No newline at end of file diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/utils/BackgroundUtils.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/utils/BackgroundUtils.java index a4f08cb202..de587253b5 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/utils/BackgroundUtils.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/utils/BackgroundUtils.java @@ -35,6 +35,18 @@ public static boolean isMainThread() { return Thread.currentThread() == Looper.getMainLooper().getThread(); } + public static void ensureMainThread() { + if (!isMainThread()) { + throw new IllegalStateException("Cannot be executed on a background thread!"); + } + } + + public static void ensureBackgroundThread() { + if (isMainThread()) { + throw new IllegalStateException("Cannot be executed on the main thread!"); + } + } + public static Cancelable runWithExecutor(Executor executor, final Callable background, final BackgroundResult result) { final AtomicBoolean cancelled = new AtomicBoolean(false); diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/utils/IOUtils.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/utils/IOUtils.java index a152bae480..782c5a052e 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/utils/IOUtils.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/utils/IOUtils.java @@ -119,17 +119,4 @@ public static void copyFile(File in, File out) throws IOException { IOUtils.closeQuietly(os); } } - - public static void deleteDirWithContents(File dir) { - if (dir.isDirectory()) { - File[] files = dir.listFiles(); - if (files != null) { - for (File c : files) { - deleteDirWithContents(c); - } - } - } - - dir.delete(); - } } diff --git a/Kuroba/app/src/test/java/com/github/adamantcheese/chan/core/ExtensionsKtTest.kt b/Kuroba/app/src/test/java/com/github/adamantcheese/chan/core/ExtensionsKtTest.kt deleted file mode 100644 index 45328fc49f..0000000000 --- a/Kuroba/app/src/test/java/com/github/adamantcheese/chan/core/ExtensionsKtTest.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.github.adamantcheese.chan.core - -import org.junit.Assert.* -import org.junit.Test - -class ExtensionsKtTest { - - @Test - fun testConvertIntToCharArray() { - val int = 0x11223344 - val charArray = int.toCharArray() - - assertEquals(0x11, charArray[0].toInt()) - assertEquals(0x22, charArray[1].toInt()) - assertEquals(0x33, charArray[2].toInt()) - assertEquals(0x44, charArray[3].toInt()) - } - - @Test - fun testCharArrayToInt() { - val charArray = CharArray(4) - charArray[0] = 0x11.toChar() - charArray[1] = 0x22.toChar() - charArray[2] = 0x33.toChar() - charArray[3] = 0x44.toChar() - - assertEquals(0x11223344, charArray.toInt()) - } -} \ No newline at end of file From fcde52d0c3dde57a6e4cb3a6a8d0c73a9dc3f95f Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 20 Oct 2019 17:55:49 +0300 Subject: [PATCH 174/184] (#172) Fix a bug where the downloading thread won't be marked as stopped in the database when stopping it by clicking the "save thread" button --- .../database/DatabaseSavedThreadManager.java | 12 +++++ .../chan/core/manager/ThreadSaveManager.java | 6 +-- .../MediaSettingsControllerPresenter.kt | 16 +++++-- .../chan/core/presenter/ThreadPresenter.java | 11 +++-- .../controller/MediaSettingsController.java | 44 ++++++++++++++----- 5 files changed, 68 insertions(+), 21 deletions(-) diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/database/DatabaseSavedThreadManager.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/database/DatabaseSavedThreadManager.java index 51cca6f2f2..1b2e03bfd9 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/database/DatabaseSavedThreadManager.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/database/DatabaseSavedThreadManager.java @@ -28,6 +28,17 @@ public DatabaseSavedThreadManager() { inject(this); } + public Callable countDownloadingThreads() { + return () -> { + return helper.savedThreadDao.queryBuilder() + .where() + .eq(SavedThread.IS_STOPPED, false) + .and() + .eq(SavedThread.IS_FULLY_DOWNLOADED, false) + .countOf(); + }; + } + public Callable> getSavedThreads() { return () -> { // We don't need fully downloaded threads here @@ -129,6 +140,7 @@ public Callable updateThreadFullyDownloadedByLoadableId(int loadableId) return true; } + savedThread.isStopped = true; savedThread.isFullyDownloaded = true; helper.savedThreadDao.update(savedThread); diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/ThreadSaveManager.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/ThreadSaveManager.java index 0c69e255d8..df65d0b605 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/ThreadSaveManager.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/ThreadSaveManager.java @@ -138,7 +138,7 @@ private void initRxWorkerQueue() { }, (error) -> Logger.e(TAG, "Uncaught exception!!! workerQueue is in error state now!!! " + - "This should not happen!!!", error), + "This should not happen!!!", error), () -> Logger.e(TAG, "workerQueue stream has completed!!! This should not happen!!!")); } @@ -971,9 +971,7 @@ private void logThreadDownloadingProgress( Logger.d(TAG, "Downloading is in progress for an image with loadable " + loadableToString(loadable) + - ", percent = " + percent + - ", total = " + count + - ", current = " + index); + ", " + index + "/" + count + " (" + percent + "%)"); } private void deleteImageCompletely( diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/MediaSettingsControllerPresenter.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/MediaSettingsControllerPresenter.kt index 1ca847bed3..834a8fd61e 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/MediaSettingsControllerPresenter.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/MediaSettingsControllerPresenter.kt @@ -52,7 +52,6 @@ class MediaSettingsControllerPresenter( ChanSettings.localThreadLocation.setNoUpdate(defaultDir) withCallbacks { - // TODO LocalThreadsLocation.setDescription() updateLocalThreadsLocation(uri.toString()) } @@ -193,7 +192,6 @@ class MediaSettingsControllerPresenter( }) } - fun moveOldFilesToTheNewDirectory( oldBaseDirectory: AbstractFile?, newBaseDirectory: AbstractFile? @@ -210,7 +208,16 @@ class MediaSettingsControllerPresenter( + ", newLocalThreadsDirectory = " + newBaseDirectory.getFullPath() ) - val filesCount = fileManager.listFiles(oldBaseDirectory).size + var filesCount = 0 + + fileManager.traverseDirectory( + oldBaseDirectory, + true, + FileManager.TraverseMode.OnlyFiles + ) { + ++filesCount + } + if (filesCount == 0) { withCallbacks { // TODO: strings @@ -221,7 +228,7 @@ class MediaSettingsControllerPresenter( } withCallbacks { - showCopyFilesDialog(oldBaseDirectory, newBaseDirectory) + showCopyFilesDialog(filesCount, oldBaseDirectory, newBaseDirectory) } } @@ -286,6 +293,7 @@ class MediaSettingsControllerPresenter( fun updateSaveLocationViewText(newLocation: String) fun showCopyFilesDialog( + filesCount: Int, oldBaseDirectory: AbstractFile, newBaseDirectory: AbstractFile ) diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/ThreadPresenter.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/ThreadPresenter.java index 41e42725d6..2d8025c3ad 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/ThreadPresenter.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/ThreadPresenter.java @@ -336,12 +336,17 @@ public boolean save() { return startedSaving; } - pin.pinType = PinType.removeDownloadNewPostsFlag(pin.pinType); - if (PinType.hasNoFlags(pin.pinType)) { + if (!PinType.hasWatchNewPostsFlag(pin.pinType)) { + pin.pinType = PinType.removeDownloadNewPostsFlag(pin.pinType); watchManager.deletePin(pin); } else { - watchManager.updatePin(pin); watchManager.stopSavingThread(pin.loadable); + + // Remove the flag after stopping thread saving, otherwise we just won't find the thread + // because the pin won't have the download flag which we check somewhere deep inside the + // stopSavingThread() method + pin.pinType = PinType.removeDownloadNewPostsFlag(pin.pinType); + watchManager.updatePin(pin); } loadable.loadableDownloadingState = Loadable.LoadableDownloadingState.NotDownloading; diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java index 49c88cb830..2f32045354 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java @@ -24,6 +24,7 @@ import androidx.annotation.NonNull; import com.github.adamantcheese.chan.R; +import com.github.adamantcheese.chan.core.database.DatabaseManager; import com.github.adamantcheese.chan.core.presenter.MediaSettingsControllerPresenter; import com.github.adamantcheese.chan.core.settings.ChanSettings; import com.github.adamantcheese.chan.ui.settings.BooleanSettingView; @@ -68,11 +69,12 @@ public class MediaSettingsController private LoadingViewController loadingViewController; private MediaSettingsControllerPresenter presenter; - @Inject FileManager fileManager; @Inject FileChooser fileChooser; + @Inject + DatabaseManager databaseManager; public MediaSettingsController(Context context) { super(context); @@ -232,6 +234,15 @@ private void setupLocalThreadLocationSetting(SettingsGroup media) { localThreadsLocation.setDescription(getLocalThreadsLocation()); } + private void showStopAllDownloadingThreadsDialog(long downloadingThreadsCount) { + new AlertDialog.Builder(context) + .setTitle("There are " + downloadingThreadsCount + " threads being downloaded") + .setMessage("You have to stop all the threads that are being downloaded before changing local threads base directory!") + .setPositiveButton("OK", ((dialog, which) -> dialog.dismiss())) + .create() + .show(); + } + private String getLocalThreadsLocation() { if (!ChanSettings.localThreadsLocationUri.get().isEmpty()) { return ChanSettings.localThreadsLocationUri.get(); @@ -241,6 +252,15 @@ private String getLocalThreadsLocation() { } private void showUseSAFOrOldAPIForLocalThreadsLocationDialog() { + long downloadingThreadsCount = databaseManager.runTask(() -> { + return databaseManager.getDatabaseSavedThreadManager().countDownloadingThreads().call(); + }); + + if (downloadingThreadsCount > 0) { + showStopAllDownloadingThreadsDialog(downloadingThreadsCount); + return; + } + AlertDialog alertDialog = new AlertDialog.Builder(context) .setTitle(R.string.use_saf_for_local_threads_location_dialog_title) .setMessage(R.string.use_saf_for_local_threads_location_dialog_message) @@ -339,7 +359,7 @@ public void askUserIfTheyWantToMoveOldThreadsToTheNewDirectory( // TODO: strings AlertDialog alertDialog = new AlertDialog.Builder(context) .setTitle("Move old local threads to the new directory?") - .setMessage("This operation may take quite some time. Once started this operation shouldn't be canceled") + .setMessage("This operation may take quite some time. Once started this operation must not be canceled.") .setPositiveButton("Move", (dialog, which) -> { presenter.moveOldFilesToTheNewDirectory( oldBaseDirectory, @@ -407,22 +427,24 @@ public void updateLocalThreadsLocation(@NotNull String newLocation) { @Override public void showCopyFilesDialog( + int filesCount, @NotNull AbstractFile oldBaseDirectory, @NotNull AbstractFile newBaseDirectory ) { BackgroundUtils.ensureMainThread(); + if (loadingViewController != null) { + throw new IllegalStateException( + "Previous loadingViewController was not destroyed" + ); + } + // TODO: strings AlertDialog alertDialog = new AlertDialog.Builder(context) .setTitle("Copy files") - .setMessage("Do you want to copy $filesCount from an old directory to the new one?") + .setMessage("Do you want to copy " + filesCount + + " files from old directory to the new one?") .setPositiveButton("Copy", (dialog, which) -> { - if (loadingViewController != null) { - throw new IllegalStateException( - "Previous loadingViewController was not destroyed" - ); - } - loadingViewController = new LoadingViewController( context, false @@ -447,7 +469,9 @@ public void showCopyFilesDialog( * ============================================== * */ - private void forgetPreviousExternalBaseDirectory(@NonNull AbstractFile oldLocalThreadsDirectory) { + private void forgetPreviousExternalBaseDirectory( + @NonNull AbstractFile oldLocalThreadsDirectory + ) { if (oldLocalThreadsDirectory instanceof ExternalFile) { Uri safTreeuri = oldLocalThreadsDirectory .getFileRoot().getHolder().uri(); From 49b1f690c872afc1ad81a162e883519537c441d6 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 26 Oct 2019 21:18:40 +0300 Subject: [PATCH 175/184] (#172) SAF integration is completed Add ability to delete old base directory after moving files into the new one Small fixes for thread album downloading where folder and file names would not be sanitized --- .../adamantcheese/chan/core/di/AppModule.java | 19 +-- .../model/export/ExportedAppSettings.java | 3 +- .../MediaSettingsControllerPresenter.kt | 111 +++++++++++------- .../chan/core/saver/ImageSaver.java | 13 +- .../chan/core/settings/ChanSettings.java | 65 +++++++--- .../chan/core/settings/SettingProvider.java | 2 + .../SharedPreferencesSettingProvider.java | 5 + .../chan/core/settings/StringSetting.java | 12 ++ .../settings/json/JsonSettingsProvider.java | 5 + .../ui/controller/ImageViewerController.java | 17 +-- .../ui/controller/LoadingViewController.java | 22 +++- .../controller/MediaSettingsController.java | 75 ++++++++++-- ...irectory.kt => SavedFilesBaseDirectory.kt} | 2 +- .../adamantcheese/chan/utils/StringUtils.java | 16 +++ .../res/layout/controller_loading_view.xml | 12 +- 15 files changed, 273 insertions(+), 106 deletions(-) rename Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/settings/base_directory/{FilesBaseDirectory.kt => SavedFilesBaseDirectory.kt} (96%) diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/AppModule.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/AppModule.java index 95ec76dc2e..a7eaa31a34 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/AppModule.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/di/AppModule.java @@ -25,14 +25,12 @@ import com.github.adamantcheese.chan.core.net.BitmapLruImageCache; import com.github.adamantcheese.chan.core.saver.ImageSaver; import com.github.adamantcheese.chan.ui.captcha.CaptchaHolder; -import com.github.adamantcheese.chan.ui.settings.base_directory.FilesBaseDirectory; import com.github.adamantcheese.chan.ui.settings.base_directory.LocalThreadsBaseDirectory; +import com.github.adamantcheese.chan.ui.settings.base_directory.SavedFilesBaseDirectory; import com.github.adamantcheese.chan.ui.theme.ThemeHelper; import com.github.adamantcheese.chan.utils.Logger; import com.github.k1rakishou.fsaf.FileChooser; import com.github.k1rakishou.fsaf.FileManager; -import com.github.k1rakishou.fsaf.manager.ExternalFileManager; -import com.github.k1rakishou.fsaf.manager.RawFileManager; import com.github.k1rakishou.fsaf.manager.base_directory.DirectoryManager; import org.codejargon.feather.Provides; @@ -105,21 +103,14 @@ public CaptchaHolder provideCaptchaHolder() { @Singleton public FileManager provideFileManager() { DirectoryManager directoryManager = new DirectoryManager(); - ExternalFileManager externalFileManager = new ExternalFileManager( - applicationContext, - directoryManager - ); - RawFileManager rawFileManager = new RawFileManager(); // Add new base directories here LocalThreadsBaseDirectory localThreadsBaseDirectory = new LocalThreadsBaseDirectory(); - FilesBaseDirectory filesBaseDirectory = new FilesBaseDirectory(); + SavedFilesBaseDirectory savedFilesBaseDirectory = new SavedFilesBaseDirectory(); FileManager fileManager = new FileManager( applicationContext, - directoryManager, - externalFileManager, - rawFileManager + directoryManager ); fileManager.registerBaseDir( @@ -127,8 +118,8 @@ public FileManager provideFileManager() { localThreadsBaseDirectory ); fileManager.registerBaseDir( - FilesBaseDirectory.class, - filesBaseDirectory + SavedFilesBaseDirectory.class, + savedFilesBaseDirectory ); return fileManager; diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/model/export/ExportedAppSettings.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/model/export/ExportedAppSettings.java index e83ee3cdaa..9079840b7f 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/model/export/ExportedAppSettings.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/model/export/ExportedAppSettings.java @@ -50,8 +50,7 @@ public ExportedAppSettings( List exportedFilters, List exportedPostHides, List exportedSavedThreads, - @NonNull - String settings + @NonNull String settings ) { this.exportedSites = exportedSites; this.exportedBoards = exportedBoards; diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/MediaSettingsControllerPresenter.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/MediaSettingsControllerPresenter.kt index 834a8fd61e..a2aa178881 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/MediaSettingsControllerPresenter.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/MediaSettingsControllerPresenter.kt @@ -3,8 +3,8 @@ package com.github.adamantcheese.chan.core.presenter import android.net.Uri import android.widget.Toast import com.github.adamantcheese.chan.core.settings.ChanSettings -import com.github.adamantcheese.chan.ui.settings.base_directory.FilesBaseDirectory import com.github.adamantcheese.chan.ui.settings.base_directory.LocalThreadsBaseDirectory +import com.github.adamantcheese.chan.ui.settings.base_directory.SavedFilesBaseDirectory import com.github.adamantcheese.chan.utils.AndroidUtils.runOnUiThread import com.github.adamantcheese.chan.utils.Logger import com.github.k1rakishou.fsaf.FileChooser @@ -47,9 +47,7 @@ class MediaSettingsControllerPresenter( } ChanSettings.localThreadsLocationUri.set(uri.toString()) - val defaultDir = ChanSettings.getDefaultLocalThreadsLocation() - - ChanSettings.localThreadLocation.setNoUpdate(defaultDir) + ChanSettings.localThreadLocation.setNoUpdate(ChanSettings.getDefaultLocalThreadsLocation()) withCallbacks { updateLocalThreadsLocation(uri.toString()) @@ -100,10 +98,7 @@ class MediaSettingsControllerPresenter( } Logger.d(TAG, "SaveLocationController with LocalThreadsSaveLocation mode returned dir $dirPath") - - // Supa hack to get the callback called - ChanSettings.localThreadLocation.setSync("") - ChanSettings.localThreadLocation.setSync(dirPath) + ChanSettings.localThreadLocation.setSyncNoCheck(dirPath) val newLocalThreadsDirectory = fileManager.newBaseDirectoryFile( LocalThreadsBaseDirectory::class.java @@ -126,9 +121,65 @@ class MediaSettingsControllerPresenter( } } + /** + * Select a directory where saved images will be stored via the SAF + */ + fun onSaveLocationUseSAFClicked() { + fileChooser.openChooseDirectoryDialog(object : DirectoryChooserCallback() { + override fun onResult(uri: Uri) { + val oldSavedFileBaseDirectory = fileManager.newBaseDirectoryFile( + SavedFilesBaseDirectory::class.java + ) + + if (oldSavedFileBaseDirectory == null) { + withCallbacks { + // TODO: string + showToast("Old saved files base directory is " + + "probably not registered (newBaseDirectoryFile returned null)") + } + + return + } + + ChanSettings.saveLocationUri.set(uri.toString()) + ChanSettings.saveLocation.setNoUpdate(ChanSettings.getDefaultSaveLocationDir()) + + withCallbacks { + updateSaveLocationViewText(uri.toString()) + } + + val newSavedFilesBaseDirectory = fileManager.newBaseDirectoryFile( + SavedFilesBaseDirectory::class.java + ) + + if (newSavedFilesBaseDirectory == null) { + withCallbacks { + // TODO: strings + showToast("New saved files base directory is probably not registered") + } + + return + } + + withCallbacks { + askUserIfTheyWantToMoveOldSavedFilesToTheNewDirectory( + oldSavedFileBaseDirectory, + newSavedFilesBaseDirectory + ) + } + } + + override fun onCancel(reason: String) { + withCallbacks { + showToast(reason, Toast.LENGTH_LONG) + } + } + }) + } + fun onSaveLocationChosen(dirPath: String) { val oldSaveFilesDirectory = fileManager.newBaseDirectoryFile( - FilesBaseDirectory::class.java + SavedFilesBaseDirectory::class.java ) if (oldSaveFilesDirectory == null) { @@ -142,13 +193,10 @@ class MediaSettingsControllerPresenter( } Logger.d(TAG, "SaveLocationController with ImageSaveLocation mode returned dir $dirPath") - - // Supa hack to get the callback called - ChanSettings.saveLocation.setSync("") - ChanSettings.saveLocation.setSync(dirPath) + ChanSettings.saveLocation.setSyncNoCheck(dirPath) val newSaveFilesDirectory = fileManager.newBaseDirectoryFile( - FilesBaseDirectory::class.java + SavedFilesBaseDirectory::class.java ) if (newSaveFilesDirectory == null) { @@ -161,37 +209,13 @@ class MediaSettingsControllerPresenter( } withCallbacks { - askUserIfTheyWantToMoveOldThreadsToTheNewDirectory( + askUserIfTheyWantToMoveOldSavedFilesToTheNewDirectory( oldSaveFilesDirectory, newSaveFilesDirectory ) } } - /** - * Select a directory where saved images will be stored via the SAF - */ - fun onSaveLocationUseSAFClicked() { - fileChooser.openChooseDirectoryDialog(object : DirectoryChooserCallback() { - override fun onResult(uri: Uri) { - ChanSettings.saveLocationUri.set(uri.toString()) - - val defaultDir = ChanSettings.getDefaultSaveLocationDir() - ChanSettings.saveLocation.setNoUpdate(defaultDir) - - withCallbacks { - updateSaveLocationViewText(uri.toString()) - } - } - - override fun onCancel(reason: String) { - withCallbacks { - showToast(reason, Toast.LENGTH_LONG) - } - } - }) - } - fun moveOldFilesToTheNewDirectory( oldBaseDirectory: AbstractFile?, newBaseDirectory: AbstractFile? @@ -240,7 +264,7 @@ class MediaSettingsControllerPresenter( val result = fileManager.copyDirectoryWithContent( oldBaseDirectory, newBaseDirectory, - false + true ) { fileIndex, totalFilesCount -> if (callbacks == null) { // User left the MediaSettings screen, we need to cancel the file copying @@ -289,7 +313,12 @@ class MediaSettingsControllerPresenter( newBaseDirectory: AbstractFile ) - fun updateLoadingViewText(newLocation: String) + fun askUserIfTheyWantToMoveOldSavedFilesToTheNewDirectory( + oldBaseDirectory: AbstractFile, + newBaseDirectory: AbstractFile + ) + + fun updateLoadingViewText(text: String) fun updateSaveLocationViewText(newLocation: String) fun showCopyFilesDialog( diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaver.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaver.java index 995b03c522..92d7f1a44b 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaver.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/saver/ImageSaver.java @@ -30,8 +30,9 @@ import com.github.adamantcheese.chan.core.settings.ChanSettings; import com.github.adamantcheese.chan.ui.helper.RuntimePermissionsHelper; import com.github.adamantcheese.chan.ui.service.SavingNotification; -import com.github.adamantcheese.chan.ui.settings.base_directory.FilesBaseDirectory; +import com.github.adamantcheese.chan.ui.settings.base_directory.SavedFilesBaseDirectory; import com.github.adamantcheese.chan.utils.Logger; +import com.github.adamantcheese.chan.utils.StringUtils; import com.github.k1rakishou.fsaf.FileManager; import com.github.k1rakishou.fsaf.file.AbstractFile; import com.github.k1rakishou.fsaf.file.DirectorySegment; @@ -148,7 +149,7 @@ public String getSubFolder(String name) { @Nullable public AbstractFile getSaveLocation(ImageSaveTask task) { - AbstractFile baseSaveDir = fileManager.newBaseDirectoryFile(FilesBaseDirectory.class); + AbstractFile baseSaveDir = fileManager.newBaseDirectoryFile(SavedFilesBaseDirectory.class); if (baseSaveDir == null) { Logger.e(TAG, "getSaveLocation() fileManager.newSaveLocationFile() returned null"); return null; @@ -161,14 +162,14 @@ public AbstractFile getSaveLocation(ImageSaveTask task) { return null; } - if (!fileManager.baseDirectoryExists(FilesBaseDirectory.class)) { + if (!fileManager.baseDirectoryExists(SavedFilesBaseDirectory.class)) { Logger.e(TAG, "Base save local directory does not exist"); return null; } String subFolder = task.getSubFolder(); if (subFolder != null) { - baseSaveDir.clone(new DirectorySegment(subFolder)); + return baseSaveDir.cloneUnsafe(subFolder); } return baseSaveDir; @@ -213,8 +214,10 @@ private boolean startBundledTaskInternal(String subFolder, List t continue; } + String fixedSubfolderName = StringUtils.dirNameRemoveBadCharacters(subFolder); + AbstractFile destinationFile = saveLocation - .clone(new DirectorySegment(subFolder), new FileSegment(fileName)); + .clone(new DirectorySegment(fixedSubfolderName), new FileSegment(fileName)); task.setDestination(destinationFile); startTask(task); diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/ChanSettings.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/ChanSettings.java index b1df9a8c5a..a248269523 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/ChanSettings.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/ChanSettings.java @@ -382,14 +382,45 @@ private static void loadProxy() { * Called on the Database thread. */ public static String serializeToString() throws IOException { + String prevSaveLocationUri = null; + String prevLocalThreadsLocationUri = null; + + // We need to check if the user has any of the location settings set to a SAF directory. + // We can't export them because if the user reinstalls the app and then imports a location + // setting that point to a SAF directory that directory won't be valid for the app because + // after clearing settings all permissions for that directory will be lost. So in case the + // user tries to export SAF directory paths we don't export them and instead export default + // locations. But we also don't wont to change the paths for the current app so we need to + // save the previous paths, patch the sharedPrefs file read it to string and then restore + // the current paths back to what they were before exporting. + if (!ChanSettings.saveLocationUri.get().isEmpty()) { + // Save the saveLocationUri + prevSaveLocationUri = ChanSettings.saveLocationUri.get(); + + ChanSettings.saveLocationUri.remove(); + ChanSettings.saveLocation.setSyncNoCheck(ChanSettings.getDefaultSaveLocationDir()); + } + + if (!ChanSettings.localThreadsLocationUri.get().isEmpty()) { + // Save the localThreadsLocationUri + prevLocalThreadsLocationUri = ChanSettings.localThreadsLocationUri.get(); + + ChanSettings.localThreadsLocationUri.remove(); + ChanSettings.localThreadLocation.setSyncNoCheck( + ChanSettings.getDefaultLocalThreadsLocation() + ); + } + File file = new File(AndroidUtils.getAppDir(), sharedPrefsFile); if (!file.exists()) { - throw new IOException("Shared preferences file does not exist! (" + file.getAbsolutePath() + ")"); + throw new IOException("Shared preferences file does not exist! " + + "(" + file.getAbsolutePath() + ")"); } if (!file.canRead()) { - throw new IOException("Cannot read from shared preferences file! (" + file.getAbsolutePath() + ")"); + throw new IOException("Cannot read from shared preferences file!" + + "(" + file.getAbsolutePath() + ")"); } byte[] buffer = new byte[(int) file.length()]; @@ -398,10 +429,22 @@ public static String serializeToString() throws IOException { int readAmount = inputStream.read(buffer); if (readAmount != file.length()) { - throw new IOException("Could not read shared prefs file readAmount != fileLength " + readAmount + ", " + file.length()); + throw new IOException("Could not read shared prefs file readAmount != fileLength " + + readAmount + ", " + file.length()); } } + // Restore back the previous paths + if (prevSaveLocationUri != null) { + ChanSettings.saveLocation.setSyncNoCheck(""); + ChanSettings.saveLocationUri.setSyncNoCheck(prevSaveLocationUri); + } + + if (prevLocalThreadsLocationUri != null) { + ChanSettings.localThreadLocation.setSyncNoCheck(""); + ChanSettings.localThreadsLocationUri.setSyncNoCheck(prevLocalThreadsLocationUri); + } + return new String(buffer); } @@ -413,11 +456,13 @@ public static void deserializeFromString(String settings) throws IOException { File file = new File(AndroidUtils.getAppDir(), sharedPrefsFile); if (!file.exists()) { - throw new IOException("Shared preferences file does not exist! (" + file.getAbsolutePath() + ")"); + throw new IOException("Shared preferences file does not exist! " + + "(" + file.getAbsolutePath() + ")"); } if (!file.canWrite()) { - throw new IOException("Cannot write to shared preferences file! (" + file.getAbsolutePath() + ")"); + throw new IOException("Cannot write to shared preferences file! " + + "(" + file.getAbsolutePath() + ")"); } try (FileOutputStream outputStream = new FileOutputStream(file)) { @@ -426,16 +471,6 @@ public static void deserializeFromString(String settings) throws IOException { } } - public static boolean isLocalThreadsDirUsesSAF() { - if (ChanSettings.localThreadsLocationUri.get().isEmpty() - && ChanSettings.localThreadLocation.get().isEmpty()) { - throw new IllegalStateException("Both localThreadsLocationUri and " + - "localThreadLocation are empty!"); - } - - return !ChanSettings.localThreadsLocationUri.get().isEmpty(); - } - public static class ThemeColor { public String theme; public String color; diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/SettingProvider.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/SettingProvider.java index 2a98cdc847..eb9aa35927 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/SettingProvider.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/SettingProvider.java @@ -34,4 +34,6 @@ public interface SettingProvider { void putString(String key, String value); void putStringSync(String key, String value); + + void removeSync(String key); } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/SharedPreferencesSettingProvider.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/SharedPreferencesSettingProvider.java index c102ea09cb..988c1a215d 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/SharedPreferencesSettingProvider.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/SharedPreferencesSettingProvider.java @@ -81,4 +81,9 @@ public void putString(String key, String value) { public void putStringSync(String key, String value) { prefs.edit().putString(key, value).commit(); } + + @Override + public void removeSync(String key) { + prefs.edit().remove(key).commit(); + } } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/StringSetting.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/StringSetting.java index 66b6fdf6f5..72a2f4e242 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/StringSetting.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/StringSetting.java @@ -58,4 +58,16 @@ public void setSync(String value) { onValueChanged(); } } + + public void setSyncNoCheck(String value) { + settingProvider.putStringSync(key, value); + cached = value; + onValueChanged(); + } + + public void remove() { + settingProvider.removeSync(key); + hasCached = false; + cached = null; + } } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/json/JsonSettingsProvider.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/json/JsonSettingsProvider.java index 897b66d691..17018cd243 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/json/JsonSettingsProvider.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/json/JsonSettingsProvider.java @@ -127,4 +127,9 @@ public void putStringSync(String key, String value) { public interface Callback { void save(); } + + @Override + public void removeSync(String key) { + throw new UnsupportedOperationException(); + } } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ImageViewerController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ImageViewerController.java index c153c5dbe0..b78e787479 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ImageViewerController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ImageViewerController.java @@ -43,7 +43,6 @@ import com.android.volley.VolleyError; import com.davemorrissey.labs.subscaleview.ImageViewState; -import com.github.adamantcheese.chan.Chan; import com.github.adamantcheese.chan.R; import com.github.adamantcheese.chan.controller.Controller; import com.github.adamantcheese.chan.core.image.ImageContainer; @@ -74,6 +73,7 @@ import com.github.adamantcheese.chan.ui.view.TransitionImageView; import com.github.adamantcheese.chan.utils.AndroidUtils; import com.github.adamantcheese.chan.utils.Logger; +import com.github.adamantcheese.chan.utils.StringUtils; import java.io.File; import java.util.ArrayList; @@ -297,15 +297,18 @@ private void saveShare(boolean share, PostImage postImage) { imageViewerCallback.getPostForPostImage(postImage).no : presenter.getLoadable().no) + "_"; + String fixedSubFolderName = StringUtils.dirNameRemoveBadCharacters(subFolderName); + String tempTitle = (presenter.getLoadable().no == 0 ? PostHelper.getTitle(imageViewerCallback.getPostForPostImage(postImage), null) : - presenter.getLoadable().title) - .toLowerCase() - .replaceAll(" ", "_") - .replaceAll("[^a-z0-9_]", ""); - tempTitle = tempTitle.substring(0, Math.min(tempTitle.length(), 50)); - subFolderName = subFolderName + tempTitle; + presenter.getLoadable().title); + + String fixedTitle = StringUtils.fileNameRemoveBadCharacters(tempTitle); + + subFolderName = fixedSubFolderName + + fixedTitle.substring(0, Math.min(fixedTitle.length(), 50)); } + task.setSubFolder(subFolderName); } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/LoadingViewController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/LoadingViewController.java index 317ecb22ff..1f766b3b13 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/LoadingViewController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/LoadingViewController.java @@ -2,12 +2,14 @@ import android.content.Context; import android.view.View; +import android.widget.ProgressBar; import android.widget.TextView; import com.github.adamantcheese.chan.R; public class LoadingViewController extends BaseFloatingController { private TextView textView; + private ProgressBar progressBar; private boolean indeterminate; public LoadingViewController(Context context, boolean indeterminate) { @@ -20,9 +22,8 @@ public LoadingViewController(Context context, boolean indeterminate) { public void onCreate() { super.onCreate(); - if (!indeterminate) { - textView = view.findViewById(R.id.progress_percent); - } + textView = view.findViewById(R.id.text); + progressBar = view.findViewById(R.id.progress_bar); } @Override @@ -30,6 +31,9 @@ public boolean onBack() { return true; } + /** + * Shows a progress bar with percentage in the center (cannot be used with indeterminate) + * */ public void updateProgress(int percent) { if (indeterminate) { throw new IllegalStateException("Cannot be used with indeterminate flag"); @@ -39,9 +43,17 @@ public void updateProgress(int percent) { textView.setVisibility(View.VISIBLE); } + if (progressBar.getVisibility() != View.VISIBLE) { + progressBar.setVisibility(View.VISIBLE); + } + textView.setText(String.valueOf(percent)); } + /** + * Hide a progress bar and instead of percentage any text may be shown + * (cannot be used with indeterminate) + * */ public void updateWithText(String text) { if (indeterminate) { throw new IllegalStateException("Cannot be used with indeterminate flag"); @@ -51,6 +63,10 @@ public void updateWithText(String text) { textView.setVisibility(View.VISIBLE); } + if (progressBar.getVisibility() == View.VISIBLE) { + progressBar.setVisibility(View.GONE); + } + textView.setText(text); } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java index 2f32045354..a4a6a4f7ac 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java @@ -215,7 +215,7 @@ private void populatePreferences() { * ============================================== * Setup Local Threads location * ============================================== - * */ + */ private void setupLocalThreadLocationSetting(SettingsGroup media) { if (!ChanSettings.incrementalThreadDownloadingEnabled.get()) { @@ -294,7 +294,7 @@ private void onLocalThreadsLocationUseOldApiClicked() { * ============================================== * Setup Save Files location * ============================================== - * */ + */ private void setupSaveLocationSetting(SettingsGroup media) { LinkSettingView chooseSaveLocationSetting = new LinkSettingView(this, @@ -348,14 +348,13 @@ private void onSaveLocationUseOldApiClicked() { * ============================================== * Presenter callbacks * ============================================== - * */ + */ @Override public void askUserIfTheyWantToMoveOldThreadsToTheNewDirectory( @NonNull AbstractFile oldBaseDirectory, @NonNull AbstractFile newBaseDirectory ) { - // TODO: strings AlertDialog alertDialog = new AlertDialog.Builder(context) .setTitle("Move old local threads to the new directory?") @@ -372,6 +371,27 @@ public void askUserIfTheyWantToMoveOldThreadsToTheNewDirectory( alertDialog.show(); } + @Override + public void askUserIfTheyWantToMoveOldSavedFilesToTheNewDirectory( + @NotNull AbstractFile oldBaseDirectory, + @NotNull AbstractFile newBaseDirectory + ) { + // TODO: strings + AlertDialog alertDialog = new AlertDialog.Builder(context) + .setTitle("Move old saved files to the new directory?") + .setMessage("This operation may take quite some time. Once started this operation must not be canceled.") + .setPositiveButton("Move", (dialog, which) -> { + presenter.moveOldFilesToTheNewDirectory( + oldBaseDirectory, + newBaseDirectory + ); + }) + .setNegativeButton("Do not", (dialog, which) -> dialog.dismiss()) + .create(); + + alertDialog.show(); + } + @Override public void onCopyDirectoryEnded( @NonNull AbstractFile oldBaseDirectory, @@ -380,24 +400,55 @@ public void onCopyDirectoryEnded( ) { BackgroundUtils.ensureMainThread(); - navigationController.popController(); + if (loadingViewController == null) { + throw new IllegalStateException("LoadingViewController was not shown beforehand!"); + } + + loadingViewController.stopPresenting(); loadingViewController = null; if (!result) { // TODO: strings showToast("Could not copy one directory's file into another one", Toast.LENGTH_LONG); } else { - if (oldBaseDirectory instanceof ExternalFile) { - forgetPreviousExternalBaseDirectory(oldBaseDirectory); - } - - // TODO: delete old directory dialog + showDeleteOldDirectoryDialog(oldBaseDirectory); // TODO: strings showToast("Successfully copied files", Toast.LENGTH_LONG); } } + private void showDeleteOldDirectoryDialog( + @NonNull AbstractFile oldBaseDirectory + ) { + // TODO: strings + AlertDialog alertDialog = new AlertDialog.Builder(context) + .setTitle("Would you like to delete old directory?") + .setMessage("Files have been copied and now exist in two directories. You may want to remove old directory since you won't need it anymore") + .setPositiveButton("Delete", (dialog, which) -> { + if (!fileManager.delete(oldBaseDirectory)) { + showToast("Couldn't delete old directory", Toast.LENGTH_LONG); + return; + } + + if (oldBaseDirectory instanceof ExternalFile) { + forgetPreviousExternalBaseDirectory(oldBaseDirectory); + } + + showToast("Successfully deleted old directory", Toast.LENGTH_LONG); + }) + .setNegativeButton("Do not", (dialog, which) -> { + if (oldBaseDirectory instanceof ExternalFile) { + forgetPreviousExternalBaseDirectory(oldBaseDirectory); + } + + dialog.dismiss(); + }) + .create(); + + alertDialog.show(); + } + @Override public void updateLoadingViewText(@NotNull String text) { BackgroundUtils.ensureMainThread(); @@ -450,7 +501,7 @@ public void showCopyFilesDialog( false ); - navigationController.pushController(loadingViewController); + navigationController.presentController(loadingViewController); presenter.moveFilesInternal( oldBaseDirectory, @@ -467,7 +518,7 @@ public void showCopyFilesDialog( * ============================================== * Other methods * ============================================== - * */ + */ private void forgetPreviousExternalBaseDirectory( @NonNull AbstractFile oldLocalThreadsDirectory diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/settings/base_directory/FilesBaseDirectory.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/settings/base_directory/SavedFilesBaseDirectory.kt similarity index 96% rename from Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/settings/base_directory/FilesBaseDirectory.kt rename to Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/settings/base_directory/SavedFilesBaseDirectory.kt index 0e6be8ac4d..e7ff01b6b7 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/settings/base_directory/FilesBaseDirectory.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/settings/base_directory/SavedFilesBaseDirectory.kt @@ -6,7 +6,7 @@ import com.github.adamantcheese.chan.core.settings.ChanSettings import com.github.k1rakishou.fsaf.manager.base_directory.BaseDirectory import java.io.File -class FilesBaseDirectory( +class SavedFilesBaseDirectory( ) : BaseDirectory(BuildConfig.DEBUG) { override fun getDirFile(): File? { diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/utils/StringUtils.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/utils/StringUtils.java index eea84bdbda..27e107a004 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/utils/StringUtils.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/utils/StringUtils.java @@ -39,4 +39,20 @@ public static String extractFileExtensionFromImageUrl(String url) { return url.substring(index + 1); } + public static String dirNameRemoveBadCharacters(String dirName) { + return dirName + .toLowerCase() + .replaceAll(" ", "_") + .replaceAll("[^a-z0-9_]", ""); + } + + /** + * The same as dirNameRemoveBadCharacters but allows dots since file names can have extensions + * */ + public static String fileNameRemoveBadCharacters(String filename) { + return filename + .toLowerCase() + .replaceAll(" ", "_") + .replaceAll("[^a-z0-9_.]", ""); + } } diff --git a/Kuroba/app/src/main/res/layout/controller_loading_view.xml b/Kuroba/app/src/main/res/layout/controller_loading_view.xml index c4c8f3d694..d5351fafd2 100644 --- a/Kuroba/app/src/main/res/layout/controller_loading_view.xml +++ b/Kuroba/app/src/main/res/layout/controller_loading_view.xml @@ -61,18 +61,18 @@ along with this program. If not, see . tools:ignore="HardcodedText" /> + app:layout_constraintBottom_toBottomOf="@+id/progress_bar" + app:layout_constraintEnd_toEndOf="@+id/progress_bar" + app:layout_constraintStart_toStartOf="@+id/progress_bar" + app:layout_constraintTop_toTopOf="@+id/progress_bar" /> Date: Sat, 26 Oct 2019 21:33:00 +0300 Subject: [PATCH 176/184] (#172) Move all strings to resources --- .../MediaSettingsControllerPresenter.kt | 59 +++--- .../controller/MediaSettingsController.java | 173 +++++++++++------- Kuroba/app/src/main/res/values/strings.xml | 25 +++ 3 files changed, 156 insertions(+), 101 deletions(-) diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/MediaSettingsControllerPresenter.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/MediaSettingsControllerPresenter.kt index a2aa178881..4a73578526 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/MediaSettingsControllerPresenter.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/MediaSettingsControllerPresenter.kt @@ -1,7 +1,9 @@ package com.github.adamantcheese.chan.core.presenter +import android.content.Context import android.net.Uri import android.widget.Toast +import com.github.adamantcheese.chan.R import com.github.adamantcheese.chan.core.settings.ChanSettings import com.github.adamantcheese.chan.ui.settings.base_directory.LocalThreadsBaseDirectory import com.github.adamantcheese.chan.ui.settings.base_directory.SavedFilesBaseDirectory @@ -11,10 +13,10 @@ import com.github.k1rakishou.fsaf.FileChooser import com.github.k1rakishou.fsaf.FileManager import com.github.k1rakishou.fsaf.callback.DirectoryChooserCallback import com.github.k1rakishou.fsaf.file.AbstractFile -import java.util.* import java.util.concurrent.Executors class MediaSettingsControllerPresenter( + private val appContext: Context, private val fileManager: FileManager, private val fileChooser: FileChooser, private var callbacks: MediaSettingsControllerCallbacks? @@ -38,16 +40,17 @@ class MediaSettingsControllerPresenter( if (oldLocalThreadsDirectory == null) { withCallbacks { - // TODO: string - showToast("Old local threads base directory is " + - "probably not registered (newBaseDirectoryFile returned null)") + showToast(appContext.getString(R.string.media_settings_old_threads_base_dir_not_registered)) } return } + Logger.d(TAG, "onLocalThreadsLocationUseSAFClicked dir = $uri") ChanSettings.localThreadsLocationUri.set(uri.toString()) - ChanSettings.localThreadLocation.setNoUpdate(ChanSettings.getDefaultLocalThreadsLocation()) + ChanSettings.localThreadLocation.setNoUpdate( + ChanSettings.getDefaultLocalThreadsLocation() + ) withCallbacks { updateLocalThreadsLocation(uri.toString()) @@ -59,8 +62,7 @@ class MediaSettingsControllerPresenter( if (newLocalThreadsDirectory == null) { withCallbacks { - // TODO: strings - showToast("New local threads base directory is probably not registered") + showToast(appContext.getString(R.string.media_settings_new_threads_base_dir_not_registered)) } return @@ -89,15 +91,13 @@ class MediaSettingsControllerPresenter( if (oldLocalThreadsDirectory == null) { withCallbacks { - // TODO: String - showToast("Old local threads base directory is " + - "probably not registered (newBaseDirectoryFile returned null)") + showToast(appContext.getString(R.string.media_settings_old_threads_base_dir_not_registered)) } return } - Logger.d(TAG, "SaveLocationController with LocalThreadsSaveLocation mode returned dir $dirPath") + Logger.d(TAG, "onLocalThreadsLocationChosen dir = $dirPath") ChanSettings.localThreadLocation.setSyncNoCheck(dirPath) val newLocalThreadsDirectory = fileManager.newBaseDirectoryFile( @@ -106,8 +106,7 @@ class MediaSettingsControllerPresenter( if (newLocalThreadsDirectory == null) { withCallbacks { - // TODO: strings - showToast("New local threads base directory is probably not registered") + showToast(appContext.getString(R.string.media_settings_new_threads_base_dir_not_registered)) } return @@ -133,14 +132,14 @@ class MediaSettingsControllerPresenter( if (oldSavedFileBaseDirectory == null) { withCallbacks { - // TODO: string - showToast("Old saved files base directory is " + - "probably not registered (newBaseDirectoryFile returned null)") + showToast(appContext.getString( + R.string.media_settings_old_saved_files_base_dir_not_registered)) } return } + Logger.d(TAG, "onSaveLocationUseSAFClicked dir = $uri") ChanSettings.saveLocationUri.set(uri.toString()) ChanSettings.saveLocation.setNoUpdate(ChanSettings.getDefaultSaveLocationDir()) @@ -154,8 +153,8 @@ class MediaSettingsControllerPresenter( if (newSavedFilesBaseDirectory == null) { withCallbacks { - // TODO: strings - showToast("New saved files base directory is probably not registered") + showToast(appContext.getString( + R.string.media_settings_new_saved_files_base_dir_not_registered)) } return @@ -184,15 +183,14 @@ class MediaSettingsControllerPresenter( if (oldSaveFilesDirectory == null) { withCallbacks { - // TODO: String - showToast("Old save files base directory is " + - "probably not registered (newBaseDirectoryFile returned null)") + showToast(appContext.getString( + R.string.media_settings_old_saved_files_base_dir_not_registered)) } return } - Logger.d(TAG, "SaveLocationController with ImageSaveLocation mode returned dir $dirPath") + Logger.d(TAG, "onSaveLocationChosen dir = $dirPath") ChanSettings.saveLocation.setSyncNoCheck(dirPath) val newSaveFilesDirectory = fileManager.newBaseDirectoryFile( @@ -201,8 +199,8 @@ class MediaSettingsControllerPresenter( if (newSaveFilesDirectory == null) { withCallbacks { - // TODO: strings - showToast("New save files base directory is probably not registered") + showToast(appContext.getString( + R.string.media_settings_new_saved_files_base_dir_not_registered)) } return @@ -244,8 +242,7 @@ class MediaSettingsControllerPresenter( if (filesCount == 0) { withCallbacks { - // TODO: strings - showToast("No files to copy") + showToast(appContext.getString(R.string.media_settings_no_files_to_copy)) } return @@ -272,13 +269,11 @@ class MediaSettingsControllerPresenter( } withCallbacks { - // TODO: strings - val text = String.format( - Locale.US, - // TODO: strings - "Copying file %d out of %d", + val text = appContext.getString( + R.string.media_settings_copying_file, fileIndex, - totalFilesCount) + totalFilesCount + ) updateLoadingViewText(text) } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java index a4a6a4f7ac..0863852e5c 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java @@ -51,6 +51,7 @@ import javax.inject.Inject; import static com.github.adamantcheese.chan.Chan.inject; +import static com.github.adamantcheese.chan.utils.AndroidUtils.getAppContext; import static com.github.adamantcheese.chan.utils.AndroidUtils.getString; public class MediaSettingsController @@ -88,7 +89,12 @@ public void onCreate() { EventBus.getDefault().register(this); navigation.setTitle(R.string.settings_screen_media); - presenter = new MediaSettingsControllerPresenter(fileManager, fileChooser, this); + presenter = new MediaSettingsControllerPresenter( + getAppContext(), + fileManager, + fileChooser, + this + ); setupLayout(); populatePreferences(); @@ -355,17 +361,23 @@ public void askUserIfTheyWantToMoveOldThreadsToTheNewDirectory( @NonNull AbstractFile oldBaseDirectory, @NonNull AbstractFile newBaseDirectory ) { - // TODO: strings AlertDialog alertDialog = new AlertDialog.Builder(context) - .setTitle("Move old local threads to the new directory?") - .setMessage("This operation may take quite some time. Once started this operation must not be canceled.") - .setPositiveButton("Move", (dialog, which) -> { - presenter.moveOldFilesToTheNewDirectory( - oldBaseDirectory, - newBaseDirectory - ); - }) - .setNegativeButton("Do not", (dialog, which) -> dialog.dismiss()) + .setTitle(context.getString(R.string.media_settings_move_threads_to_new_dir)) + .setMessage(context.getString(R.string.media_settings_operation_may_take_some_time)) + .setPositiveButton( + context.getString(R.string.media_settings_move_threads), + (dialog, which) -> { + presenter.moveOldFilesToTheNewDirectory( + oldBaseDirectory, + newBaseDirectory + ); + }) + .setNegativeButton( + context.getString(R.string.media_settings_do_not_move_threads), + (dialog, which) -> { + dialog.dismiss(); + } + ) .create(); alertDialog.show(); @@ -376,17 +388,23 @@ public void askUserIfTheyWantToMoveOldSavedFilesToTheNewDirectory( @NotNull AbstractFile oldBaseDirectory, @NotNull AbstractFile newBaseDirectory ) { - // TODO: strings AlertDialog alertDialog = new AlertDialog.Builder(context) - .setTitle("Move old saved files to the new directory?") - .setMessage("This operation may take quite some time. Once started this operation must not be canceled.") - .setPositiveButton("Move", (dialog, which) -> { - presenter.moveOldFilesToTheNewDirectory( - oldBaseDirectory, - newBaseDirectory - ); - }) - .setNegativeButton("Do not", (dialog, which) -> dialog.dismiss()) + .setTitle(context.getString(R.string.media_settings_move_saved_file_to_new_dir)) + .setMessage(context.getString(R.string.media_settings_operation_may_take_some_time)) + .setPositiveButton( + context.getString(R.string.media_settings_move_saved_files), + (dialog, which) -> { + presenter.moveOldFilesToTheNewDirectory( + oldBaseDirectory, + newBaseDirectory + ); + }) + .setNegativeButton( + context.getString(R.string.media_settings_do_not_move_saved_files), + (dialog, which) -> { + dialog.dismiss(); + } + ) .create(); alertDialog.show(); @@ -408,42 +426,48 @@ public void onCopyDirectoryEnded( loadingViewController = null; if (!result) { - // TODO: strings - showToast("Could not copy one directory's file into another one", Toast.LENGTH_LONG); + showToast(context.getString(R.string.media_settings_couldnot_copy_files), Toast.LENGTH_LONG); } else { showDeleteOldDirectoryDialog(oldBaseDirectory); - - // TODO: strings - showToast("Successfully copied files", Toast.LENGTH_LONG); + showToast(context.getString(R.string.media_settings_files_copied), Toast.LENGTH_LONG); } } private void showDeleteOldDirectoryDialog( @NonNull AbstractFile oldBaseDirectory ) { - // TODO: strings AlertDialog alertDialog = new AlertDialog.Builder(context) - .setTitle("Would you like to delete old directory?") - .setMessage("Files have been copied and now exist in two directories. You may want to remove old directory since you won't need it anymore") - .setPositiveButton("Delete", (dialog, which) -> { - if (!fileManager.delete(oldBaseDirectory)) { - showToast("Couldn't delete old directory", Toast.LENGTH_LONG); - return; - } - - if (oldBaseDirectory instanceof ExternalFile) { - forgetPreviousExternalBaseDirectory(oldBaseDirectory); - } - - showToast("Successfully deleted old directory", Toast.LENGTH_LONG); - }) - .setNegativeButton("Do not", (dialog, which) -> { - if (oldBaseDirectory instanceof ExternalFile) { - forgetPreviousExternalBaseDirectory(oldBaseDirectory); - } - - dialog.dismiss(); - }) + .setTitle(context.getString(R.string.media_settings_would_you_life_to_delete_old_dir)) + .setMessage(context.getString(R.string.media_settings_file_have_been_copied)) + .setPositiveButton( + context.getString(R.string.media_settings_delete_button_name), + (dialog, which) -> { + if (!fileManager.delete(oldBaseDirectory)) { + showToast( + context.getString(R.string.media_settings_couldnot_delete_old_dir), + Toast.LENGTH_LONG + ); + return; + } + + if (oldBaseDirectory instanceof ExternalFile) { + forgetPreviousExternalBaseDirectory(oldBaseDirectory); + } + + showToast( + context.getString(R.string.media_settings_old_dir_deleted), + Toast.LENGTH_LONG + ); + }) + .setNegativeButton( + context.getString(R.string.media_settings_do_not_delete), + (dialog, which) -> { + if (oldBaseDirectory instanceof ExternalFile) { + forgetPreviousExternalBaseDirectory(oldBaseDirectory); + } + + dialog.dismiss(); + }) .create(); alertDialog.show(); @@ -490,25 +514,33 @@ public void showCopyFilesDialog( ); } - // TODO: strings AlertDialog alertDialog = new AlertDialog.Builder(context) - .setTitle("Copy files") - .setMessage("Do you want to copy " + filesCount + - " files from old directory to the new one?") - .setPositiveButton("Copy", (dialog, which) -> { - loadingViewController = new LoadingViewController( - context, - false - ); - - navigationController.presentController(loadingViewController); - - presenter.moveFilesInternal( - oldBaseDirectory, - newBaseDirectory - ); - }) - .setNegativeButton("Do not", (dialog, which) -> dialog.dismiss()) + .setTitle(context.getString(R.string.media_settings_copy_files)) + .setMessage( + context.getString(R.string.media_settings_do_you_want_to_copy_files, + filesCount) + ) + .setPositiveButton( + context.getString(R.string.media_settings_copy_files), + (dialog, which) -> { + loadingViewController = new LoadingViewController( + context, + false + ); + + navigationController.presentController(loadingViewController); + + presenter.moveFilesInternal( + oldBaseDirectory, + newBaseDirectory + ); + }) + .setNegativeButton( + context.getString(R.string.media_settings_do_not_copy_files), + (dialog, which) -> { + dialog.dismiss(); + } + ) .create(); alertDialog.show(); @@ -524,11 +556,14 @@ private void forgetPreviousExternalBaseDirectory( @NonNull AbstractFile oldLocalThreadsDirectory ) { if (oldLocalThreadsDirectory instanceof ExternalFile) { - Uri safTreeuri = oldLocalThreadsDirectory + Uri safTreeUri = oldLocalThreadsDirectory .getFileRoot().getHolder().uri(); - if (!fileChooser.forgetSAFTree(safTreeuri)) { - showToast("Couldn't release uri permissions", Toast.LENGTH_SHORT); + if (!fileChooser.forgetSAFTree(safTreeUri)) { + showToast( + context.getString(R.string.media_settings_could_not_release_uri_permissions), + Toast.LENGTH_SHORT + ); } } } diff --git a/Kuroba/app/src/main/res/values/strings.xml b/Kuroba/app/src/main/res/values/strings.xml index ae66c11fc5..fe5cb8aae2 100644 --- a/Kuroba/app/src/main/res/values/strings.xml +++ b/Kuroba/app/src/main/res/values/strings.xml @@ -707,4 +707,29 @@ Don't have a 4chan Pass?
Base local threads directory does not exist (or it was deleted). You need to manually set it again in Media settings. Could not save one or more images Could not create base local threads directory + Could not copy one directory\'s file into another one + Successfully copied files + Would you like to delete old directory? + Files have been copied and now exist in two directories. You may want to remove old directory since you won\'t need it anymore + Delete + Couldn\'t delete old directory + Successfully deleted old directory + Do not + Copy files + Do you want to copy %1$d files from old directory to the new one?" + Do not + Couldn\'t release uri permissions + Move old local threads to the new directory? + This operation may take quite some time. Once started this operation must not be canceled. + Move + Move old saved files to the new directory? + Do not + Move + Do not + Old local threads base directory is probably not registered (newBaseDirectoryFile returned null) + New local threads base directory is probably not registered + Old saved files base directory is probably not registered (newBaseDirectoryFile returned null) + New saved files base directory is probably not registered + No files to copy + Copying file %1$d out of %2$d From 1eb75214c5637e5b3c38d4697fcf97302269326a Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 27 Oct 2019 11:57:04 +0300 Subject: [PATCH 177/184] (#172) Update Fuck-Storage-Access-Framework to the latest release version --- Kuroba/app/build.gradle | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/Kuroba/app/build.gradle b/Kuroba/app/build.gradle index 02d432dc49..21d0cd93ec 100644 --- a/Kuroba/app/build.gradle +++ b/Kuroba/app/build.gradle @@ -96,12 +96,6 @@ android { exclude 'META-INF/LICENSE-W3C-TEST' } - // TODO: delete before pushing - configurations.all { - // Check for updates every build - resolutionStrategy.cacheChangingModulesFor 0, 'seconds' - } - buildTypes { release { /* @@ -167,9 +161,7 @@ dependencies { implementation 'io.reactivex.rxjava2:rxjava:2.2.12' implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - - // TODO: delete "changing = true" before pushing - implementation ('com.github.K1rakishou:Fuck-Storage-Access-Framework:develop-SNAPSHOT') { changing = true } + implementation 'com.github.K1rakishou:Fuck-Storage-Access-Framework:v1.0-alpha' testImplementation 'junit:junit:4.12' } From 24d792d17586a885be5d167f5715ae7dd62df06c Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 27 Oct 2019 16:25:27 +0300 Subject: [PATCH 178/184] (#172) Use generic base newBaseDirectoryFile when calling it from the kotlin's functions --- .../core/manager/SavedThreadLoaderManager.kt | 2 +- .../MediaSettingsControllerPresenter.kt | 38 +++++++------------ 2 files changed, 15 insertions(+), 25 deletions(-) diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/SavedThreadLoaderManager.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/SavedThreadLoaderManager.kt index 47c62c25ed..a813a39d70 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/SavedThreadLoaderManager.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/SavedThreadLoaderManager.kt @@ -23,7 +23,7 @@ class SavedThreadLoaderManager @Inject constructor( throw RuntimeException("Cannot be executed on the main thread!") } - val baseDir = fileManager.newBaseDirectoryFile(LocalThreadsBaseDirectory::class.java) + val baseDir = fileManager.newBaseDirectoryFile() if (baseDir == null) { Logger.e(TAG, "loadSavedThread() fileManager.newLocalThreadFile() returned null") return null diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/MediaSettingsControllerPresenter.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/MediaSettingsControllerPresenter.kt index 4a73578526..8f4dc7ca2e 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/MediaSettingsControllerPresenter.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/MediaSettingsControllerPresenter.kt @@ -34,9 +34,8 @@ class MediaSettingsControllerPresenter( fun onLocalThreadsLocationUseSAFClicked() { fileChooser.openChooseDirectoryDialog(object : DirectoryChooserCallback() { override fun onResult(uri: Uri) { - val oldLocalThreadsDirectory = fileManager.newBaseDirectoryFile( - LocalThreadsBaseDirectory::class.java - ) + val oldLocalThreadsDirectory = + fileManager.newBaseDirectoryFile() if (oldLocalThreadsDirectory == null) { withCallbacks { @@ -56,9 +55,8 @@ class MediaSettingsControllerPresenter( updateLocalThreadsLocation(uri.toString()) } - val newLocalThreadsDirectory = fileManager.newBaseDirectoryFile( - LocalThreadsBaseDirectory::class.java - ) + val newLocalThreadsDirectory = + fileManager.newBaseDirectoryFile() if (newLocalThreadsDirectory == null) { withCallbacks { @@ -85,9 +83,8 @@ class MediaSettingsControllerPresenter( } fun onLocalThreadsLocationChosen(dirPath: String) { - val oldLocalThreadsDirectory = fileManager.newBaseDirectoryFile( - LocalThreadsBaseDirectory::class.java - ) + val oldLocalThreadsDirectory = + fileManager.newBaseDirectoryFile() if (oldLocalThreadsDirectory == null) { withCallbacks { @@ -100,9 +97,8 @@ class MediaSettingsControllerPresenter( Logger.d(TAG, "onLocalThreadsLocationChosen dir = $dirPath") ChanSettings.localThreadLocation.setSyncNoCheck(dirPath) - val newLocalThreadsDirectory = fileManager.newBaseDirectoryFile( - LocalThreadsBaseDirectory::class.java - ) + val newLocalThreadsDirectory = + fileManager.newBaseDirectoryFile() if (newLocalThreadsDirectory == null) { withCallbacks { @@ -126,9 +122,8 @@ class MediaSettingsControllerPresenter( fun onSaveLocationUseSAFClicked() { fileChooser.openChooseDirectoryDialog(object : DirectoryChooserCallback() { override fun onResult(uri: Uri) { - val oldSavedFileBaseDirectory = fileManager.newBaseDirectoryFile( - SavedFilesBaseDirectory::class.java - ) + val oldSavedFileBaseDirectory = + fileManager.newBaseDirectoryFile() if (oldSavedFileBaseDirectory == null) { withCallbacks { @@ -147,9 +142,8 @@ class MediaSettingsControllerPresenter( updateSaveLocationViewText(uri.toString()) } - val newSavedFilesBaseDirectory = fileManager.newBaseDirectoryFile( - SavedFilesBaseDirectory::class.java - ) + val newSavedFilesBaseDirectory = + fileManager.newBaseDirectoryFile() if (newSavedFilesBaseDirectory == null) { withCallbacks { @@ -177,9 +171,7 @@ class MediaSettingsControllerPresenter( } fun onSaveLocationChosen(dirPath: String) { - val oldSaveFilesDirectory = fileManager.newBaseDirectoryFile( - SavedFilesBaseDirectory::class.java - ) + val oldSaveFilesDirectory = fileManager.newBaseDirectoryFile() if (oldSaveFilesDirectory == null) { withCallbacks { @@ -193,9 +185,7 @@ class MediaSettingsControllerPresenter( Logger.d(TAG, "onSaveLocationChosen dir = $dirPath") ChanSettings.saveLocation.setSyncNoCheck(dirPath) - val newSaveFilesDirectory = fileManager.newBaseDirectoryFile( - SavedFilesBaseDirectory::class.java - ) + val newSaveFilesDirectory = fileManager.newBaseDirectoryFile() if (newSaveFilesDirectory == null) { withCallbacks { From 97a7b2b2ac5b94ede90f2ab4497ede16e3556ef5 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 27 Oct 2019 17:20:49 +0300 Subject: [PATCH 179/184] (#172) Fix compilation error --- Kuroba/app/src/main/res/values/strings.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/Kuroba/app/src/main/res/values/strings.xml b/Kuroba/app/src/main/res/values/strings.xml index 6ae2ae53ad..72fe3664a9 100644 --- a/Kuroba/app/src/main/res/values/strings.xml +++ b/Kuroba/app/src/main/res/values/strings.xml @@ -623,7 +623,6 @@ Don't have a 4chan Pass?
You MAY LOSE some of your settings/pins/hidden threads if you are trying to import a file created with a different app version (upgrade or downgrade). The app will be restarted. \nAre you sure you want to continue?
- Exported successfully to \"%1$s\". File path has been copied to the clipboard. External storage is not mounted Could not create directory for export file %1$s; you probably won\'t be able to export settings Apply to replies From 0184fab3a3944a2fd3b735127861f47049544204 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 27 Oct 2019 18:22:28 +0300 Subject: [PATCH 180/184] (#172) Fix image saving related bugs, like file separators filtered in the paths thus resulting in losing the sub directories Fix shared_prefs importing/exporting for dev builds (wrong appId was used) Extract MediaSettingsControllerCallbacks into it's own class to avoid compilation errors --- .../MediaSettingsControllerPresenter.kt | 45 ++++-------- .../chan/core/settings/ChanSettings.java | 5 +- .../ui/controller/ImageViewerController.java | 68 ++++++++++++------- .../controller/MediaSettingsController.java | 47 +++++++------ .../MediaSettingsControllerCallbacks.kt | 34 ++++++++++ Kuroba/app/src/main/res/values/strings.xml | 7 +- 6 files changed, 125 insertions(+), 81 deletions(-) create mode 100644 Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsControllerCallbacks.kt diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/MediaSettingsControllerPresenter.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/MediaSettingsControllerPresenter.kt index 8f4dc7ca2e..af5a95246a 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/MediaSettingsControllerPresenter.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/presenter/MediaSettingsControllerPresenter.kt @@ -5,6 +5,7 @@ import android.net.Uri import android.widget.Toast import com.github.adamantcheese.chan.R import com.github.adamantcheese.chan.core.settings.ChanSettings +import com.github.adamantcheese.chan.ui.controller.MediaSettingsControllerCallbacks import com.github.adamantcheese.chan.ui.settings.base_directory.LocalThreadsBaseDirectory import com.github.adamantcheese.chan.ui.settings.base_directory.SavedFilesBaseDirectory import com.github.adamantcheese.chan.utils.AndroidUtils.runOnUiThread @@ -215,6 +216,20 @@ class MediaSettingsControllerPresenter( return } + // FIXME: this does not work when oldBaseDirectory was backed by the Java File and the new + // one by SAF the paths will be different. I should probably remove the base dir prefixes + // from both files split them into segments and compare segments. + if (oldBaseDirectory.getFullPath() == newBaseDirectory.getFullPath()) { + val message = appContext.getString( + R.string.media_settings_you_are_trying_to_move_files_in_the_same_directory) + + withCallbacks { + showToast(message, Toast.LENGTH_LONG) + } + + return + } + Logger.d(TAG, "oldLocalThreadsDirectory = " + oldBaseDirectory.getFullPath() + ", newLocalThreadsDirectory = " + newBaseDirectory.getFullPath() @@ -289,36 +304,6 @@ class MediaSettingsControllerPresenter( } } - interface MediaSettingsControllerCallbacks { - fun showToast(message: String, length: Int = Toast.LENGTH_SHORT) - fun updateLocalThreadsLocation(newLocation: String) - - fun askUserIfTheyWantToMoveOldThreadsToTheNewDirectory( - oldBaseDirectory: AbstractFile, - newBaseDirectory: AbstractFile - ) - - fun askUserIfTheyWantToMoveOldSavedFilesToTheNewDirectory( - oldBaseDirectory: AbstractFile, - newBaseDirectory: AbstractFile - ) - - fun updateLoadingViewText(text: String) - fun updateSaveLocationViewText(newLocation: String) - - fun showCopyFilesDialog( - filesCount: Int, - oldBaseDirectory: AbstractFile, - newBaseDirectory: AbstractFile - ) - - fun onCopyDirectoryEnded( - oldBaseDirectory: AbstractFile, - newBaseDirectory: AbstractFile, - result: Boolean - ) - } - companion object { private const val TAG = "MediaSettingsControllerPresenter" } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/ChanSettings.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/ChanSettings.java index 0311a23f74..3a9097a422 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/ChanSettings.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/settings/ChanSettings.java @@ -22,6 +22,7 @@ import androidx.annotation.NonNull; +import com.github.adamantcheese.chan.BuildConfig; import com.github.adamantcheese.chan.R; import com.github.adamantcheese.chan.core.manager.ThreadSaveManager; import com.github.adamantcheese.chan.ui.adapter.PostsFilter; @@ -103,7 +104,9 @@ public String getKey() { } private static Proxy proxy; - private static final String sharedPrefsFile = "shared_prefs/com.github.adamantcheese.chan_preferences.xml"; + private static final String sharedPrefsFile = "shared_prefs/" + + BuildConfig.APPLICATION_ID + + "_preferences.xml"; private static final StringSetting theme; public static final OptionsSetting layoutMode; diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ImageViewerController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ImageViewerController.java index 6f938b8184..503e7a0c22 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ImageViewerController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ImageViewerController.java @@ -39,6 +39,7 @@ import android.widget.ListView; import android.widget.Toast; +import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import com.android.volley.VolleyError; @@ -281,32 +282,14 @@ private void saveShare(boolean share, PostImage postImage) { ImageSaveTask task = new ImageSaveTask(loadable, postImage); task.setShare(share); if (ChanSettings.saveBoardFolder.get()) { - String subFolderName = - presenter.getLoadable().site.name() + - File.separator + - presenter.getLoadable().boardCode; + String subFolderName; + if (ChanSettings.saveThreadFolder.get()) { - //save to op no appended with the first 50 characters of the subject - //should be unique and perfectly understandable title wise - // - //if we're saving from the catalog, find the post for the image and use its attributes to keep everything consistent - //as the loadable is for the catalog and doesn't have the right info - subFolderName = subFolderName + - File.separator + - (presenter.getLoadable().no == 0 ? - imageViewerCallback.getPostForPostImage(postImage).no : - presenter.getLoadable().no) + - "_"; - String fixedSubFolderName = StringUtils.dirNameRemoveBadCharacters(subFolderName); - - String tempTitle = (presenter.getLoadable().no == 0 ? - PostHelper.getTitle(imageViewerCallback.getPostForPostImage(postImage), null) : - presenter.getLoadable().title); - - String fixedTitle = StringUtils.fileNameRemoveBadCharacters(tempTitle); - - subFolderName = fixedSubFolderName + - fixedTitle.substring(0, Math.min(fixedTitle.length(), 50)); + subFolderName = appendAdditionalSubDirectories(postImage); + } else { + subFolderName = presenter.getLoadable().site.name() + + File.separator + + presenter.getLoadable().boardCode; } task.setSubFolder(subFolderName); @@ -324,6 +307,41 @@ private void saveShare(boolean share, PostImage postImage) { } } + @NonNull + private String appendAdditionalSubDirectories(PostImage postImage) { + // save to op no appended with the first 50 characters of the subject + // should be unique and perfectly understandable title wise + // + // if we're saving from the catalog, find the post for the image and use its attributes + // to keep everything consistent as the loadable is for the catalog and doesn't have + // the right info + + String siteName = presenter.getLoadable().site.name(); + + int postNoString = presenter.getLoadable().no == 0 + ? imageViewerCallback.getPostForPostImage(postImage).no + : presenter.getLoadable().no; + + String sanitizedSubFolderName = StringUtils.dirNameRemoveBadCharacters(siteName) + + File.separator + + StringUtils.dirNameRemoveBadCharacters(presenter.getLoadable().boardCode) + + File.separator + + postNoString + + "_"; + + String tempTitle = (presenter.getLoadable().no == 0 + ? PostHelper.getTitle(imageViewerCallback.getPostForPostImage(postImage), null) + : presenter.getLoadable().title); + + String sanitizedFileName = StringUtils.dirNameRemoveBadCharacters(tempTitle); + String truncatedFileName = sanitizedFileName.substring( + 0, + Math.min(sanitizedFileName.length(), 50) + ); + + return sanitizedSubFolderName + truncatedFileName; + } + @Override public boolean onBack() { showSystemUI(); diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java index 90158e123d..380ad9d1b4 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java @@ -57,7 +57,7 @@ public class MediaSettingsController extends SettingsController - implements MediaSettingsControllerPresenter.MediaSettingsControllerCallbacks { + implements MediaSettingsControllerCallbacks { private static final String TAG = "MediaSettingsController"; // Special setting views @@ -448,37 +448,21 @@ public void onCopyDirectoryEnded( if (!result) { showToast(context.getString(R.string.media_settings_couldnot_copy_files), Toast.LENGTH_LONG); } else { - showDeleteOldDirectoryDialog(oldBaseDirectory); + showDeleteOldFilesDialog(oldBaseDirectory); showToast(context.getString(R.string.media_settings_files_copied), Toast.LENGTH_LONG); } } - private void showDeleteOldDirectoryDialog( + private void showDeleteOldFilesDialog( @NonNull AbstractFile oldBaseDirectory ) { AlertDialog alertDialog = new AlertDialog.Builder(context) - .setTitle(context.getString(R.string.media_settings_would_you_life_to_delete_old_dir)) + .setTitle(context.getString(R.string.media_settings_would_you_like_to_delete_file_in_old_dir)) .setMessage(context.getString(R.string.media_settings_file_have_been_copied)) .setPositiveButton( context.getString(R.string.media_settings_delete_button_name), - (dialog, which) -> { - if (!fileManager.delete(oldBaseDirectory)) { - showToast( - context.getString(R.string.media_settings_couldnot_delete_old_dir), - Toast.LENGTH_LONG - ); - return; - } - - if (oldBaseDirectory instanceof ExternalFile) { - forgetPreviousExternalBaseDirectory(oldBaseDirectory); - } - - showToast( - context.getString(R.string.media_settings_old_dir_deleted), - Toast.LENGTH_LONG - ); - }) + (dialog, which) -> onDeleteOldFilesClicked(oldBaseDirectory) + ) .setNegativeButton( context.getString(R.string.media_settings_do_not_delete), (dialog, which) -> { @@ -493,6 +477,25 @@ private void showDeleteOldDirectoryDialog( alertDialog.show(); } + private void onDeleteOldFilesClicked(@NonNull AbstractFile oldBaseDirectory) { + if (!fileManager.deleteContent(oldBaseDirectory)) { + String message = + context.getString(R.string.media_settings_couldnot_delete_files_in_old_dir); + + showToast(message, Toast.LENGTH_LONG); + return; + } + + if (oldBaseDirectory instanceof ExternalFile) { + forgetPreviousExternalBaseDirectory(oldBaseDirectory); + } + + showToast( + context.getString(R.string.media_settings_old_dir_deleted), + Toast.LENGTH_LONG + ); + } + @Override public void updateLoadingViewText(@NotNull String text) { BackgroundUtils.ensureMainThread(); diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsControllerCallbacks.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsControllerCallbacks.kt new file mode 100644 index 0000000000..c049d4a53a --- /dev/null +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsControllerCallbacks.kt @@ -0,0 +1,34 @@ +package com.github.adamantcheese.chan.ui.controller + +import android.widget.Toast +import com.github.k1rakishou.fsaf.file.AbstractFile + +interface MediaSettingsControllerCallbacks { + fun showToast(message: String, length: Int = Toast.LENGTH_SHORT) + fun updateLocalThreadsLocation(newLocation: String) + + fun askUserIfTheyWantToMoveOldThreadsToTheNewDirectory( + oldBaseDirectory: AbstractFile, + newBaseDirectory: AbstractFile + ) + + fun askUserIfTheyWantToMoveOldSavedFilesToTheNewDirectory( + oldBaseDirectory: AbstractFile, + newBaseDirectory: AbstractFile + ) + + fun updateLoadingViewText(text: String) + fun updateSaveLocationViewText(newLocation: String) + + fun showCopyFilesDialog( + filesCount: Int, + oldBaseDirectory: AbstractFile, + newBaseDirectory: AbstractFile + ) + + fun onCopyDirectoryEnded( + oldBaseDirectory: AbstractFile, + newBaseDirectory: AbstractFile, + result: Boolean + ) +} \ No newline at end of file diff --git a/Kuroba/app/src/main/res/values/strings.xml b/Kuroba/app/src/main/res/values/strings.xml index 72fe3664a9..2a3b407a1f 100644 --- a/Kuroba/app/src/main/res/values/strings.xml +++ b/Kuroba/app/src/main/res/values/strings.xml @@ -723,10 +723,10 @@ Don't have a 4chan Pass?
Could not create base local threads directory Could not copy one directory\'s file into another one Successfully copied files - Would you like to delete old directory? - Files have been copied and now exist in two directories. You may want to remove old directory since you won\'t need it anymore + Would you like to delete files in the old directory? + Files have been copied and now exist in two directories. You may want to remove files in the old directory since you won\'t need them anymore Delete - Couldn\'t delete old directory + Couldn\'t delete files in the old directory Successfully deleted old directory Do not Copy files @@ -746,4 +746,5 @@ Don't have a 4chan Pass?
New saved files base directory is probably not registered No files to copy Copying file %1$d out of %2$d + You are trying to move files in the same directory. The directory was changed but the files were not touched. From ff2d6c15a157b1622450f726cceaae80e7a28dd0 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 27 Oct 2019 19:30:31 +0300 Subject: [PATCH 181/184] (#172) Fix ImageLoaderV2 setting bitmap into a request field (haha) Additional active local threads checks before allowing the user to change base dir for local threads Some strings that were forgotten about were moved to resources --- .../chan/core/image/ImageLoaderV2.java | 2 +- .../chan/core/manager/ThreadSaveManager.java | 21 +++--- .../controller/MediaSettingsController.java | 71 ++++++++++++++++--- .../MediaSettingsControllerCallbacks.java | 37 ++++++++++ .../MediaSettingsControllerCallbacks.kt | 34 --------- Kuroba/app/src/main/res/values/strings.xml | 21 +++--- 6 files changed, 123 insertions(+), 63 deletions(-) create mode 100644 Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsControllerCallbacks.java delete mode 100644 Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsControllerCallbacks.kt diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/image/ImageLoaderV2.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/image/ImageLoaderV2.java index aa301164b6..4f72e59ebc 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/image/ImageLoaderV2.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/image/ImageLoaderV2.java @@ -210,7 +210,7 @@ public ImageLoader.ImageContainer getFromDisk( bitmapField.setAccessible(true); urlField.setAccessible(true); bitmapField.set(finalContainer, bitmap); - urlField.set(finalContainer, bitmap); + urlField.set(finalContainer, imageOnDiskFile.getFullPath()); if (imageListener != null) { imageListener.onResponse(finalContainer, true); diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/ThreadSaveManager.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/ThreadSaveManager.java index 2ba6c21f11..40e9fc55ea 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/ThreadSaveManager.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/manager/ThreadSaveManager.java @@ -265,11 +265,23 @@ public boolean enqueueThreadToSave( } } + Logger.d(TAG, "Enqueued new download request for loadable " + loadableToString(loadable)); + // Enqueue the download workerQueue.onNext(loadable); return true; } + public boolean isThereAtLeastOneActiveDownload() { + boolean hasActiveDownloads = false; + + synchronized (activeDownloads) { + hasActiveDownloads = !activeDownloads.isEmpty(); + } + + return hasActiveDownloads; + } + /** * Cancels all downloads */ @@ -1061,15 +1073,6 @@ private void downloadImage( return; } - if (!isCurrentDownloadRunning(loadable)) { - if (isCurrentDownloadStopped(loadable)) { - Logger.d(TAG, "File downloading with name " + filename + " has been stopped"); - } else { - Logger.d(TAG, "File downloading with name " + filename + " has been canceled"); - } - return; - } - AbstractFile imageFile = threadSaveDirImages .clone(new FileSegment(filename)); diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java index 380ad9d1b4..5e59fa0660 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsController.java @@ -25,6 +25,7 @@ import com.github.adamantcheese.chan.R; import com.github.adamantcheese.chan.core.database.DatabaseManager; +import com.github.adamantcheese.chan.core.manager.ThreadSaveManager; import com.github.adamantcheese.chan.core.presenter.MediaSettingsControllerPresenter; import com.github.adamantcheese.chan.core.settings.ChanSettings; import com.github.adamantcheese.chan.ui.settings.BooleanSettingView; @@ -79,6 +80,8 @@ public class MediaSettingsController FileChooser fileChooser; @Inject DatabaseManager databaseManager; + @Inject + ThreadSaveManager threadSaveManager; public MediaSettingsController(Context context) { super(context); @@ -261,10 +264,19 @@ private void setupLocalThreadLocationSetting(SettingsGroup media) { } private void showStopAllDownloadingThreadsDialog(long downloadingThreadsCount) { + String title = context.getString( + R.string.media_settings_there_are_active_downloads, + downloadingThreadsCount + ); + String message = context.getString(R.string.media_settings_you_have_to_stop_all_downloads); + new AlertDialog.Builder(context) - .setTitle("There are " + downloadingThreadsCount + " threads being downloaded") - .setMessage("You have to stop all the threads that are being downloaded before changing local threads base directory!") - .setPositiveButton("OK", ((dialog, which) -> dialog.dismiss())) + .setTitle(title) + .setMessage(message) + .setPositiveButton( + context.getString(R.string.media_settings_ok), + ((dialog, which) -> dialog.dismiss()) + ) .create() .show(); } @@ -287,13 +299,44 @@ private void showUseSAFOrOldAPIForLocalThreadsLocationDialog() { return; } + boolean areThereActiveDownloads = threadSaveManager.isThereAtLeastOneActiveDownload(); + if (areThereActiveDownloads) { + showSomeDownloadsAreStillBeingProcessed(); + return; + } + + int positiveButtonTextId = R.string.media_settings_use_saf_dialog_positive_button_text; + AlertDialog alertDialog = new AlertDialog.Builder(context) - .setTitle(R.string.use_saf_for_local_threads_location_dialog_title) - .setMessage(R.string.use_saf_for_local_threads_location_dialog_message) - .setPositiveButton(R.string.use_saf_dialog_positive_button_text, (dialog, which) -> { + .setTitle(R.string.media_settings_use_saf_for_local_threads_location_dialog_title) + .setMessage(R.string.media_settings_use_saf_for_local_threads_location_dialog_message) + .setPositiveButton(positiveButtonTextId, (dialog, which) -> { presenter.onLocalThreadsLocationUseSAFClicked(); }) - .setNegativeButton(R.string.use_saf_dialog_negative_button_text, (dialog, which) -> { + .setNegativeButton(R.string.media_settings_use_saf_dialog_negative_button_text, (dialog, which) -> { + onLocalThreadsLocationUseOldApiClicked(); + dialog.dismiss(); + }) + .create(); + + alertDialog.show(); + } + + private void showSomeDownloadsAreStillBeingProcessed() { + String title = + context.getString(R.string.media_settings_some_thread_downloads_are_still_processed); + String message = + context.getString(R.string.media_settings_do_not_terminate_the_app_manually); + int positiveButtonTextId = R.string.media_settings_use_saf_dialog_positive_button_text; + int negativeButtonTextId = R.string.media_settings_use_saf_dialog_negative_button_text; + + AlertDialog alertDialog = new AlertDialog.Builder(context) + .setTitle(title) + .setMessage(message) + .setPositiveButton(positiveButtonTextId, (dialog, which) -> { + presenter.onLocalThreadsLocationUseSAFClicked(); + }) + .setNegativeButton(negativeButtonTextId, (dialog, which) -> { onLocalThreadsLocationUseOldApiClicked(); dialog.dismiss(); }) @@ -342,12 +385,12 @@ private String getSaveLocation() { private void showUseSAFOrOldAPIForSaveLocationDialog() { AlertDialog alertDialog = new AlertDialog.Builder(context) - .setTitle(R.string.use_saf_for_save_location_dialog_title) - .setMessage(R.string.use_saf_for_save_location_dialog_message) - .setPositiveButton(R.string.use_saf_dialog_positive_button_text, (dialog, which) -> { + .setTitle(R.string.media_settings_use_saf_for_save_location_dialog_title) + .setMessage(R.string.media_settings_use_saf_for_save_location_dialog_message) + .setPositiveButton(R.string.media_settings_use_saf_dialog_positive_button_text, (dialog, which) -> { presenter.onSaveLocationUseSAFClicked(); }) - .setNegativeButton(R.string.use_saf_dialog_negative_button_text, (dialog, which) -> { + .setNegativeButton(R.string.media_settings_use_saf_dialog_negative_button_text, (dialog, which) -> { onSaveLocationUseOldApiClicked(); dialog.dismiss(); }) @@ -517,6 +560,12 @@ public void showToast(@NotNull String message, int length) { Toast.makeText(context, message, length).show(); } + @Override + public void showToast(String message) { + BackgroundUtils.ensureMainThread(); + Toast.makeText(context, message, Toast.LENGTH_SHORT).show(); + } + @Override public void updateLocalThreadsLocation(@NotNull String newLocation) { BackgroundUtils.ensureMainThread(); diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsControllerCallbacks.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsControllerCallbacks.java new file mode 100644 index 0000000000..82b7fbe702 --- /dev/null +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsControllerCallbacks.java @@ -0,0 +1,37 @@ +package com.github.adamantcheese.chan.ui.controller; + +import com.github.k1rakishou.fsaf.file.AbstractFile; + +public interface MediaSettingsControllerCallbacks { + + void showToast(String message, int length); + void showToast(String message); + + void updateLocalThreadsLocation(String newLocation); + + void askUserIfTheyWantToMoveOldThreadsToTheNewDirectory( + AbstractFile oldBaseDirectory, + AbstractFile newBaseDirectory + ); + + void askUserIfTheyWantToMoveOldSavedFilesToTheNewDirectory( + AbstractFile oldBaseDirectory, + AbstractFile newBaseDirectory + ); + + void updateLoadingViewText(String text); + void updateSaveLocationViewText(String newLocation); + + void showCopyFilesDialog( + int filesCount, + AbstractFile oldBaseDirectory, + AbstractFile newBaseDirectory + ); + + void onCopyDirectoryEnded( + AbstractFile oldBaseDirectory, + AbstractFile newBaseDirectory, + boolean result + ); + +} diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsControllerCallbacks.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsControllerCallbacks.kt deleted file mode 100644 index c049d4a53a..0000000000 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/MediaSettingsControllerCallbacks.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.github.adamantcheese.chan.ui.controller - -import android.widget.Toast -import com.github.k1rakishou.fsaf.file.AbstractFile - -interface MediaSettingsControllerCallbacks { - fun showToast(message: String, length: Int = Toast.LENGTH_SHORT) - fun updateLocalThreadsLocation(newLocation: String) - - fun askUserIfTheyWantToMoveOldThreadsToTheNewDirectory( - oldBaseDirectory: AbstractFile, - newBaseDirectory: AbstractFile - ) - - fun askUserIfTheyWantToMoveOldSavedFilesToTheNewDirectory( - oldBaseDirectory: AbstractFile, - newBaseDirectory: AbstractFile - ) - - fun updateLoadingViewText(text: String) - fun updateSaveLocationViewText(newLocation: String) - - fun showCopyFilesDialog( - filesCount: Int, - oldBaseDirectory: AbstractFile, - newBaseDirectory: AbstractFile - ) - - fun onCopyDirectoryEnded( - oldBaseDirectory: AbstractFile, - newBaseDirectory: AbstractFile, - result: Boolean - ) -} \ No newline at end of file diff --git a/Kuroba/app/src/main/res/values/strings.xml b/Kuroba/app/src/main/res/values/strings.xml index 2a3b407a1f..b6e6b1200f 100644 --- a/Kuroba/app/src/main/res/values/strings.xml +++ b/Kuroba/app/src/main/res/values/strings.xml @@ -707,20 +707,20 @@ Don't have a 4chan Pass?
Only posts quoting you - Use the new Storage Access Framework API to choose images download directory? - Use the new Storage Access Framework API to choose local threads download directory? - If you choose to use the SAF you will be able to choose the sd-card (or even something like Google Drive) as a place to store downloaded images - If you choose to use the SAF you will be able to choose the sd-card (or even something like Google Drive) as a place to store local threads images - Use SAF API - Use old API Overwrite existing file or create a new one? Overwrite Create new - Local threads location - Local threads location]]> Base local threads directory does not exist (or it was deleted). You need to manually set it again in Media settings. Could not save one or more images Could not create base local threads directory + + Use the new Storage Access Framework API to choose images download directory? + Use the new Storage Access Framework API to choose local threads download directory? + If you choose to use the SAF you will be able to choose the sd-card (or even something like Google Drive) as a place to store downloaded images + If you choose to use the SAF you will be able to choose the sd-card (or even something like Google Drive) as a place to store local threads images + Use SAF API + Use old API + Local threads location Could not copy one directory\'s file into another one Successfully copied files Would you like to delete files in the old directory? @@ -747,4 +747,9 @@ Don't have a 4chan Pass?
No files to copy Copying file %1$d out of %2$d You are trying to move files in the same directory. The directory was changed but the files were not touched. + There are %1$d threads being downloaded + You have to stop all the threads that are being downloaded before changing local threads base directory! + OK + Some thread downloads are still being processed + You may have to wait for couple of minutes until all active downloads are complete. DO NOT TERMINATE THE APP MANUALLY!!! Because this will break your local threads. From 5fe98c249e2ec04591094d39cbb46976613f1cca Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 27 Oct 2019 19:58:16 +0300 Subject: [PATCH 182/184] (#172) Add a warning alert dialog that will be shown when the user attempts to export a directory path that is located in a directory using SAF --- .../ImportExportSettingsController.java | 62 ++++++++++++++++++- Kuroba/app/src/main/res/values/strings.xml | 5 ++ 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ImportExportSettingsController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ImportExportSettingsController.java index 00106d440a..5b27b77ed1 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ImportExportSettingsController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ImportExportSettingsController.java @@ -28,6 +28,7 @@ import com.github.adamantcheese.chan.StartActivity; import com.github.adamantcheese.chan.core.presenter.ImportExportSettingsPresenter; import com.github.adamantcheese.chan.core.repository.ImportExportRepository; +import com.github.adamantcheese.chan.core.settings.ChanSettings; import com.github.adamantcheese.chan.ui.settings.LinkSettingView; import com.github.adamantcheese.chan.ui.settings.SettingsController; import com.github.adamantcheese.chan.ui.settings.SettingsGroup; @@ -120,19 +121,74 @@ private void populatePreferences() { } } + private void onExportClicked() { + boolean localThreadsLocationIsSAFBacked = !ChanSettings.localThreadsLocationUri.get().isEmpty(); + boolean savedFilesLocationIsSAFBacked = !ChanSettings.saveLocationUri.get().isEmpty(); + + if (localThreadsLocationIsSAFBacked || savedFilesLocationIsSAFBacked) { + showSomeBaseDirectoriesWillBeResetToDefaultDialog( + localThreadsLocationIsSAFBacked, + savedFilesLocationIsSAFBacked + ); + return; + } + + showCreateNewOrOverwriteDialog(); + } + + private void showSomeBaseDirectoriesWillBeResetToDefaultDialog( + boolean localThreadsLocationIsSAFBacked, + boolean savedFilesLocationIsSAFBacked + ) { + if (!localThreadsLocationIsSAFBacked && !savedFilesLocationIsSAFBacked) { + throw new IllegalStateException("Both variables are false, wtf?"); + } + + String localThreadsString = localThreadsLocationIsSAFBacked + ? context.getString(R.string.import_or_export_warning_local_threads_base_dir) + : ""; + String andString = localThreadsLocationIsSAFBacked && savedFilesLocationIsSAFBacked + ? context.getString(R.string.import_or_export_warning_and) + : ""; + String savedFilesString = savedFilesLocationIsSAFBacked + ? context.getString(R.string.import_or_export_warning_saved_files_base_dir) + : ""; + + String message = context.getString( + R.string.import_or_export_warning_super_long_message, + localThreadsString, + andString, + savedFilesString + ); + + AlertDialog alertDialog = new AlertDialog.Builder(context) + .setTitle(context.getString(R.string.import_or_export_warning)) + .setMessage(message) + .setPositiveButton(R.string.media_settings_ok, (dialog, which) -> { + dialog.dismiss(); + showCreateNewOrOverwriteDialog(); + }) + .create(); + + alertDialog.show(); + } + /** * SAF is kinda retarded so it cannot be used to overwrite a file that already exist on the disk * (or at some network location). When trying to do so, a new file with appended "(1)" at the * end will appear. That's why there are two methods (one for overwriting an existing file and * the other one for creating a new file) instead of one that does everything. * */ - private void onExportClicked() { + private void showCreateNewOrOverwriteDialog() { + int positiveButtonId = R.string.import_or_export_dialog_positive_button_text; + int negativeButtonId = R.string.import_or_export_dialog_negative_button_text; + AlertDialog alertDialog = new AlertDialog.Builder(context) .setTitle(R.string.import_or_export_dialog_title) - .setPositiveButton(R.string.import_or_export_dialog_positive_button_text, (dialog, which) -> { + .setPositiveButton(positiveButtonId, (dialog, which) -> { overwriteExisting(); }) - .setNegativeButton(R.string.import_or_export_dialog_negative_button_text, (dialog, which) -> { + .setNegativeButton(negativeButtonId, (dialog, which) -> { createNew(); }) .create(); diff --git a/Kuroba/app/src/main/res/values/strings.xml b/Kuroba/app/src/main/res/values/strings.xml index b6e6b1200f..98597a8b65 100644 --- a/Kuroba/app/src/main/res/values/strings.xml +++ b/Kuroba/app/src/main/res/values/strings.xml @@ -710,6 +710,11 @@ Don't have a 4chan Pass?
Overwrite existing file or create a new one? Overwrite Create new + Warning! + You have %1$s %2$s %3$s set to be located in a directory that uses Storage Access Framework. Such directory locations cannot be exported into the settings file because SAF is fucking retarded (Basically if you try to export them and then use on another phone or if you delete the app then install it and import those settings the directory locations they are pointing to WILL NOT BE VALID ANYMORE AND THE APP WILL CRASH). Thus THEY WILL BE RESET TO DEFAULT in the exported settings file! So once you import them you will have to manually set back those locations in the Media Settings (so that the app can work with them). Sorry for such an inconvenience but there is nothing we can do. \n\nBlame Google for forcing this retarded fucking shit. + \"local threads base directory\" + and + \"saved files base directory\" Base local threads directory does not exist (or it was deleted). You need to manually set it again in Media settings. Could not save one or more images Could not create base local threads directory From 4dba625ab996abb978e15180fd9048ec3a97a348 Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 27 Oct 2019 20:04:53 +0300 Subject: [PATCH 183/184] (#172) Remove merge conflict marker from travis.yml --- .travis.yml | 1 - .../chan/ui/controller/ImportExportSettingsController.java | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1204d16e33..99fd6e5bf3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,3 @@ -<<<<<<< HEAD sudo: false language: diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ImportExportSettingsController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ImportExportSettingsController.java index 5b27b77ed1..2b1ec8a423 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ImportExportSettingsController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ImportExportSettingsController.java @@ -185,7 +185,7 @@ private void showCreateNewOrOverwriteDialog() { AlertDialog alertDialog = new AlertDialog.Builder(context) .setTitle(R.string.import_or_export_dialog_title) - .setPositiveButton(positiveButtonId, (dialog, which) -> { + .setPositiveButton(positiveButtonId, (dialog, which) -> { overwriteExisting(); }) .setNegativeButton(negativeButtonId, (dialog, which) -> { From 8e908500e4d900da68d2de43ac0e4cb51d37ffcc Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sun, 27 Oct 2019 21:07:04 +0300 Subject: [PATCH 184/184] (#172) Set the CURRENT_EXPORT_SETTINGS_VERSION to what it should be (5) --- .../chan/core/repository/ImportExportRepository.kt | 2 +- .../chan/ui/controller/ImportExportSettingsController.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/ImportExportRepository.kt b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/ImportExportRepository.kt index b31414c41b..ac9a48cbf7 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/ImportExportRepository.kt +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/core/repository/ImportExportRepository.kt @@ -607,6 +607,6 @@ constructor( // Don't forget to change this when changing any of the Export models. // Also, don't forget to handle the change in the onUpgrade or onDowngrade methods - const val CURRENT_EXPORT_SETTINGS_VERSION = 4 + const val CURRENT_EXPORT_SETTINGS_VERSION = 5 } } diff --git a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ImportExportSettingsController.java b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ImportExportSettingsController.java index 2b1ec8a423..5b27b77ed1 100644 --- a/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ImportExportSettingsController.java +++ b/Kuroba/app/src/main/java/com/github/adamantcheese/chan/ui/controller/ImportExportSettingsController.java @@ -185,7 +185,7 @@ private void showCreateNewOrOverwriteDialog() { AlertDialog alertDialog = new AlertDialog.Builder(context) .setTitle(R.string.import_or_export_dialog_title) - .setPositiveButton(positiveButtonId, (dialog, which) -> { + .setPositiveButton(positiveButtonId, (dialog, which) -> { overwriteExisting(); }) .setNegativeButton(negativeButtonId, (dialog, which) -> {