diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 1cfcaa79..5ac91667 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -21,7 +21,4 @@ Description: What this is about, what happens and what is expected to happen. What needs to be done for it to happen. If a crash is happening a log is needed. Screenshots or demonstration videos are always helpful too. - - About logging: - https://gsantner.net/android-contribution-guide/?packageid=com.github.dfa.diaspora_android&name=dandelion&web=https://github.com/diaspora-for-android/dandelion#logcat --> diff --git a/.github/workflows/build-android-project.yml b/.github/workflows/build-android-project.yml new file mode 100644 index 00000000..07923101 --- /dev/null +++ b/.github/workflows/build-android-project.yml @@ -0,0 +1,68 @@ +############################################################################################################################## +# # Cleanup: +#const sleep = ms => () => new Promise((resolve, reject) => window.setTimeout(resolve, ms)); +#Promise.resolve() +#.then(() => { document.getElementsByClassName("details-overlay details-reset position-relative d-inline-block ")[3].children[0].click(); }) +#.then(sleep(500)) +#.then(() => { document.getElementsByClassName("dropdown-item btn-link menu-item-danger")[0].click(); }) +#.then(sleep(1000)) +#.then(() => { document.getElementsByClassName("btn btn-block btn-danger")[0].click();}); +# +# while [ 1 ] ; do sleep 4; xdotool key Up; sleep 0.1; xdotool key Return; done +############################################################################################################################## + +name: "CI" + +on: [push, pull_request] + +jobs: + build: + if: "!contains(github.event.head_commit.message, 'ci skip') && (!contains(github.event_name, 'pull_request') || (contains(github.event_name, 'pull_request') && github.event.pull_request.head.repo.full_name != github.repository))" + runs-on: ubuntu-latest + steps: + + - name: "Checkout: Code" + uses: actions/checkout@v2 + + + - name: "Checkout: Code (PR)" + uses: actions/checkout@v2 + if: "contains(github.event_name, 'pull_request')" + with: + ref: ${{github.event.pull_request.head.ref}} + repository: ${{github.event.pull_request.head.repo.full_name}} + + - name: "Setup: Java" + uses: actions/setup-java@v1 + with: + java-version: 1.8 + + - name: "Cache: Gradle" + uses: actions/cache@v2 + with: + path: | + ~/.gradle + .gradle + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('gradle/wrapper/gradle-wrapper.*') }} + + - name: "Build: Project with make" + run: make clean all + + - name: "Build: List dist files" + if: always() + run: find dist -type f -maxdepth 2 + + - name: "Artifacts: All" + if: always() + uses: actions/upload-artifact@v2.2.1 + with: + name: "all" + path: dist + retention-days: 5 + + - name: "Artifacts: Android APK" + uses: actions/upload-artifact@v2.2.1 + with: + name: "android-apk" + path: | + dist/*.apk diff --git a/.gitignore b/.gitignore index 6aa6839b..59057232 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ tmp/ ### Gradle ### .gradle build/ +dist/ gradle-app.setting # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b4204f72..00000000 --- a/.travis.yml +++ /dev/null @@ -1,48 +0,0 @@ -language: android -jdk: oraclejdk8 - -before_cache: - # Do not cache a few Gradle files/directories (see https://docs.travis-ci.com/user/languages/java/#Caching) - - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock - - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ - -cache: - directories: - # Android SDK - - $HOME/android-sdk-dl - - $HOME/android-sdk - - # Gradle dependencies - - $HOME/.gradle/caches/ - - $HOME/.gradle/wrapper/ - - # Android build cache (see http://tools.android.com/tech-docs/build-cache) - - $HOME/.android/build-cache - -install: - # Download and unzip the Android SDK tools (if not already there thanks to the cache mechanism) - # Latest version available here: https://developer.android.com/studio/index.html#downloads - - if test ! -e $HOME/android-sdk-dl/sdk-tools.zip ; then curl https://dl.google.com/android/repository/sdk-tools-linux-3859397.zip > $HOME/android-sdk-dl/sdk-tools.zip ; fi - - unzip -qq -n $HOME/android-sdk-dl/sdk-tools.zip -d $HOME/android-sdk - - # Install or update Android SDK components (will not do anything if already up to date thanks to the cache mechanism) - - echo y | $HOME/android-sdk/tools/bin/sdkmanager 'tools' > /dev/null - - echo y | $HOME/android-sdk/tools/bin/sdkmanager 'platform-tools' > /dev/null - - echo y | $HOME/android-sdk/tools/bin/sdkmanager 'build-tools;26.0.2' > /dev/null - - echo y | $HOME/android-sdk/tools/bin/sdkmanager 'platforms;android-27' > /dev/null - - echo y | $HOME/android-sdk/tools/bin/sdkmanager 'extras;google;m2repository' > /dev/null - -branches: - except: - - gh-pages - - l10n_master - - crowdin - -env: - global: - - ANDROID_HOME=$HOME/android-sdk - matrix: - - TASK="clean lintFlavorDefaultDebug --stacktrace" - - TASK="clean build check -x lint --stacktrace" - -script: "./gradlew --no-daemon --parallel $TASK" diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e8072e6..a5861d19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,36 @@ +### Recent changes +- See [Discussions](https://github.com/gsantner/dandelion/discussions), [Issues](https://github.com/gsantner/dandelion/issues) and [Project page](https://github.com/gsantner/dandelion#readme) to see what is going on. + +### v1.4.0 +- Add seconds to 'save picture' date format +- Updated translations +- Added german F-Droid description translation +- Update to Android SDK 29 + +### v1.3.0 +- Add option to open youtube links external/in YouTube app (optional) +- Pull to refresh + +### v1.2.3 +**Improved:** +- More supported languages, more complete translations! + +### v1.2.1 +**App release: dandelior** +- Added an (rebranded) flavor of dandelion: dandelior +- Only differenties in use are other (black) icon and AMOLED colors by default enabled +- Already available on F-Droid + +**New features:** +- All new Aspects and Tags, using a searchable dialog + +**Fixed:** +- Sometimes the Stream went white, which is due an still (>3 years) unfixed Android Support library bug. It should not occur very often anymore due less use of fragments. + +**Improved:** +- Various small tweaks +- Updated translation files + ### v1.1.3 - Improve sharing *a lot*, add support for multiple filetypes - Support for downloading GIFs ;) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 7969e4cd..16e6a136 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -1,6 +1,5 @@ -* **[Gregor Santner](http://gsantner.net)**
~° Current developer of dandelion +* **[Gregor Santner](http://github.com/gsantner)**
~° Development of dandelion * **[Paul Schaub](https://github.com/vanitasvitae)**
~° Development of dandelion * **[Martín Vukovic](martinvukovic AT protonmail DOT com)**
~° Diaspora Native WebApp * **[Gaukler Faun](https://github.com/scoute-dich)**
~° Diaspora Native WebApp additions @@ -30,3 +29,4 @@ Where: * **[Jean Lucas](jean AT 4ray DOT co)**
~° Spanish translation * **[asereze](https://github.com/asereze)**
~° Sardinian translation * **[Xosé M. Lamas](http://xmgz.eu)**
~° Galician translation +* **[massimiliano](https://framagit.org/massimiliano)**
~° Contributor diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..09b21c00 --- /dev/null +++ b/Makefile @@ -0,0 +1,99 @@ +# License of Makefile: Public Domain / CC0 +.PHONY: $(shell sed -n -e '/^$$/ { n ; /^[^ .\#][^ ]*:/ { s/:.*$$// ; p ; } ; }' $(MAKEFILE_LIST)) +.NOTPARALLEL: clean +.DEFAULT_GOAL := all + +env-%: + @: $(if ${${*}},,$(error Environment variable $* not set)) +#################################################################################### + +DIST_DIR = dist +MOVE = mv + +all: $(DIST_DIR) spellcheck lint deptree test build aapt_dump_badging + +#################################################################################### + +$(DIST_DIR): + mkdir -p ${DIST_DIR} + +ANDROID_BUILD_TOOLS := $(shell test -n "$ANDROID_SDK_ROOT" && find "${ANDROID_SDK_ROOT}/build-tools" -iname "aapt" | sort -r | head -n1 | xargs dirname) +TOOL_SPELLCHECKING_ISPELL := $(shell command -v ispell 2> /dev/null) + +FLAVOR := $(or ${FLAVOR},${FLAVOR},Atest) + +.NOTPARALLEL: gradle gradle-analyze-log +gradle: env-ANDROID_SDK_ROOT + mkdir -p $(DIST_DIR)/log/ + chmod +x gradlew + ./gradlew --no-daemon --parallel --stacktrace $A 2>&1 | tee "$(DIST_DIR)/log/gradle.log" + @echo "-----------------------------------------------------------------------------------" + +gradle-analyze-log: + mv "$(DIST_DIR)/log/gradle.log" "$(DIST_DIR)/log/gradle$A.log" + cat "$(DIST_DIR)/log/gradle$A.log" | grep "BUILD " | tail -n1 | grep -q "BUILD SUCCESSFUL in" + +adb: env-ANDROID_SDK_ROOT + "${ANDROID_SDK_ROOT}/platform-tools/adb" $A 2>&1 | tee "$(DIST_DIR)/log/adb-$L.log" + +aapt: env-ANDROID_SDK_ROOT + "${ANDROID_BUILD_TOOLS}/aapt" $A 2>&1 | grep -v 'application-label-' | tee "$(DIST_DIR)/log/aapt$L.log" + +build: + rm -f $(DIST_DIR)/*.apk + $(MAKE) A="clean assembleFlavor$(FLAVOR) -x lint" gradle + find app -type f -newermt '-300 seconds' -iname '*.apk' -not -iname '*unsigned.apk' | xargs cp -R -t $(DIST_DIR)/ + $(MAKE) A="-build" gradle-analyze-log + +lint: + rm -Rf $(DIST_DIR)/lint + mkdir -p $(DIST_DIR)/lint/ + $(MAKE) A="lintFlavorDefaultDebug" gradle + find app -type f -iname 'lint-results-*' | grep -v 'intermediates' | xargs cp -R -t $(DIST_DIR)/lint + $(MAKE) A="-lint" gradle-analyze-log + +test: + rm -Rf $(DIST_DIR)/tests + $(MAKE) A="testFlavorDefaultDebugUnitTest -x lint" gradle + mkdir -p app/build/test-results/testFlavorDefaultDebugUnitTest && echo 'PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHRlc3RzdWl0ZSBuYW1lPSJkdW1teSIgdGVzdHM9IjEiIHNraXBwZWQ9IjAiIGZhaWx1cmVzPSIwIiBlcnJvcnM9IjAiIHRpbWVzdGFtcD0iMjAyMC0xMi0wOFQwMDowMDowMCIgaG9zdG5hbWU9ImxvY2FsaG9zdCIgdGltZT0iMC4wMSI+CiAgPHByb3BlcnRpZXMvPgogIDx0ZXN0Y2FzZSBuYW1lPSJkdW1teSIgY2xhc3NuYW1lPSJkdW1teSIgdGltZT0iMC4wMSIvPgogIDxzeXN0ZW0tb3V0PjwhW0NEQVRBW11dPjwvc3lzdGVtLW91dD4KICA8c3lzdGVtLWVycj48IVtDREFUQVtdXT48L3N5c3RlbS1lcnI+CjwvdGVzdHN1aXRlPgo=' | base64 -d > 'app/build/test-results/testFlavorDefaultDebugUnitTest/TEST-dummy.xml' + find app -type d -iname 'testFlavorDefaultDebugUnitTest' | xargs cp -R -t $(DIST_DIR)/ + mv ${DIST_DIR}/testFlavorDefaultDebugUnitTest $(DIST_DIR)/tests + $(MAKE) A="-test" gradle-analyze-log + +deptree: + $(MAKE) A="app:dependencies --configuration flavor$(FLAVOR)DebugRuntimeClasspath" gradle + $(MAKE) A="-dependency-tree" gradle-analyze-log + +clean: + $(MAKE) A="clean" gradle + rm -Rf $(DIST_DIR) app/build app/flavor* .idea dist + find . -type f -iname "*.iml" -delete + $(MAKE) $(DIST_DIR) + @echo "-----------------------------------------------------------------------------------" + +install: + $(MAKE) A="install -r $(DIST_DIR)/*.apk" L="install" adb + +run: + $(MAKE) A="shell monkey -p $$(aapt dump badging $(DIST_DIR)/*.apk | grep package: | sed 's@.* name=@@' | sed 's@ .*@@' | xargs | head -n1) -c android.intent.category.LAUNCHER 1" L="run" adb + +aapt_dump_badging: + $(MAKE) A="dump badging $(DIST_DIR)/*.apk" aapt + @echo "-----------------------------------------------------------------------------------" + +spellcheck: + mkdir -p "$(DIST_DIR)/lint/" +ifndef TOOL_SPELLCHECKING_ISPELL + @echo "Tool ispell (spellcheck) not found in PATH. Spellcheck skipped." > "$(DIST_DIR)/lint/stringsxml-spellcheck.txt" +else + @echo "Use ispell for spellchecking the original values/strings.xml" + find . -iname "strings.xml" -path "*/main*/values/*" | head -n1 | xargs cat \ + | grep "@@' | sed 's@@@' | sed 's@\\n@ @g' | sed 's@\\@@g' \ + | ispell -W3 -a | grep ^\& | sed 's@[0-9]@@g' | sort | uniq | cut -d, -f1-4 \ + | sed 's@^..@- @' | column -t -s: \ + > "$(DIST_DIR)/lint/stringsxml-spellcheck.txt" + @echo "\nPotential words with bad spelling:" +endif + @cat "$(DIST_DIR)/lint/stringsxml-spellcheck.txt" + @echo "-----------------------------------------------------------------------------------" + diff --git a/NEWS.md b/NEWS.md new file mode 100644 index 00000000..6139d132 --- /dev/null +++ b/NEWS.md @@ -0,0 +1,96 @@ +# dandelion - News + +## General + +### Installation +You can install and update from [F-Droid](https://f-droid.org/repository/browse/?fdid=com.github.dfa.diaspora_android) or [GitHub](https://github.com/gsantner/dandelion/releases/latest). + +F-Droid is a store for free & open source apps. +The *.apk's available for download are signed by the F-Droid team and guaranteed to correspond to the (open source) source code of dandelion. +Generally this is the recommended way to install dandelion & keep it updated. + + +### Get informed +* Check the [project readme](https://github.com/gsantner/dandelion/tree/news#readme) for general project information. +* Check the [project news](https://github.com/gsantner/dandelion/blob/master/NEWS.md#readme) for more details on what is going on. +* Check the [project git history](https://github.com/gsantner/dandelion/commits/master) for most recent code changes. + +### The right place to ask +If you have questions or found an issue please head to the [dandelion project](https://github.com/gsantner/dandelion/issues/new/choose) and ask there. +[Search](https://github.com/gsantner/dandelion/issues?q=#js-issues-search) for same/similar and related issues/questions before, it might be already answered or resolved. + + +### Navigation +* [dandelion v1.2 - Add dandelior - Searchable Tags and Aspects](#dandelion-v12---add-dandelior---searchable-tags-and-aspects) +* [dandelion v0.1.2 - Aspekte, Pod wechseln](#dandelion-v012---aspekte-pod-wechseln) + + + + + + + + + +------------------------------------------------------------------------------------------------------------------------------------ + +------------------------------------------------------------------------------------------------------------------------------------ + +------------------------------------------------------------------------------------------------------------------------------------ + + +# dandelion\* v1.2 - Add dandelior\* - Searchable Tags and Aspects +_12. August 2018_ + +## dandelior\* is a rebranded version of dandelion\* +dandelior\* is based 100% on the same code and resources as dandelion\*. Its from the same code repository, just a different build flavor. +The main purpose of dandelior\* is the most requested feature till date - to support multiple accounts / another account at dandelion\*. + +- Added an (rebranded) flavor of dandelion: dandelior +- Only differenties in use are other (black) icon and AMOLED colors by default enabled +- Already available on F-Droid + +**New features:** +- All new Aspects and Tags, using a searchable dialog + +**Fixed:** +- Sometimes the Stream went white, which is due an still (3+ years) unfixed Android Support library bug. It should not occur very often anymore due less use of fragments. + +**Improved:** +- Various small tweaks +- Updated translation files + + + + + + + + + +------------------------------------------------------------------------------------------------------------------------------------ + +------------------------------------------------------------------------------------------------------------------------------------ + +------------------------------------------------------------------------------------------------------------------------------------ + + +# dandelion v0.1.2 - Aspekte, Pod wechseln +_05. Juni 2016_ + +In den letzten Tagen hat @gsantner viel Zeit in die inoffizielle diaspora\* Android App ([dandelion\*](https://github.com/gsantner/dandelion)) investiert. + +Dabei wurden unter anderem folgende Änderungen beigesteuert: + +- Allgemeines zur Usability +- Animationen für den Activity-Wechsel und Startup, WebView-Scroll-Top +- Podliste caching +- Aspekt-Liste und Aspekte hinzugefügt +- Verbessertes Sharing aus der App +- Material Progressbar +- Suche verbessert +- Collapsing top menu +- toolbar/actions/menu geändert, fab entfernt +- Refactoring layout & menu files, dialogs +- Überarbeitete Main,Splash,PodSelectionActivity +- Pod wechseln diff --git a/README.md b/README.md index bcdadee7..a061ee60 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -[![GitHub release](https://img.shields.io/github/tag/diaspora-for-android/dandelion.svg)](https://github.com/diaspora-for-android/dandelion/releases) -[![Build Status](https://travis-ci.org/Diaspora-for-Android/dandelion.svg?branch=master)](https://travis-ci.org/Diaspora-for-Android/dandelion) -[![Translate - with Stringlate](https://img.shields.io/badge/stringlate-translate-green.svg)](https://lonamiwebs.github.io/stringlate/translate?git=https%3A%2F%2Fgithub.com%2Fdiaspora-for-android%2Fdandelion.git&mail=gro.xobliam@@rentnasg) -[![Chat - Matrix](https://img.shields.io/badge/chat-on%20matrix-blue.svg)](https://matrix.to/#/#dandelion:matrix.org) [![Chat - FreeNode IRC](https://img.shields.io/badge/chat-on%20irc-blue.svg)](https://kiwiirc.com/client/irc.freenode.net/?nick=dandelion-anon|?##dandelion) -[![Donate Bitcoin](https://img.shields.io/badge/donate-bitcoin-orange.svg)](https://gsantner.net/supportme/?project=dandelion&source=readme) -[![Donate LiberaPay](https://img.shields.io/badge/donate-liberapay-orange.svg)](https://liberapay.com/gsantner/donate) +[![GitHub releases](https://img.shields.io/github/tag/gsantner/dandelion.svg)](https://github.com/gsantner/dandelion/releases) +[![GitHub downloads](https://img.shields.io/github/downloads/gsantner/dandelion/total.svg?logo=github&logoColor=lime)](https://github.com/gsantner/dandelion/releases) +[![Translate on Crowdin](https://img.shields.io/badge/translate-crowdin-green.svg)](https://crowdin.com/project/diaspora-for-android/invite) +[![Chat on Matrix](https://img.shields.io/badge/chat-matrix-blue.svg)](https://matrix.to/#/#dandelion:matrix.org) +[![GitHub CI](https://github.com/gsantner/dandelion/workflows/CI/badge.svg)](https://github.com/gsantner/dandelion/actions) +[![Codacy code quality](https://img.shields.io/codacy/grade/aff869c440bc48b7bd64680e97cbc453)](https://www.codacy.com/app/gsantner/dandelion) # dandelion\* @@ -41,44 +41,41 @@ dandelion\* requires access to the Internet and to external storage to be able t The project is always open for contributions and accepts pull requests. The project uses [AOSP Java Code Style](https://source.android.com/source/code-style#follow-field-naming-conventions), with one exception: private members are `_camelCase` instead of `mBigCamel`. You may use Android Studios _auto reformat feature_ before sending a PR. -Translations can be contributed on GitHub or via [E-Mail](http://gsantner.net/#contact). You can use Stringlate ([![Translate - with Stringlate](https://img.shields.io/badge/stringlate-translate-green.svg)](https://lonamiwebs.github.io/stringlate/translate?git=https%3A%2F%2Fgithub.com%2Fdiaspora-for-android%2Fdandelion.git)) to translate the project directly on your Android phone. It allows you to export as E-Mail attachement and to post on GitHub. - -Join our IRC or Matrix channel (bridged) and say hello! Don't be afraid to start talking. [![Chat - Matrix](https://img.shields.io/badge/chat-on%20matrix-blue.svg)](https://matrix.to/#/#dandelion:matrix.org) [![Chat - FreeNode IRC](https://img.shields.io/badge/chat-on%20irc-blue.svg)](https://kiwiirc.com/client/irc.freenode.net/?nick=dandelion-anon|?##dandelion) +Translations can be contributed on GitHub. You can use Stringlate ([![Translate - with Stringlate](https://img.shields.io/badge/stringlate-translate-green.svg)](https://lonamiwebs.github.io/stringlate/translate?git=https%3A%2F%2Fgithub.com%2Fgsantner%2Fdandelion.git)) to translate the project directly on your Android phone. It allows you to export as E-Mail attachement and to post on GitHub. +Join our Matrix channel and say hello! Don't be afraid to start talking. [![Chat - Matrix](https://img.shields.io/badge/chat-on%20matrix-blue.svg)](https://matrix.to/#/#dandelion:matrix.org) Note that the main project members are working on this project for free during leisure time, are mostly busy with their job/university/school, and may not react or start coding immediately. #### Resources -* Project: [Changelog](/CHANGELOG.md) | [Issues level/beginner](https://github.com/diaspora-for-android/dandelion/issues?q=is%3Aissue+is%3Aopen+label%3Alevel%2Fbeginner) | [License](/LICENSE.txt) | [CoC](/CODE_OF_CONDUCT.md) +* Project: [Changelog](/CHANGELOG.md) | [Issues level/beginner](https://github.com/gsantner/dandelion/issues?q=is%3Aissue+is%3Aopen+label%3Alevel%2Fbeginner) | [License](/LICENSE.txt) | [CoC](/CODE_OF_CONDUCT.md) * Project diaspora\* account: [dandelion00@diasp.org](https://diasp.org/people/48b78420923501341ef3782bcb452bd5) * diaspora\*: [GitHub](https://github.com/diaspora/diaspora) | [Web](https://diasporafoundation.org) | [d\* HQ account](https://pod.diaspora.software/people/7bca7c80311b01332d046c626dd55703) * App on F-Droid: [Metadata](https://gitlab.com/fdroid/fdroiddata/blob/master/metadata/com.github.dfa.diaspora_android.txt) | [Page](https://f-droid.org/packages/com.github.dfa.diaspora_android/) | [Wiki](https://f-droid.org/wiki/page/com.github.dfa.diaspora_android) | [Build log](https://f-droid.org/wiki/page/com.github.dfa.diaspora_android/lastbuild) ## Licensing -dandelion\* is released under GNU GENERAL PUBLIC LICENSE (see [LICENCE](https://github.com/Diaspora-for-Android/dandelion/blob/master/LICENSE.md)). +dandelion\* is released under GNU GENERAL PUBLIC LICENSE (see [LICENCE](https://github.com/gsantner/dandelion/blob/master/LICENSE.md)). The app is licensed GPL v3. Localization files and resources (strings\*.xml) are licensed CC0 1.0. For more licensing informations, see [`3rd party licenses`](/app/src/main/res/raw/licenses_3rd_party.md). ## Screenshots
- - - - - + + + + +
- - - - + + + +
### Notice #### Maintainers -- gsantner ([GitHub](https://github.com/gsantner), [Web](https://gsantner.net), [diaspora*](https://pod.geraspora.de/people/d1cbdd70095301341e834860008dbc6c)) - - Bitcoin: [1B9ZyYdQoY9BxMe9dRUEKaZbJWsbQqfXU5](http://gsantner.net/donate/#donate) +- gsantner ([GitHub](https://github.com/gsantner), [diaspora*](https://pod.geraspora.de/people/d1cbdd70095301341e834860008dbc6c)) - vanitasvitae ([GitHub](https://github.com/vanitasvitae), [diaspora*](https://pod.geraspora.de/people/bbd7af90fbec013213e34860008dbc6c)) - - Bitcoin: 1Ao3W6NaQv3xKppviB7RSFKjHo6PGd8RTy diff --git a/SCREENSHOTS.md b/SCREENSHOTS.md deleted file mode 100644 index 9403120d..00000000 --- a/SCREENSHOTS.md +++ /dev/null @@ -1,16 +0,0 @@ -## Screenshots - -
- - - - - -
- -
- - - - -
diff --git a/app/build.gradle b/app/build.gradle index a11ef9b9..09c8527f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,46 +6,27 @@ if (enable_plugin_kotlin) { } android { - compileSdkVersion version_setup_compileSdk - flavorDimensions "default" + buildToolsVersion rootProject.ext.version_buildTools + compileSdkVersion rootProject.ext.version_compileSdk defaultConfig { - minSdkVersion version_setup_minSdk - targetSdkVersion version_setup_targetSdk + resValue "string", "manifest_package_id", "com.github.dfa.diaspora_android" + applicationId "com.github.dfa.diaspora_android" + versionName "1.3.5" + versionCode 46 + vectorDrawables.useSupportLibrary = true + + minSdkVersion rootProject.ext.version_minSdk + targetSdkVersion rootProject.ext.version_compileSdk buildConfigField "boolean", "IS_TEST_BUILD", "false" buildConfigField "boolean", "IS_GPLAY_BUILD", "false" buildConfigField "String[]", "DETECTED_ANDROID_LOCALES", "${findUsedAndroidLocales()}" + buildConfigField "String", "BUILD_DATE", "\"${getBuildDate()}\"" buildConfigField "String", "GITHASH", "\"${getGitHash()}\"" - resValue "string", "manifest_package_id", "com.github.dfa.diaspora_android" - - applicationId "com.github.dfa.diaspora_android" - versionName "1.1.3" - versionCode 32 - - - vectorDrawables.useSupportLibrary = true - resValue 'string', 'app_name', "dandelion*" - manifestPlaceholders = [appIcon: "@drawable/ic_launcher"] - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - } - - sourceSets { - if (enable_plugin_kotlin) { - main.java.srcDirs += 'src/main/kotlin' - } + setProperty("archivesBaseName", applicationId + "-v" + versionCode + "-" + versionName) } + flavorDimensions "default" productFlavors { flavorDefault { } @@ -55,17 +36,62 @@ android { buildConfigField "boolean", "IS_GPLAY_BUILD", "true" }*/ - flavorTest { - applicationId "com.github.dfa.secondlion" - resValue 'string', 'app_name', "secondlion*" - manifestPlaceholders = [appIcon: "@drawable/ic_launcher_test"] + flavorDandelior { + applicationId "net.gsantner.dandelior" + } + + flavorAtest { + applicationId "net.gsantner.secondlion" versionCode = Integer.parseInt(new Date().format('yyMMdd')) versionName = new Date().format('yyMMdd') buildConfigField "boolean", "IS_TEST_BUILD", "true" } } + + sourceSets { + main { assets.srcDirs = ['src/main/assets'] } + if (enable_plugin_kotlin) { + main.java.srcDirs += 'src/main/kotlin' + } + main.java.srcDirs += 'thirdparty/java' + main.res.srcDirs += 'thirdparty/res' + main.assets.srcDirs += 'thirdparty/assets' + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + configurations.all { + resolutionStrategy { + eachDependency { details -> + if (details.requested.group == 'com.android.support') { + if (details.requested.name != 'multidex' && details.requested.name != 'multidex-instrumentation') { + details.useVersion "${rootProject.ext.version_library_appcompat}" + } + } + } + } + } + + packagingOptions { + exclude 'META-INF/LICENSE-LGPL-2.1.txt' + exclude 'META-INF/LICENSE-LGPL-3.txt' + exclude 'META-INF/LICENSE-W3C-TEST' + } + + compileOptions { + encoding = 'UTF-8' + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + lintOptions { - disable 'MissingTranslation' + disable 'MissingTranslation', 'InvalidPackage', 'ObsoleteLintCustomCheck', 'DefaultLocale', 'UnusedAttribute', 'VectorRaster', 'InflateParams', 'IconLocation', 'UnusedResources', 'TypographyEllipsis' + abortOnError false } } @@ -75,7 +101,7 @@ dependencies { // Jars implementation fileTree(dir: 'libs', include: ['*.jar']) - testImplementation 'junit:junit:4.12' + testImplementation 'junit:junit:4.13' // Android standard libs implementation "com.android.support:appcompat-v7:${version_library_appcompat}" @@ -83,16 +109,19 @@ dependencies { implementation "com.android.support:support-v4:${version_library_appcompat}" implementation "com.android.support:customtabs:${version_library_appcompat}" implementation "com.android.support:cardview-v7:${version_library_appcompat}" + implementation "com.android.support:preference-v7:${version_library_appcompat}" // UI libraries implementation "com.github.DASAR:ShiftColorPicker:v0.5" // Tool libraries + implementation 'commons-io:commons-io:2.6' implementation "info.guardianproject.netcipher:netcipher:${version_library_netcipher}" implementation "info.guardianproject.netcipher:netcipher-webkit:${version_library_netcipher}" + //noinspection AnnotationProcessorOnCompilePath implementation "com.jakewharton:butterknife:${version_library_butterknife}" if (enable_plugin_kotlin) { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$version_plugin_kotlin" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${version_plugin_kotlin}" } // Processors diff --git a/app/src/flavorAtest/res/drawable-anydpi-v26/ic_launcher.xml b/app/src/flavorAtest/res/drawable-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..d378acd7 --- /dev/null +++ b/app/src/flavorAtest/res/drawable-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/flavorAtest/res/drawable-anydpi-v26/ic_launcher_round.xml b/app/src/flavorAtest/res/drawable-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 00000000..d378acd7 --- /dev/null +++ b/app/src/flavorAtest/res/drawable-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable-hdpi/ic_launcher_test.png b/app/src/flavorAtest/res/drawable-hdpi/ic_launcher.png similarity index 100% rename from app/src/main/res/drawable-hdpi/ic_launcher_test.png rename to app/src/flavorAtest/res/drawable-hdpi/ic_launcher.png diff --git a/app/src/main/res/drawable-hdpi/ic_launcher_test_round.png b/app/src/flavorAtest/res/drawable-hdpi/ic_launcher_round.png similarity index 100% rename from app/src/main/res/drawable-hdpi/ic_launcher_test_round.png rename to app/src/flavorAtest/res/drawable-hdpi/ic_launcher_round.png diff --git a/app/src/flavorAtest/res/drawable-ldpi/ic_launcher.png b/app/src/flavorAtest/res/drawable-ldpi/ic_launcher.png new file mode 100644 index 00000000..b85b0c40 Binary files /dev/null and b/app/src/flavorAtest/res/drawable-ldpi/ic_launcher.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_launcher_test.png b/app/src/flavorAtest/res/drawable-mdpi/ic_launcher.png similarity index 100% rename from app/src/main/res/drawable-mdpi/ic_launcher_test.png rename to app/src/flavorAtest/res/drawable-mdpi/ic_launcher.png diff --git a/app/src/main/res/drawable-mdpi/ic_launcher_test_round.png b/app/src/flavorAtest/res/drawable-mdpi/ic_launcher_round.png similarity index 100% rename from app/src/main/res/drawable-mdpi/ic_launcher_test_round.png rename to app/src/flavorAtest/res/drawable-mdpi/ic_launcher_round.png diff --git a/app/src/main/res/drawable-xhdpi/ic_launcher_test.png b/app/src/flavorAtest/res/drawable-xhdpi/ic_launcher.png similarity index 100% rename from app/src/main/res/drawable-xhdpi/ic_launcher_test.png rename to app/src/flavorAtest/res/drawable-xhdpi/ic_launcher.png diff --git a/app/src/main/res/drawable-xhdpi/ic_launcher_test_round.png b/app/src/flavorAtest/res/drawable-xhdpi/ic_launcher_round.png similarity index 100% rename from app/src/main/res/drawable-xhdpi/ic_launcher_test_round.png rename to app/src/flavorAtest/res/drawable-xhdpi/ic_launcher_round.png diff --git a/app/src/main/res/drawable-xxhdpi/ic_launcher_test.png b/app/src/flavorAtest/res/drawable-xxhdpi/ic_launcher.png similarity index 100% rename from app/src/main/res/drawable-xxhdpi/ic_launcher_test.png rename to app/src/flavorAtest/res/drawable-xxhdpi/ic_launcher.png diff --git a/app/src/main/res/drawable-xxhdpi/ic_launcher_test_round.png b/app/src/flavorAtest/res/drawable-xxhdpi/ic_launcher_round.png similarity index 100% rename from app/src/main/res/drawable-xxhdpi/ic_launcher_test_round.png rename to app/src/flavorAtest/res/drawable-xxhdpi/ic_launcher_round.png diff --git a/app/src/main/res/drawable-xxxhdpi/ic_launcher_test.png b/app/src/flavorAtest/res/drawable-xxxhdpi/ic_launcher.png similarity index 100% rename from app/src/main/res/drawable-xxxhdpi/ic_launcher_test.png rename to app/src/flavorAtest/res/drawable-xxxhdpi/ic_launcher.png diff --git a/app/src/main/res/drawable-xxxhdpi/ic_launcher_test_round.png b/app/src/flavorAtest/res/drawable-xxxhdpi/ic_launcher_round.png similarity index 100% rename from app/src/main/res/drawable-xxxhdpi/ic_launcher_test_round.png rename to app/src/flavorAtest/res/drawable-xxxhdpi/ic_launcher_round.png diff --git a/app/src/main/res/drawable/ic_launcher_test_background.xml b/app/src/flavorAtest/res/drawable/ic_launcher_background.xml similarity index 100% rename from app/src/main/res/drawable/ic_launcher_test_background.xml rename to app/src/flavorAtest/res/drawable/ic_launcher_background.xml diff --git a/app/src/main/res/drawable/ic_launcher_test_foreground.xml b/app/src/flavorAtest/res/drawable/ic_launcher_foreground.xml similarity index 100% rename from app/src/main/res/drawable/ic_launcher_test_foreground.xml rename to app/src/flavorAtest/res/drawable/ic_launcher_foreground.xml diff --git a/app/src/main/ic_launcher_test-web.png b/app/src/flavorAtest/res/ic_launcher-web.png similarity index 100% rename from app/src/main/ic_launcher_test-web.png rename to app/src/flavorAtest/res/ic_launcher-web.png diff --git a/app/src/flavorAtest/res/values/strings-flavor.xml b/app/src/flavorAtest/res/values/strings-flavor.xml new file mode 100644 index 00000000..78eb04f7 --- /dev/null +++ b/app/src/flavorAtest/res/values/strings-flavor.xml @@ -0,0 +1,4 @@ + + + secondlion* + diff --git a/app/src/flavorDandelior/ic_launcher-web.png b/app/src/flavorDandelior/ic_launcher-web.png new file mode 100644 index 00000000..39789cb4 Binary files /dev/null and b/app/src/flavorDandelior/ic_launcher-web.png differ diff --git a/app/src/flavorDandelior/res/drawable-anydpi-v26/ic_launcher.xml b/app/src/flavorDandelior/res/drawable-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..d378acd7 --- /dev/null +++ b/app/src/flavorDandelior/res/drawable-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/flavorDandelior/res/drawable-anydpi-v26/ic_launcher_round.xml b/app/src/flavorDandelior/res/drawable-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 00000000..d378acd7 --- /dev/null +++ b/app/src/flavorDandelior/res/drawable-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/flavorDandelior/res/drawable-hdpi/ic_launcher.png b/app/src/flavorDandelior/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 00000000..07dad0c9 Binary files /dev/null and b/app/src/flavorDandelior/res/drawable-hdpi/ic_launcher.png differ diff --git a/app/src/flavorDandelior/res/drawable-hdpi/ic_launcher_round.png b/app/src/flavorDandelior/res/drawable-hdpi/ic_launcher_round.png new file mode 100644 index 00000000..07dad0c9 Binary files /dev/null and b/app/src/flavorDandelior/res/drawable-hdpi/ic_launcher_round.png differ diff --git a/app/src/flavorDandelior/res/drawable-ldpi/ic_launcher.png b/app/src/flavorDandelior/res/drawable-ldpi/ic_launcher.png new file mode 100644 index 00000000..66c10bba Binary files /dev/null and b/app/src/flavorDandelior/res/drawable-ldpi/ic_launcher.png differ diff --git a/app/src/flavorDandelior/res/drawable-ldpi/ic_launcher_round.png b/app/src/flavorDandelior/res/drawable-ldpi/ic_launcher_round.png new file mode 100644 index 00000000..66c10bba Binary files /dev/null and b/app/src/flavorDandelior/res/drawable-ldpi/ic_launcher_round.png differ diff --git a/app/src/flavorDandelior/res/drawable-mdpi/ic_launcher.png b/app/src/flavorDandelior/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 00000000..1288f555 Binary files /dev/null and b/app/src/flavorDandelior/res/drawable-mdpi/ic_launcher.png differ diff --git a/app/src/flavorDandelior/res/drawable-mdpi/ic_launcher_round.png b/app/src/flavorDandelior/res/drawable-mdpi/ic_launcher_round.png new file mode 100644 index 00000000..70ce8fb1 Binary files /dev/null and b/app/src/flavorDandelior/res/drawable-mdpi/ic_launcher_round.png differ diff --git a/app/src/flavorDandelior/res/drawable-xhdpi/ic_launcher.png b/app/src/flavorDandelior/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 00000000..37e164bc Binary files /dev/null and b/app/src/flavorDandelior/res/drawable-xhdpi/ic_launcher.png differ diff --git a/app/src/flavorDandelior/res/drawable-xhdpi/ic_launcher_round.png b/app/src/flavorDandelior/res/drawable-xhdpi/ic_launcher_round.png new file mode 100644 index 00000000..d9040fa3 Binary files /dev/null and b/app/src/flavorDandelior/res/drawable-xhdpi/ic_launcher_round.png differ diff --git a/app/src/flavorDandelior/res/drawable-xxhdpi/ic_launcher.png b/app/src/flavorDandelior/res/drawable-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..56e46378 Binary files /dev/null and b/app/src/flavorDandelior/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/app/src/flavorDandelior/res/drawable-xxhdpi/ic_launcher_round.png b/app/src/flavorDandelior/res/drawable-xxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..56e46378 Binary files /dev/null and b/app/src/flavorDandelior/res/drawable-xxhdpi/ic_launcher_round.png differ diff --git a/app/src/flavorDandelior/res/drawable-xxxhdpi/ic_launcher.png b/app/src/flavorDandelior/res/drawable-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..9fe1d567 Binary files /dev/null and b/app/src/flavorDandelior/res/drawable-xxxhdpi/ic_launcher.png differ diff --git a/app/src/flavorDandelior/res/drawable-xxxhdpi/ic_launcher_round.png b/app/src/flavorDandelior/res/drawable-xxxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..9fe1d567 Binary files /dev/null and b/app/src/flavorDandelior/res/drawable-xxxhdpi/ic_launcher_round.png differ diff --git a/app/src/flavorDandelior/res/drawable/ic_launcher_background.xml b/app/src/flavorDandelior/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..f280ce18 --- /dev/null +++ b/app/src/flavorDandelior/res/drawable/ic_launcher_background.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/flavorDandelior/res/drawable/ic_launcher_foreground.xml b/app/src/flavorDandelior/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 00000000..2610c60c --- /dev/null +++ b/app/src/flavorDandelior/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/flavorDandelior/res/values/strings-flavor.xml b/app/src/flavorDandelior/res/values/strings-flavor.xml new file mode 100644 index 00000000..75d07bd2 --- /dev/null +++ b/app/src/flavorDandelior/res/values/strings-flavor.xml @@ -0,0 +1,4 @@ + + + dandelior* + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 91d95680..2e1a7bd1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -13,8 +13,9 @@ - @@ -68,6 +70,7 @@ + diff --git a/app/src/main/ic_launcher_test.png b/app/src/main/ic_launcher_test.png deleted file mode 100755 index 18e3ea1d..00000000 Binary files a/app/src/main/ic_launcher_test.png and /dev/null differ diff --git a/app/src/main/java/com/github/dfa/diaspora_android/App.java b/app/src/main/java/com/github/dfa/diaspora_android/App.java index 03271637..150cb378 100644 --- a/app/src/main/java/com/github/dfa/diaspora_android/App.java +++ b/app/src/main/java/com/github/dfa/diaspora_android/App.java @@ -35,6 +35,8 @@ import com.github.dfa.diaspora_android.util.AppSettings; import com.github.dfa.diaspora_android.util.DiasporaUrlHelper; import net.gsantner.opoc.util.AdBlock; +import net.gsantner.opoc.util.ContextUtils; +import net.gsantner.opoc.util.ShareUtil; public class App extends Application { private volatile static App app; @@ -50,10 +52,18 @@ public class App extends Application { @Override public void onCreate() { super.onCreate(); + ShareUtil.setFileProviderAuthority(BuildConfig.APPLICATION_ID); app = this; final Context c = getApplicationContext(); appSettings = AppSettings.get(); + String a = new ContextUtils(this).bcstr("FLAVOR", ""); + a += "__"; + + if (appSettings.isAppFirstStart() && "flavorDandelior".equals(new ContextUtils(this).bcstr("FLAVOR", ""))) { + appSettings.setAmoledColorMode(true); + } + // Init app log AppLog.setLoggingEnabled(appSettings.isLoggingEnabled()); AppLog.setLoggingSpamEnabled(appSettings.isLoggingSpamEnabled()); diff --git a/app/src/main/java/com/github/dfa/diaspora_android/activity/AboutActivity.java b/app/src/main/java/com/github/dfa/diaspora_android/activity/AboutActivity.java index cd43fb88..0a68ee00 100644 --- a/app/src/main/java/com/github/dfa/diaspora_android/activity/AboutActivity.java +++ b/app/src/main/java/com/github/dfa/diaspora_android/activity/AboutActivity.java @@ -169,7 +169,7 @@ public class AboutActivity extends ThemedActivity if (isAdded()) { try { PackageInfo pInfo = getActivity().getPackageManager().getPackageInfo(getActivity().getPackageName(), 0); - appVersion.setText(getString(R.string.fragment_debug__app_version, pInfo.versionName + " (" + pInfo.versionCode + ")")); + appVersion.setText(getString(R.string.app_version_with_arg, pInfo.versionName + " (" + pInfo.versionCode + ")")); } catch (PackageManager.NameNotFoundException e) { e.printStackTrace(); @@ -213,8 +213,8 @@ public class AboutActivity extends ThemedActivity Intent sharingIntent = new Intent(android.content.Intent.ACTION_SEND); sharingIntent.setType("text/plain"); sharingIntent.putExtra(android.content.Intent.EXTRA_SUBJECT, getString(R.string.app_name)); - sharingIntent.putExtra(android.content.Intent.EXTRA_TEXT, getString(R.string.fragment_about__spread_the_word_share_text, getString(R.string.fragment_about__fdroid_link))); - startActivity(Intent.createChooser(sharingIntent, getResources().getString(R.string.action_share_dotdotdot))); + sharingIntent.putExtra(android.content.Intent.EXTRA_TEXT, getString(R.string.hey_checkout_dandelion_tag__appspecific, getString(R.string.fragment_about__fdroid_link))); + startActivity(Intent.createChooser(sharingIntent, getResources().getString(R.string.share_dotdotdot))); break; } } @@ -258,9 +258,9 @@ public class AboutActivity extends ThemedActivity ButterKnife.bind(this, view); accentColor = ContextUtils.get().colorToHexString(ThemeHelper.getAccentColor()); - maintainers.setTextFormatted(getString(R.string.fragment_license__maintainers_text, + maintainers.setTextFormatted(getString(R.string.this_app_is_currently_developed_and_maintained_by_witharg, ContextUtils.get().loadMarkdownForTextViewFromRaw(R.raw.maintainers, ""))); - contributors.setTextFormatted(getString(R.string.fragment_license__contributors_thank_you, + contributors.setTextFormatted(getString(R.string.thank_you_witharg, ContextUtils.get().loadMarkdownForTextViewFromRaw(R.raw.contributors, ""))); thirdPartyLibs.setTextFormatted( ContextUtils.get().loadMarkdownForTextViewFromRaw(R.raw.licenses_3rd_party, "")); @@ -341,7 +341,7 @@ public class AboutActivity extends ThemedActivity ClipboardManager clipboard = (ClipboardManager) getActivity().getSystemService(CLIPBOARD_SERVICE); ClipData clip = ClipData.newPlainText("DEBUG_LOG", AppLog.Log.getLogBuffer()); clipboard.setPrimaryClip(clip); - Toast.makeText(DebugFragment.this.getActivity(), R.string.fragment_debug__toast_log_copied, Toast.LENGTH_SHORT).show(); + Toast.makeText(DebugFragment.this.getActivity(), R.string.debug_log_copied_to_clipboard, Toast.LENGTH_SHORT).show(); } else { AppLog.d(this, "Not Added!"); } @@ -356,13 +356,13 @@ public class AboutActivity extends ThemedActivity PackageInfo pInfo = getActivity().getPackageManager().getPackageInfo(getActivity().getPackageName(), 0); AppSettings appSettings = ((App) getActivity().getApplication()).getSettings(); packageName.setText(pInfo.packageName); - appVersion.setText(getString(R.string.fragment_debug__app_version, pInfo.versionName + " (" + pInfo.versionCode + ")")); + appVersion.setText(getString(R.string.app_version_with_arg, pInfo.versionName + " (" + pInfo.versionCode + ")")); - osVersion.setText(getString(R.string.fragment_debug__android_version, Build.VERSION.RELEASE)); - deviceName.setText(getString(R.string.fragment_debug__device_name, Build.MANUFACTURER + " " + Build.MODEL)); + osVersion.setText(getString(R.string.android_version_witharg, Build.VERSION.RELEASE)); + deviceName.setText(getString(R.string.device_name_witharg, Build.MANUFACTURER + " " + Build.MODEL)); if (app.getSettings().getPod() != null) { - podDomain.setText(getString(R.string.fragment_debug__pod_profile_url, app.getSettings().getPod().getPodUrl())); - podName.setText(getString(R.string.fragment_debug__pod_profile_name, app.getSettings().getPod().getName())); + podDomain.setText(getString(R.string.pod_domain_witharg__appspecific, app.getSettings().getPod().getPodUrl())); + podName.setText(getString(R.string.pod_profile_name_witharg__appspecific, app.getSettings().getPod().getName())); } } catch (PackageManager.NameNotFoundException e) { @@ -420,11 +420,11 @@ public class AboutActivity extends ThemedActivity public CharSequence getPageTitle(int position) { switch (position) { case 0: - return getString(R.string.about_activity__title_about_app); + return getString(R.string.about); case 1: - return getString(R.string.about_activity__title_about_license); + return getString(R.string.license); case 2: - return getString(R.string.about_activity__title_debug_info); + return getString(R.string.debugging); } return null; } diff --git a/app/src/main/java/com/github/dfa/diaspora_android/activity/AspectListFragment.java b/app/src/main/java/com/github/dfa/diaspora_android/activity/AspectListFragment.java deleted file mode 100644 index 50cf66af..00000000 --- a/app/src/main/java/com/github/dfa/diaspora_android/activity/AspectListFragment.java +++ /dev/null @@ -1,208 +0,0 @@ -/* - This file is part of the dandelion*. - - dandelion* 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. - - dandelion* 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 the dandelion*. - - If not, see . - */ -package com.github.dfa.diaspora_android.activity; - -import android.content.Context; -import android.graphics.Color; -import android.graphics.PorterDuff; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v7.widget.AppCompatImageView; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.RelativeLayout; -import android.widget.TextView; - -import com.github.dfa.diaspora_android.App; -import com.github.dfa.diaspora_android.R; -import com.github.dfa.diaspora_android.data.DiasporaAspect; -import com.github.dfa.diaspora_android.listener.OnSomethingClickListener; -import com.github.dfa.diaspora_android.ui.theme.ThemedFragment; -import com.github.dfa.diaspora_android.util.AppSettings; -import com.github.dfa.diaspora_android.util.ContextUtils; -import com.github.dfa.diaspora_android.util.DiasporaUrlHelper; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import butterknife.BindView; -import butterknife.ButterKnife; - -/** - * Fragment that shows a list of the Aspects - */ -public class AspectListFragment extends ThemedFragment implements OnSomethingClickListener { - - public static final String TAG = "com.github.dfa.diaspora_android.AspectListFragment"; - - @BindView(R.id.fragment_list__recycler_view) - public RecyclerView aspectsRecyclerView; - - @BindView(R.id.fragment_list__spacer) - public View space; - - @BindView(R.id.fragment_list__root) - public RelativeLayout rootView; - - protected App app; - protected DiasporaUrlHelper urls; - - @Override - protected int getLayoutResId() { - return R.layout.recycler_list__fragment; - } - - @Override - public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - ButterKnife.bind(this, view); - app = (App) getActivity().getApplication(); - AppSettings appSettings = app.getSettings(); - urls = new DiasporaUrlHelper(appSettings); - - aspectsRecyclerView.setHasFixedSize(true); - aspectsRecyclerView.setNestedScrollingEnabled(false); - - RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(getContext()); - aspectsRecyclerView.setLayoutManager(layoutManager); - - final AspectAdapter adapter = new AspectAdapter(appSettings, this); - aspectsRecyclerView.setAdapter(adapter); - - //Set window title - getActivity().setTitle(R.string.nav_aspects); - } - - @Override - public String getFragmentTag() { - return TAG; - } - - @Override - public boolean onBackPressed() { - return false; - } - - @Override - public void onSomethingClicked(Object null1, Integer null2, String aspectId) { - ((MainActivity) getActivity()).openDiasporaUrl(urls.getAspectUrl(aspectId)); - } - - @Override - protected void applyColorToViews() { - aspectsRecyclerView.invalidate(); - if (getAppSettings().isAmoledColorMode()) { - rootView.setBackgroundColor(Color.BLACK); - space.setBackgroundColor(Color.BLACK); - } - } - - public static class AspectAdapter extends RecyclerView.Adapter { - private boolean isAmoledColorMode; - private final AppSettings appSettings; - private final DiasporaAspect[] aspectList; - private final List aspectFavsList; - private final OnSomethingClickListener aspectClickedListener; - - static class ViewHolder extends RecyclerView.ViewHolder { - @BindView(R.id.recycler_view__list_item__text) - public TextView title; - @BindView(R.id.recycler_view__list_item__favourite) - AppCompatImageView favouriteImage; - @BindView(R.id.recycler_view__list_item__root) - RelativeLayout root; - - ViewHolder(View v) { - super(v); - ButterKnife.bind(this, v); - } - } - - - AspectAdapter(AppSettings appSettings, OnSomethingClickListener aspectClickedListener) { - this.appSettings = appSettings; - this.aspectList = appSettings.getAspects(); - this.aspectFavsList = new ArrayList<>(Arrays.asList(appSettings.getAspectFavs())); - this.aspectClickedListener = aspectClickedListener; - this.isAmoledColorMode = appSettings.isAmoledColorMode(); - } - - @Override - public int getItemCount() { - return aspectList.length; - } - - @Override - public AspectAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - View v = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.recycler_list__list_item_with_fav, parent, false); - return new ViewHolder(v); - } - - @Override - public void onBindViewHolder(final ViewHolder holder, int position) { - // Alternating colors - final Context c = holder.root.getContext(); - final DiasporaAspect aspect = aspectList[position]; - holder.title.setText(aspect.name); - if (position % 2 == 1) { - holder.root.setBackgroundColor(isAmoledColorMode ? Color.BLACK : ContextUtils.get().rcolor(R.color.alternate_row_color)); - holder.title.setTextColor(isAmoledColorMode ? Color.GRAY : Color.BLACK); - } else { - holder.root.setBackgroundColor(isAmoledColorMode ? Color.BLACK : Color.WHITE); - holder.title.setTextColor(isAmoledColorMode ? Color.GRAY : Color.BLACK); - } - - // Favourite (Star) Image - applyFavouriteImage(holder.favouriteImage, isAspectFaved(aspect.name)); - - // Click on fav button - holder.favouriteImage.setOnClickListener(new View.OnClickListener() { - public void onClick(View v) { - if (isAspectFaved(aspect.name)) { - aspectFavsList.remove(aspectFavsList.indexOf(aspect.name)); - } else { - aspectFavsList.add(aspect.name); - } - appSettings.setAspectFavs(aspectFavsList); - applyFavouriteImage(holder.favouriteImage, isAspectFaved(aspect.name)); - } - }); - - holder.root.setOnClickListener(new View.OnClickListener() { - public void onClick(View v) { - aspectClickedListener.onSomethingClicked(null, null, aspect.id + ""); - } - }); - } - - private boolean isAspectFaved(String tag) { - return aspectFavsList.contains(tag); - } - - private void applyFavouriteImage(AppCompatImageView imageView, boolean isFaved) { - imageView.setImageResource(isFaved ? R.drawable.ic_star_filled_48px : R.drawable.ic_star_border_black_48px); - imageView.setColorFilter(isFaved ? appSettings.getAccentColor() : (isAmoledColorMode ? Color.GRAY : 0), PorterDuff.Mode.SRC_ATOP); - } - } -} diff --git a/app/src/main/java/com/github/dfa/diaspora_android/activity/DiasporaStreamFragment.java b/app/src/main/java/com/github/dfa/diaspora_android/activity/DiasporaStreamFragment.java index 77bd31fd..03517332 100644 --- a/app/src/main/java/com/github/dfa/diaspora_android/activity/DiasporaStreamFragment.java +++ b/app/src/main/java/com/github/dfa/diaspora_android/activity/DiasporaStreamFragment.java @@ -128,7 +128,7 @@ public class DiasporaStreamFragment extends BrowserFragment { @Override public boolean onOptionsItemSelected(MenuItem item) { AppLog.d(this, "StreamFragment.onOptionsItemSelected()"); - ShareUtil shu = new ShareUtil(getContext()).setFileProviderAuthority(BuildConfig.APPLICATION_ID); + ShareUtil shu = new ShareUtil(getContext()); PermissionChecker permc = new PermissionChecker(getActivity()); switch (item.getItemId()) { case R.id.action_reload: { @@ -152,7 +152,7 @@ public class DiasporaStreamFragment extends BrowserFragment { sharingIntent.setType("text/plain"); sharingIntent.putExtra(Intent.EXTRA_SUBJECT, webView.getTitle()); sharingIntent.putExtra(Intent.EXTRA_TEXT, webView.getUrl()); - startActivity(Intent.createChooser(sharingIntent, getResources().getString(R.string.action_share_dotdotdot))); + startActivity(Intent.createChooser(sharingIntent, getResources().getString(R.string.share_dotdotdot))); return true; } @@ -165,7 +165,7 @@ public class DiasporaStreamFragment extends BrowserFragment { case R.id.action_share_link_to_clipboard: { shu.setClipboard(webView.getUrl()); - Toast.makeText(getContext(), R.string.share__toast_link_address_copied, Toast.LENGTH_SHORT).show(); + Toast.makeText(getContext(), R.string.link_adress_copied, Toast.LENGTH_SHORT).show(); return true; } @@ -180,13 +180,13 @@ public class DiasporaStreamFragment extends BrowserFragment { } case R.id.action_take_screenshot: { - if (permc.doIfExtStoragePermissionGranted(getString(R.string.permissions_screenshot))) { + if (permc.doIfExtStoragePermissionGranted(getString(R.string.screenshot_permission__appspecific))) { File fileSaveDirectory = appSettings.getAppSaveDirectory(); if (permc.mkdirIfStoragePermissionGranted(fileSaveDirectory)) { Bitmap bmp = ShareUtil.getBitmapFromWebView(webView); String filename = "dandelion-" + ShareUtil.SDF_SHORT.format(new Date()) + ".jpg"; - _cu.writeImageToFileJpeg(new File(fileSaveDirectory, filename), bmp); - Snackbar.make(webView, getString(R.string.share__toast_screenshot) + _cu.writeImageToFile(new File(fileSaveDirectory, filename), bmp); + Snackbar.make(webView, getString(R.string.saving_screenshot_as) + " " + filename, Snackbar.LENGTH_LONG).show(); } } @@ -194,8 +194,8 @@ public class DiasporaStreamFragment extends BrowserFragment { } case R.id.action_share_screenshot: { - if (permc.doIfExtStoragePermissionGranted(getString(R.string.permissions_screenshot))) { - shu.shareImage(ShareUtil.getBitmapFromWebView(webView), Bitmap.CompressFormat.JPEG); + if (permc.doIfExtStoragePermissionGranted(getString(R.string.screenshot_permission__appspecific))) { + shu.shareImage(ShareUtil.getBitmapFromWebView(webView)); } return true; } @@ -293,7 +293,7 @@ public class DiasporaStreamFragment extends BrowserFragment { if (hasWRITE_EXTERNAL_STORAGE != PackageManager.PERMISSION_GRANTED) { if (!shouldShowRequestPermissionRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { new ThemedAlertDialogBuilder(getContext(), appSettings) - .setMessage(R.string.permissions_image) + .setMessage(R.string.image_permission_description__appspecific) .setNegativeButton(android.R.string.no, null) .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { @Override diff --git a/app/src/main/java/com/github/dfa/diaspora_android/activity/MainActivity.java b/app/src/main/java/com/github/dfa/diaspora_android/activity/MainActivity.java index 099d505f..ba8af918 100644 --- a/app/src/main/java/com/github/dfa/diaspora_android/activity/MainActivity.java +++ b/app/src/main/java/com/github/dfa/diaspora_android/activity/MainActivity.java @@ -36,6 +36,7 @@ import android.support.customtabs.CustomTabsSession; import android.support.design.widget.AppBarLayout; import android.support.design.widget.NavigationView; import android.support.design.widget.Snackbar; +import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.content.ContextCompat; import android.support.v4.content.LocalBroadcastManager; @@ -61,8 +62,8 @@ import android.widget.TextView; import android.widget.Toast; import com.github.dfa.diaspora_android.App; -import com.github.dfa.diaspora_android.BuildConfig; import com.github.dfa.diaspora_android.R; +import com.github.dfa.diaspora_android.data.DiasporaAspect; import com.github.dfa.diaspora_android.data.DiasporaPodList; import com.github.dfa.diaspora_android.data.DiasporaUserProfile; import com.github.dfa.diaspora_android.listener.DiasporaUserProfileChangedListener; @@ -71,6 +72,7 @@ import com.github.dfa.diaspora_android.receiver.OpenExternalLinkReceiver; import com.github.dfa.diaspora_android.receiver.UpdateTitleReceiver; import com.github.dfa.diaspora_android.ui.BadgeDrawable; import com.github.dfa.diaspora_android.ui.PodSelectionDialog; +import com.github.dfa.diaspora_android.ui.SearchOrCustomTextDialogCreator; import com.github.dfa.diaspora_android.ui.theme.ThemeHelper; import com.github.dfa.diaspora_android.ui.theme.ThemedActivity; import com.github.dfa.diaspora_android.ui.theme.ThemedAlertDialogBuilder; @@ -262,7 +264,7 @@ public class MainActivity extends ThemedActivity //Setup snackbar snackbarExitApp = Snackbar - .make(fragmentContainer, R.string.confirm_exit, Snackbar.LENGTH_LONG) + .make(fragmentContainer, R.string.do_you_want_to_exit, Snackbar.LENGTH_LONG) .setAction(android.R.string.yes, new View.OnClickListener() { public void onClick(View view) { finish(); @@ -271,13 +273,13 @@ public class MainActivity extends ThemedActivity }); snackbarLastVisitedTimestampInStream = Snackbar.make(fragmentContainer, - R.string.jump_to_last_visited_timestamp_in_stream, Snackbar.LENGTH_LONG) + R.string.jump_to_last_visited_page_in_stream__appspecific, Snackbar.LENGTH_LONG) .setAction(android.R.string.yes, new View.OnClickListener() { public void onClick(View view) { openDiasporaUrl(urls.getStreamWithTimestampUrl(diasporaUserProfile.getLastVisitedPositionInStream())); } }); - snackbarNoInternet = Snackbar.make(fragmentContainer, R.string.no_internet, Snackbar.LENGTH_LONG); + snackbarNoInternet = Snackbar.make(fragmentContainer, R.string.sorry_need_to_be_connected_to_internet, Snackbar.LENGTH_LONG); // Load app settings setupNavigationSlider(); @@ -306,14 +308,6 @@ public class MainActivity extends ThemedActivity BrowserFragment bf = new BrowserFragment(); fm.beginTransaction().add(bf, fragmentTag).commit(); return bf; - case TagListFragment.TAG: - TagListFragment hlf = new TagListFragment(); - fm.beginTransaction().add(hlf, fragmentTag).commit(); - return hlf; - case AspectListFragment.TAG: - AspectListFragment alf = new AspectListFragment(); - fm.beginTransaction().add(alf, fragmentTag).commit(); - return alf; case PodSelectionFragment.TAG: PodSelectionFragment psf = new PodSelectionFragment(); fm.beginTransaction().add(psf, fragmentTag).commit(); @@ -337,7 +331,7 @@ public class MainActivity extends ThemedActivity // This URL seems to be called somehow, but it doesn't make sense ;) toolbarTop.postDelayed(() -> { Intent i = new Intent(ACTION_OPEN_EXTERNAL_URL); - i.putExtra(EXTRA_URL, "https://github.com/Diaspora-for-Android/dandelion/blob/master/README.md"); + i.putExtra(EXTRA_URL, "https://github.com/gsantner/dandelion/blob/master/README.md"); LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(i); }, 1000); return; @@ -370,6 +364,13 @@ public class MainActivity extends ThemedActivity * @param fragment Fragment to show */ protected void showFragment(ThemedFragment fragment) { + if (PodSelectionFragment.TAG.equals(fragment.getTag())) { + Fragment fragment1 = fm.findFragmentByTag(DiasporaStreamFragment.TAG); + if (fragment1 != null) { + new net.gsantner.opoc.util.ContextUtils(this).restartApp(MainActivity.class); + } + } + AppLog.v(this, "showFragment()"); ThemedFragment currentTop = (ThemedFragment) fm.findFragmentById(R.id.fragment_container); if (currentTop == null || !currentTop.getFragmentTag().equals(fragment.getFragmentTag())) { @@ -387,7 +388,7 @@ public class MainActivity extends ThemedActivity */ private void setupNavigationSlider() { ActionBarDrawerToggle toggle = new ActionBarDrawerToggle( - this, navDrawer, toolbarTop, R.string.navigation_drawer_open, R.string.navigation_drawer_close); + this, navDrawer, toolbarTop, R.string.open_navdrawer, R.string.close_navdrawer); navDrawer.addDrawerListener(toggle); toggle.syncState(); @@ -431,8 +432,6 @@ public class MainActivity extends ThemedActivity app.getAvatarImageLoader().startImageDownload(navheaderImage, avatarUrl); } } - } else if (BuildConfig.IS_TEST_BUILD) { - navheaderImage.setImageResource(R.drawable.ic_launcher_test); } updateNavigationViewEntryVisibilities(); } @@ -459,7 +458,7 @@ public class MainActivity extends ThemedActivity navMenu.findItem(R.id.nav_statistics).setVisible(_appSettings.isVisibleInNavStatistics()); navMenu.findItem(R.id.nav_reports).setVisible(_appSettings.isVisibleInNavReports()); navMenu.findItem(R.id.nav_toggle_desktop_page).setVisible(_appSettings.isVisibleInNavToggleMobileDesktop()); - navMenu.findItem(R.id.nav_dandelion).setVisible(_appSettings.isVisibleInNavDandelionAccount()); + navMenu.findItem(R.id.nav_product_support).setVisible(_appSettings.isVisibleInNavGsantnerAccount()); // Hide whole group (for logged in use) if no pod was selected @@ -563,15 +562,15 @@ public class MainActivity extends ThemedActivity } else if ("sc_new_post".equals(action)) { openDiasporaUrl(urls.getNewPostUrl()); return; - } else if ("sc_nav_followed_tags".equals(action)) { - showFragment(getFragment(TagListFragment.TAG)); - return; - } else if ("sc_aspects".equals(action)) { - showFragment(getFragment(AspectListFragment.TAG)); - return; } else if ("sc_activities".equals(action)) { openDiasporaUrl(urls.getActivityUrl()); return; + } else if ("sc_contacts".equals(action)) { + onNavigationItemSelected(navView.getMenu().findItem(R.id.nav_aspects)); + return; + } else if ("sc_tags".equals(action)) { + onNavigationItemSelected(navView.getMenu().findItem(R.id.nav_followed_tags)); + return; } //Catch split screen recreation if (action != null && action.equals(Intent.ACTION_MAIN) && getTopFragment() != null) { @@ -855,7 +854,7 @@ public class MainActivity extends ThemedActivity public void onClick(DialogInterface dialogInterface, int which) { String query = input.getText().toString().trim().replaceAll((which == DialogInterface.BUTTON_NEGATIVE ? "\\*" : "\\#"), ""); if (query.equals("")) { - Snackbar.make(fragmentContainer, R.string.search_alert_bypeople_validate_needsomedata, Snackbar.LENGTH_LONG).show(); + Snackbar.make(fragmentContainer, R.string.please_add_a_name, Snackbar.LENGTH_LONG).show(); } else { openDiasporaUrl(which == DialogInterface.BUTTON_NEGATIVE ? urls.getSearchPeopleUrl(query) : urls.getSearchTagsUrl(query)); } @@ -867,8 +866,8 @@ public class MainActivity extends ThemedActivity final AlertDialog dialog = new ThemedAlertDialogBuilder(this, _appSettings) .setView(layout).setTitle(R.string.search_alert_title) .setCancelable(true) - .setPositiveButton(R.string.search_alert_tag, clickListener) - .setNegativeButton(R.string.search_alert_people, clickListener) + .setPositiveButton(R.string.by_tags, clickListener) + .setNegativeButton(R.string.by_people, clickListener) .create(); input.setOnEditorActionListener(new TextView.OnEditorActionListener() { @@ -1064,12 +1063,39 @@ public class MainActivity extends ThemedActivity break; case R.id.nav_followed_tags: { - showFragment(getFragment(TagListFragment.TAG)); + SearchOrCustomTextDialogCreator.showDiasporaTagsDialog(this, arg -> { + if (arg.startsWith(SearchOrCustomTextDialogCreator.SPECIAL_PREFIX)) { + arg = arg.replace(SearchOrCustomTextDialogCreator.SPECIAL_PREFIX, "").trim(); + if (arg.equals(getString(R.string.manage_hashtags))) { + openDiasporaUrl(urls.getManageTagsUrl()); + } else { + openDiasporaUrl(urls.getAllFollowedTagsUrl()); + } + } else { + openDiasporaUrl(urls.getSearchTagsUrl(arg)); + } + }); } break; case R.id.nav_aspects: { - showFragment(getFragment(AspectListFragment.TAG)); + SearchOrCustomTextDialogCreator.showDiasporaAspectsDialog(this, arg -> { + if (arg.startsWith(SearchOrCustomTextDialogCreator.SPECIAL_PREFIX)) { + arg = arg.replace(SearchOrCustomTextDialogCreator.SPECIAL_PREFIX, "").trim(); + if (arg.equals(getString(R.string.manage_your_contact_list))) { + openDiasporaUrl(urls.getContactsUrl()); + } else if (arg.equals(getString(R.string.nav_profile))) { + openDiasporaUrl(urls.getProfileUrl()); + } + } else { + for (DiasporaAspect daspect : _appSettings.getAspects()) { + if (arg.equals(daspect.name)) { + openDiasporaUrl(urls.getAspectUrl(Long.toString(daspect.id))); + break; + } + } + } + }); } break; @@ -1150,8 +1176,8 @@ public class MainActivity extends ThemedActivity } break; - case R.id.nav_dandelion: { - openDiasporaUrl(urls.getProfileUrl("48b78420923501341ef3782bcb452bd5")); + case R.id.nav_product_support: { + openDiasporaUrl(urls.getProfileUrl("d1cbdd70095301341e834860008dbc6c")); } break; diff --git a/app/src/main/java/com/github/dfa/diaspora_android/activity/PodSelectionFragment.java b/app/src/main/java/com/github/dfa/diaspora_android/activity/PodSelectionFragment.java index 8f6c5ad5..24da3d47 100644 --- a/app/src/main/java/com/github/dfa/diaspora_android/activity/PodSelectionFragment.java +++ b/app/src/main/java/com/github/dfa/diaspora_android/activity/PodSelectionFragment.java @@ -161,7 +161,7 @@ public class PodSelectionFragment extends ThemedFragment implements SearchView.O } catch (JSONException ignored) { } } else { - Snackbar.make(listViewPod, R.string.podlist_error, Snackbar.LENGTH_SHORT).show(); + Snackbar.make(listViewPod, R.string.could_not_retrieve_list_of_pods__appspecific, Snackbar.LENGTH_SHORT).show(); } } } @@ -173,7 +173,7 @@ public class PodSelectionFragment extends ThemedFragment implements SearchView.O rootView.setBackgroundColor(appSettings.isAmoledColorMode() ? Color.BLACK : Color.WHITE); listViewPod.setDivider(new ColorDrawable(Color.GRAY)); listViewPod.setDividerHeight(dividerHeight); - int bgcolor = appSettings.isAmoledColorMode() ? Color.BLACK : appSettings.getAccentColor(); + int bgcolor = appSettings.isAmoledColorMode() ? Color.DKGRAY : appSettings.getAccentColor(); buttonUseCustomPod.setBackgroundColor(bgcolor); buttonUseCustomPod.setTextColor(_cu.shouldColorOnTopBeLight(bgcolor) ? Color.WHITE : Color.BLACK); diff --git a/app/src/main/java/com/github/dfa/diaspora_android/activity/SettingsActivity.java b/app/src/main/java/com/github/dfa/diaspora_android/activity/SettingsActivity.java index 943dbfcc..0a38744e 100644 --- a/app/src/main/java/com/github/dfa/diaspora_android/activity/SettingsActivity.java +++ b/app/src/main/java/com/github/dfa/diaspora_android/activity/SettingsActivity.java @@ -131,7 +131,7 @@ public class SettingsActivity extends ThemedActivity implements SharedPreference if (top != null && top.getFragmentTag().equals(SettingsFragmentProxy.TAG)) { ProxyHandler.ProxySettings newProxySettings = getAppSettings().getProxySettings(); if (oldProxySettings.isEnabled() && !newProxySettings.isEnabled()) { - Toast.makeText(this, R.string.toast__proxy_disabled__restart_required, Toast.LENGTH_LONG).show(); + Toast.makeText(this, R.string.app_needs_restart_to_disable_proxy_usage, Toast.LENGTH_LONG).show(); } } super.onBackPressed(); @@ -226,7 +226,7 @@ public class SettingsActivity extends ThemedActivity implements SharedPreference } else if (settings.isKeyEqual(key, R.string.pref_key__change_account)) { new ThemedAlertDialogBuilder(getActivity(), AppSettings.get()) .setTitle(getString(R.string.confirmation)) - .setMessage(getString(R.string.pref_warning__change_account)) + .setMessage(getString(R.string.logout_warning_description)) .setNegativeButton(android.R.string.no, null) .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { @@ -271,6 +271,8 @@ public class SettingsActivity extends ThemedActivity implements SharedPreference @Override public boolean onPreferenceTreeClick(PreferenceScreen screen, Preference preference) { + AppSettings settings = ((App) getActivity().getApplication()).getSettings(); + DiasporaUrlHelper diasporaUrlHelper = new DiasporaUrlHelper(settings); if (isAdded() && preference.hasKey()) { String key = preference.getKey(); if (key.equals(getString(R.string.pref_key__primary_color__preference_click))) { @@ -279,6 +281,13 @@ public class SettingsActivity extends ThemedActivity implements SharedPreference } else if (key.equals(getString(R.string.pref_key__accent_color__preference_click))) { showColorPickerDialog(2); return true; + } else if (key.equals(getString(R.string.pref_key__manage_theme))) { + Intent intent = new Intent(getActivity(), MainActivity.class); + intent.setAction(MainActivity.ACTION_OPEN_URL); + intent.putExtra(MainActivity.URL_MESSAGE, diasporaUrlHelper.getThemeUrl()); + startActivity(intent); + getActivity().finish(); + return true; } } return super.onPreferenceTreeClick(screen, preference); @@ -310,7 +319,7 @@ public class SettingsActivity extends ThemedActivity implements SharedPreference final LineColorPicker base = dialogLayout.findViewById(R.id.color_picker_dialog__base_picker); final LineColorPicker shade = dialogLayout.findViewById(R.id.color_picker_dialog__shade_picker); - title.setText(type == 1 ? R.string.pref_title__primary_color : R.string.pref_title__accent_color); + title.setText(type == 1 ? R.string.primary_colors : R.string.accent_color); title.setTextColor(getResources().getColor(R.color.white)); final int[] current = (type == 1 ? appSettings.getPrimaryColorSettings() : appSettings.getAccentColorSettings()); base.setColors((type == 1 ? ColorPalette.getBaseColors(context) : ColorPalette.getAccentColors(context))); @@ -412,7 +421,7 @@ public class SettingsActivity extends ThemedActivity implements SharedPreference if (appSettings.isKeyEqual(key, R.string.pref_key__http_proxy_load_tor_preset)) { appSettings.setProxyHttpHost("127.0.0.1"); appSettings.setProxyHttpPort(8118); - Toast.makeText(screen.getContext(), R.string.toast__proxy_orbot_preset_loaded, Toast.LENGTH_SHORT).show(); + Toast.makeText(screen.getContext(), R.string.orbot_proxy_preset_loaded, Toast.LENGTH_SHORT).show(); return true; } } @@ -477,17 +486,13 @@ public class SettingsActivity extends ThemedActivity implements SharedPreference ThemedAlertDialogBuilder builder = new ThemedAlertDialogBuilder(getActivity(), appSettings); builder.setTitle(R.string.confirmation) - .setMessage(R.string.dialog_content__wipe_settings) + .setMessage(R.string.wipe_settings_warning__appspecific) .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { appSettings.resetAppSettings(); appSettings.resetPodSettings(); - Intent restartActivity = new Intent(getActivity(), MainActivity.class); - PendingIntent pendingIntent = PendingIntent.getActivity(getActivity(), 12374, restartActivity, PendingIntent.FLAG_CANCEL_CURRENT); - AlarmManager mgr = (AlarmManager) getActivity().getSystemService(Context.ALARM_SERVICE); - mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 100, pendingIntent); - System.exit(0); + new net.gsantner.opoc.util.ContextUtils(appSettings.getContext()).restartApp(MainActivity.class); } }).setNegativeButton(android.R.string.cancel, null) .create().show(); diff --git a/app/src/main/java/com/github/dfa/diaspora_android/activity/TagListFragment.java b/app/src/main/java/com/github/dfa/diaspora_android/activity/TagListFragment.java deleted file mode 100644 index adb832c5..00000000 --- a/app/src/main/java/com/github/dfa/diaspora_android/activity/TagListFragment.java +++ /dev/null @@ -1,206 +0,0 @@ -/* - This file is part of the dandelion*. - - dandelion* 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. - - dandelion* 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 the dandelion*. - - If not, see . - */ -package com.github.dfa.diaspora_android.activity; - -import android.content.Context; -import android.graphics.Color; -import android.graphics.PorterDuff; -import android.os.Bundle; -import android.support.v7.widget.AppCompatImageView; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.RelativeLayout; -import android.widget.TextView; - -import com.github.dfa.diaspora_android.App; -import com.github.dfa.diaspora_android.R; -import com.github.dfa.diaspora_android.listener.OnSomethingClickListener; -import com.github.dfa.diaspora_android.ui.theme.ThemedFragment; -import com.github.dfa.diaspora_android.util.AppSettings; -import com.github.dfa.diaspora_android.util.ContextUtils; -import com.github.dfa.diaspora_android.util.DiasporaUrlHelper; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import butterknife.BindView; -import butterknife.ButterKnife; - -/** - * Fragment that shows a list of the HashTags the user follows - */ -public class TagListFragment extends ThemedFragment implements OnSomethingClickListener { - - public static final String TAG = "com.github.dfa.diaspora_android.TagListFragment"; - - @BindView(R.id.fragment_list__recycler_view) - public RecyclerView followedTagsRecyclerView; - - @BindView(R.id.fragment_list__spacer) - public View space; - - @BindView(R.id.fragment_list__root) - public RelativeLayout rootView; - - protected App app; - protected DiasporaUrlHelper urls; - - @Override - protected int getLayoutResId() { - return R.layout.recycler_list__fragment; - } - - @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - ButterKnife.bind(this, view); - app = (App) getActivity().getApplication(); - AppSettings appSettings = app.getSettings(); - urls = new DiasporaUrlHelper(appSettings); - - followedTagsRecyclerView.setHasFixedSize(true); - followedTagsRecyclerView.setNestedScrollingEnabled(false); - - RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(getContext()); - followedTagsRecyclerView.setLayoutManager(layoutManager); - - final FollowedTagsAdapter adapter = new FollowedTagsAdapter(appSettings, this); - followedTagsRecyclerView.setAdapter(adapter); - - //Set window title - getActivity().setTitle(R.string.nav_followed_tags); - } - - @Override - public String getFragmentTag() { - return TAG; - } - - @Override - public boolean onBackPressed() { - return false; - } - - @Override - public void onSomethingClicked(Object null1, Integer null2, String tag) { - ((MainActivity) getActivity()).openDiasporaUrl(urls.getSearchTagsUrl(tag)); - } - - @Override - protected void applyColorToViews() { - followedTagsRecyclerView.invalidate(); - if (getAppSettings().isAmoledColorMode()) { - rootView.setBackgroundColor(Color.BLACK); - space.setBackgroundColor(Color.BLACK); - } - } - - public static class FollowedTagsAdapter extends RecyclerView.Adapter { - private boolean isAmoledColorMode; - private AppSettings appSettings; - private String[] followedTagsList; - private List followedTagsFavsList; - private OnSomethingClickListener tagClickedListener; - - static class ViewHolder extends RecyclerView.ViewHolder { - @BindView(R.id.recycler_view__list_item__text) - public TextView title; - @BindView(R.id.recycler_view__list_item__favourite) - AppCompatImageView favouriteImage; - @BindView(R.id.recycler_view__list_item__root) - RelativeLayout root; - - ViewHolder(View v) { - super(v); - ButterKnife.bind(this, v); - } - } - - - FollowedTagsAdapter(AppSettings appSettings, OnSomethingClickListener tagClickedListener) { - this.appSettings = appSettings; - this.followedTagsList = appSettings.getFollowedTags(); - this.followedTagsFavsList = new ArrayList<>(Arrays.asList(appSettings.getFollowedTagsFavs())); - this.tagClickedListener = tagClickedListener; - this.isAmoledColorMode = appSettings.isAmoledColorMode(); - } - - @Override - public int getItemCount() { - return followedTagsList.length; - } - - @Override - public FollowedTagsAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - View v = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.recycler_list__list_item_with_fav, parent, false); - return new ViewHolder(v); - } - - @Override - public void onBindViewHolder(final ViewHolder holder, final int position) { - // Alternating colors - final Context c = holder.root.getContext(); - final String tag = followedTagsList[position]; - holder.title.setText(tag); - if (position % 2 == 1) { - holder.root.setBackgroundColor(isAmoledColorMode ? Color.BLACK : ContextUtils.get().rcolor(R.color.alternate_row_color)); - holder.title.setTextColor(isAmoledColorMode ? Color.GRAY : Color.BLACK); - } else { - holder.root.setBackgroundColor(isAmoledColorMode ? Color.BLACK : Color.WHITE); - holder.title.setTextColor(isAmoledColorMode ? Color.GRAY : Color.BLACK); - } - - // Favourite (Star) Image - applyFavouriteImage(holder.favouriteImage, isFollowedTagFaved(tag)); - - // Click on fav button - holder.favouriteImage.setOnClickListener(new View.OnClickListener() { - public void onClick(View v) { - if (isFollowedTagFaved(tag)) { - followedTagsFavsList.remove(followedTagsFavsList.indexOf(tag)); - } else { - followedTagsFavsList.add(tag); - } - appSettings.setFollowedTagsFavs(followedTagsFavsList); - applyFavouriteImage(holder.favouriteImage, isFollowedTagFaved(tag)); - } - }); - - holder.root.setOnClickListener(new View.OnClickListener() { - public void onClick(View v) { - tagClickedListener.onSomethingClicked(null, null, tag); - } - }); - } - - private boolean isFollowedTagFaved(String tag) { - return followedTagsFavsList.contains(tag); - } - - private void applyFavouriteImage(AppCompatImageView imageView, boolean isFaved) { - imageView.setImageResource(isFaved ? R.drawable.ic_star_filled_48px : R.drawable.ic_star_border_black_48px); - imageView.setColorFilter(isFaved ? appSettings.getAccentColor() : (isAmoledColorMode ? Color.GRAY : 0), PorterDuff.Mode.SRC_ATOP); - } - } -} diff --git a/app/src/main/java/com/github/dfa/diaspora_android/data/DiasporaPodList.java b/app/src/main/java/com/github/dfa/diaspora_android/data/DiasporaPodList.java index 3c56cef3..3052ea77 100644 --- a/app/src/main/java/com/github/dfa/diaspora_android/data/DiasporaPodList.java +++ b/app/src/main/java/com/github/dfa/diaspora_android/data/DiasporaPodList.java @@ -13,7 +13,7 @@ import java.util.List; /** - * Created by gsantner (http://gsantner.net/ on 30.09.16. + * Created by gsantner (gsantner AT mailbox DOT org on 30.09.16. * DiasporaPodList - List container for DiasporaPod's, with methods to merge with other DiasporaPodLists * DiasporaPod - Data container for a Pod, can include N DiasporaPodUrl's * DiasporaPodUrl - A Url of an DiasporaPod diff --git a/app/src/main/java/com/github/dfa/diaspora_android/data/DiasporaUserProfile.java b/app/src/main/java/com/github/dfa/diaspora_android/data/DiasporaUserProfile.java index 863b7c6f..22efea59 100644 --- a/app/src/main/java/com/github/dfa/diaspora_android/data/DiasporaUserProfile.java +++ b/app/src/main/java/com/github/dfa/diaspora_android/data/DiasporaUserProfile.java @@ -32,7 +32,7 @@ import org.json.JSONObject; /** * User profile - * Created by gsantner (http://gsantner.net/) on 24.03.16. Part of dandelion*. + * Created by gsantner (gsantner AT mailbox DOT org) on 24.03.16. Part of dandelion*. */ public class DiasporaUserProfile { private static final int MINIMUM_USERPROFILE_LOAD_TIMEDIFF = 5000; diff --git a/app/src/main/java/com/github/dfa/diaspora_android/listener/DiasporaUserProfileChangedListener.java b/app/src/main/java/com/github/dfa/diaspora_android/listener/DiasporaUserProfileChangedListener.java index aff25a2a..dc1b67dc 100644 --- a/app/src/main/java/com/github/dfa/diaspora_android/listener/DiasporaUserProfileChangedListener.java +++ b/app/src/main/java/com/github/dfa/diaspora_android/listener/DiasporaUserProfileChangedListener.java @@ -21,7 +21,7 @@ package com.github.dfa.diaspora_android.listener; import com.github.dfa.diaspora_android.data.DiasporaUserProfile; /** - * Created by gsantner (http://gsantner.net/) on 26.03.16. + * Created by gsantner (gsantner AT mailbox DOT org) on 26.03.16. * Interface that needs to be implemented by classes that listen for Profile related changes */ public interface DiasporaUserProfileChangedListener { diff --git a/app/src/main/java/com/github/dfa/diaspora_android/service/FetchPodsService.java b/app/src/main/java/com/github/dfa/diaspora_android/service/FetchPodsService.java index 3e594308..d9a24ecd 100644 --- a/app/src/main/java/com/github/dfa/diaspora_android/service/FetchPodsService.java +++ b/app/src/main/java/com/github/dfa/diaspora_android/service/FetchPodsService.java @@ -59,7 +59,7 @@ public class FetchPodsService extends Service { } class GetPodsTask extends AsyncTask { - private static final String PODDY_PODLIST_URL = "https://raw.githubusercontent.com/Diaspora-for-Android/dandelion/master/app/src/main/res/raw/podlist.json"; + private static final String PODDY_PODLIST_URL = "https://raw.githubusercontent.com/gsantner/dandelion/master/app/src/main/res/raw/podlist.json"; private final Service service; diff --git a/app/src/main/java/com/github/dfa/diaspora_android/ui/PodSelectionDialog.java b/app/src/main/java/com/github/dfa/diaspora_android/ui/PodSelectionDialog.java index aa885038..abb31703 100644 --- a/app/src/main/java/com/github/dfa/diaspora_android/ui/PodSelectionDialog.java +++ b/app/src/main/java/com/github/dfa/diaspora_android/ui/PodSelectionDialog.java @@ -37,7 +37,7 @@ import butterknife.OnItemSelected; /** * Dialog that helps the user configure a pod - * Created by gsantner (http://gsantner.net) on 06.10.16. + * Created by gsantner on 06.10.16. */ public class PodSelectionDialog extends ThemedAppCompatDialogFragment { public static final String TAG = "com.github.dfa.diaspora_android.ui.PodSelectionDialog"; diff --git a/app/src/main/java/com/github/dfa/diaspora_android/ui/SearchOrCustomTextDialogCreator.java b/app/src/main/java/com/github/dfa/diaspora_android/ui/SearchOrCustomTextDialogCreator.java new file mode 100644 index 00000000..c7421567 --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/ui/SearchOrCustomTextDialogCreator.java @@ -0,0 +1,85 @@ +package com.github.dfa.diaspora_android.ui; + +import android.app.Activity; +import android.support.v4.content.ContextCompat; + +import com.github.dfa.diaspora_android.R; +import com.github.dfa.diaspora_android.data.DiasporaAspect; +import com.github.dfa.diaspora_android.util.AppSettings; + +import net.gsantner.opoc.ui.SearchOrCustomTextDialog; +import net.gsantner.opoc.util.Callback; + +import java.util.ArrayList; +import java.util.Arrays; + +public class SearchOrCustomTextDialogCreator { + public static final String SPECIAL_PREFIX = "\uD83D\uDCA0"; + + public static void showDiasporaTagsDialog(final Activity activity, final Callback.a1 callback) { + SearchOrCustomTextDialog.DialogOptions dopt = new SearchOrCustomTextDialog.DialogOptions(); + baseConf(activity, dopt); + dopt.callback = callback; + dopt.isSearchEnabled = true; + dopt.searchHintText = R.string.search; + dopt.titleText = R.string.tags; + + new Thread(() -> { + AppSettings appSettings = AppSettings.get(); + ArrayList hl = new ArrayList<>(); + ArrayList data = new ArrayList<>(Arrays.asList(appSettings.getFollowedTags())); + if (data.size() > 0) { + String highlighted = surroundString(data.remove(0)); + data.add(0, highlighted); + hl.add(highlighted); + } + + for (int strid : new int[]{R.string.manage_hashtags}) { + String special = surroundString(appSettings.rstr(strid)); + data.add(0, special); + hl.add(special); + } + dopt.data = data; + dopt.highlightData = hl; + activity.runOnUiThread(() -> SearchOrCustomTextDialog.showMultiChoiceDialogWithSearchFilterUI(activity, dopt)); + }).start(); + } + + private static String surroundString(String text) { + return SPECIAL_PREFIX + " " + text + " "; + } + + + public static void showDiasporaAspectsDialog(final Activity activity, final Callback.a1 callback) { + SearchOrCustomTextDialog.DialogOptions dopt = new SearchOrCustomTextDialog.DialogOptions(); + baseConf(activity, dopt); + dopt.callback = callback; + dopt.isSearchEnabled = false; + dopt.titleText = R.string.contacts; + + new Thread(() -> { + AppSettings appSettings = AppSettings.get(); + ArrayList hl = new ArrayList<>(); + ArrayList data = new ArrayList<>(); + for (DiasporaAspect aspect : AppSettings.get().getAspects()) { + data.add(aspect.name); + } + for (int strid : new int[]{R.string.nav_profile, R.string.manage_your_contact_list}) { + String special = surroundString(appSettings.rstr(strid)); + data.add(0, special); + hl.add(special); + } + dopt.data = data; + dopt.highlightData = hl; + activity.runOnUiThread(() -> SearchOrCustomTextDialog.showMultiChoiceDialogWithSearchFilterUI(activity, dopt)); + }).start(); + } + + + private static void baseConf(Activity activity, SearchOrCustomTextDialog.DialogOptions dopt) { + AppSettings as = new AppSettings(activity); + dopt.isDarkDialog = as.isAmoledColorMode(); + dopt.textColor = ContextCompat.getColor(activity, dopt.isDarkDialog ? R.color.white : R.color.primary_text); + dopt.highlightColor = as.getAccentColor(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/dfa/diaspora_android/util/ActivityUtils.java b/app/src/main/java/com/github/dfa/diaspora_android/util/ActivityUtils.java index 817b8e10..09ecb767 100644 --- a/app/src/main/java/com/github/dfa/diaspora_android/util/ActivityUtils.java +++ b/app/src/main/java/com/github/dfa/diaspora_android/util/ActivityUtils.java @@ -52,7 +52,7 @@ public class ActivityUtils extends net.gsantner.opoc.util.ActivityUtils { public boolean showInfoIfUserNotConnectedToInternet(View anchor) { boolean isOnline = WebHelper.isOnline(_context); if (!isOnline) { - showSnackBar(R.string.no_internet, true); + showSnackBar(R.string.sorry_need_to_be_connected_to_internet, true); } return !isOnline; } diff --git a/app/src/main/java/com/github/dfa/diaspora_android/util/AppSettings.java b/app/src/main/java/com/github/dfa/diaspora_android/util/AppSettings.java index 79953d7a..1ced82bb 100644 --- a/app/src/main/java/com/github/dfa/diaspora_android/util/AppSettings.java +++ b/app/src/main/java/com/github/dfa/diaspora_android/util/AppSettings.java @@ -37,7 +37,7 @@ import java.util.List; /** * Settings - * Created by gsantner (http://gsantner.net/) on 20.03.16. Part of dandelion*. + * Created by gsantner (gsantner AT mailbox DOT org) on 20.03.16. Part of dandelion*. */ @SuppressWarnings("ConstantConditions") public class AppSettings extends SharedPreferencesPropertyBackend { @@ -347,8 +347,8 @@ public class AppSettings extends SharedPreferencesPropertyBackend { return getBool(R.string.pref_key__visibility_nav__reports, false); } - public boolean isVisibleInNavDandelionAccount() { - return getBool(R.string.pref_key__visibility_nav__dandelion_account, false); + public boolean isVisibleInNavGsantnerAccount() { + return getBool(R.string.pref_key__visibility_nav__gsantner_account, false); } public boolean isVisibleInNavToggleMobileDesktop() { @@ -359,6 +359,14 @@ public class AppSettings extends SharedPreferencesPropertyBackend { return getBool(R.string.pref_key__topbar_stream_shortcut, false); } + public boolean isOpenYoutubeExternalEnabled() { + return getBool(R.string.pref_key__open_youtube_external_enabled, true); + } + + public boolean isSwipeRefreshEnabled() { + return getBool(R.string.pref_key__swipe_refresh_enabled, true); + } + public String getScreenRotation() { return getString(R.string.pref_key__screen_rotation, R.string.rotation_val_system); } @@ -444,6 +452,10 @@ public class AppSettings extends SharedPreferencesPropertyBackend { return getBool(R.string.pref_key__primary_color__amoled_mode, false); } + public void setAmoledColorMode(boolean enable) { + setBool(R.string.pref_key__primary_color__amoled_mode, enable); + } + public boolean isAdBlockEnabled() { return getBool(R.string.pref_key__adblock_enable, true); } diff --git a/app/src/main/java/com/github/dfa/diaspora_android/util/DiasporaUrlHelper.java b/app/src/main/java/com/github/dfa/diaspora_android/util/DiasporaUrlHelper.java index a4bf65c6..b272dac0 100644 --- a/app/src/main/java/com/github/dfa/diaspora_android/util/DiasporaUrlHelper.java +++ b/app/src/main/java/com/github/dfa/diaspora_android/util/DiasporaUrlHelper.java @@ -62,6 +62,7 @@ public class DiasporaUrlHelper { public static final String SUBURL_NOTIFICATIONS_MENTIONED = "/notifications?type=mentioned"; public static final String SUBURL_NOTIFICATIONS_RESHARED = "/notifications?type=reshared"; public static final String SUBURL_NOTIFICATIONS_STARTED_SHARING = "/notifications?type=started_sharing"; + public static final String SUBURL_THEME = "/user/edit"; public DiasporaUrlHelper(AppSettings settings) { this.settings = settings; @@ -229,6 +230,15 @@ public class DiasporaUrlHelper { return getPodUrl() + SUBURL_SEARCH_TAGS + query; } + /** + * Return a url that queries posts for the given hashtag query + * + * @return https://(pod-domain.tld)/followed_tags + */ + public String getAllFollowedTagsUrl() { + return getPodUrl() + SUBURL_FOLOWED_TAGS; + } + /** * Return a url that queries user accounts for query * @@ -344,4 +354,13 @@ public class DiasporaUrlHelper { } return app.getString(R.string.aspects); } + + /** + * Return a url that points to the settings of the pod. + * + * @return https://(pod-domain.tld)/user/edit + */ + public String getThemeUrl() { + return getPodUrl() + SUBURL_THEME; + } } diff --git a/app/src/main/java/com/github/dfa/diaspora_android/web/BrowserFragment.java b/app/src/main/java/com/github/dfa/diaspora_android/web/BrowserFragment.java index 28db279b..bfc53fd9 100644 --- a/app/src/main/java/com/github/dfa/diaspora_android/web/BrowserFragment.java +++ b/app/src/main/java/com/github/dfa/diaspora_android/web/BrowserFragment.java @@ -21,6 +21,7 @@ package com.github.dfa.diaspora_android.web; import android.content.Context; import android.content.MutableContextWrapper; import android.os.Bundle; +import android.support.v4.widget.SwipeRefreshLayout; import android.view.View; import android.view.ViewGroup; import android.webkit.WebSettings; @@ -50,6 +51,7 @@ public class BrowserFragment extends ThemedFragment { protected WebSettings webSettings; protected String pendingUrl; + protected SwipeRefreshLayout swipe;//pull to refresh @Override protected int getLayoutResId() { @@ -90,6 +92,18 @@ public class BrowserFragment extends ThemedFragment { webView.setParentActivity(getActivity()); this.setRetainInstance(true); + + //pull to refresh + swipe = view.findViewById(R.id.swipe); + swipe.setDistanceToTriggerSync(2000); + swipe.setOnRefreshListener(() -> reloadUrl()); + if (appSettings.isSwipeRefreshEnabled()) { + swipe.setEnabled(true); + } else { + swipe.setRefreshing(false); + swipe.setEnabled(false); + return; + } } @Override @@ -192,6 +206,7 @@ public class BrowserFragment extends ThemedFragment { @Override public void run() { getWebView().reload(); + swipe.setRefreshing(false);//pull to refresh } }); diff --git a/app/src/main/java/com/github/dfa/diaspora_android/web/ContextMenuWebView.java b/app/src/main/java/com/github/dfa/diaspora_android/web/ContextMenuWebView.java index 6cb85c8f..e1194c9e 100644 --- a/app/src/main/java/com/github/dfa/diaspora_android/web/ContextMenuWebView.java +++ b/app/src/main/java/com/github/dfa/diaspora_android/web/ContextMenuWebView.java @@ -78,14 +78,14 @@ public class ContextMenuWebView extends NestedWebView { public boolean onMenuItemClick(MenuItem item) { HitTestResult result = getHitTestResult(); String url = result.getExtra(); - final ShareUtil shu = new ShareUtil(context).setFileProviderAuthority(BuildConfig.APPLICATION_ID); + final ShareUtil shu = new ShareUtil(context); final PermissionChecker permc = new PermissionChecker(parentActivity); final AppSettings appSettings = new AppSettings(context); switch (item.getItemId()) { //Save image to external memory case ID_SAVE_IMAGE: { - if (permc.doIfExtStoragePermissionGranted(context.getString(R.string.permissions_image))) { + if (permc.doIfExtStoragePermissionGranted(context.getString(R.string.image_permission_description__appspecific))) { File fileSaveDirectory = appSettings.getAppSaveDirectory(); if (permc.mkdirIfStoragePermissionGranted(fileSaveDirectory)) { String filename = "dandelion-" + ShareUtil.SDF_SHORT.format(new Date()) + url.substring(url.lastIndexOf(".")); @@ -95,7 +95,7 @@ public class ContextMenuWebView extends NestedWebView { ((DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE)).enqueue(request);*/ new DownloadTask(new File(fileSaveDirectory, filename), (ok, dlfile) -> { if (ok) { - Toast.makeText(context, context.getText(R.string.share__toast_saved_image_to_location) + " " + dlfile.getName(), Toast.LENGTH_LONG).show(); + Toast.makeText(context, context.getText(R.string.saving_image_to) + " " + dlfile.getName(), Toast.LENGTH_LONG).show(); } }).execute(url); } @@ -104,13 +104,13 @@ public class ContextMenuWebView extends NestedWebView { } case ID_SHARE_IMAGE: { - if (permc.doIfExtStoragePermissionGranted(context.getString(R.string.permissions_image))) { + if (permc.doIfExtStoragePermissionGranted(context.getString(R.string.image_permission_description__appspecific))) { File fileSaveDirectory = appSettings.getAppSaveDirectory(); if (permc.mkdirIfStoragePermissionGranted(fileSaveDirectory)) { String filename = ".dandelion-shared" + url.substring(url.lastIndexOf(".")); new DownloadTask(new File(fileSaveDirectory, filename), (ok, dlfile) -> { if (ok) { - Toast.makeText(context, context.getText(R.string.share__toast_saved_image_to_location) + " " + dlfile.getName(), Toast.LENGTH_LONG).show(); + Toast.makeText(context, context.getText(R.string.saving_image_to) + " " + dlfile.getName(), Toast.LENGTH_LONG).show(); shu.shareStream(dlfile, "image/" + dlfile.getAbsolutePath().lastIndexOf(".") + 1); } }).execute(url); @@ -132,7 +132,7 @@ public class ContextMenuWebView extends NestedWebView { if (url != null) { ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); clipboard.setPrimaryClip(ClipData.newPlainText("text", url)); - Toast.makeText(context, R.string.share__toast_link_address_copied, Toast.LENGTH_SHORT).show(); + Toast.makeText(context, R.string.link_adress_copied, Toast.LENGTH_SHORT).show(); } break; @@ -144,7 +144,7 @@ public class ContextMenuWebView extends NestedWebView { sendIntent.putExtra(Intent.EXTRA_TEXT, url); sendIntent.setType("text/plain"); context.startActivity(Intent.createChooser(sendIntent, getResources() - .getText(R.string.context_menu_share_link))); + .getText(R.string.share_link_address))); } break; } @@ -157,16 +157,16 @@ public class ContextMenuWebView extends NestedWebView { result.getType() == HitTestResult.SRC_IMAGE_ANCHOR_TYPE) { // Menu options for an image. menu.setHeaderTitle(result.getExtra()); - menu.add(0, ID_SAVE_IMAGE, 0, context.getString(R.string.context_menu_save_image)).setOnMenuItemClickListener(handler); - menu.add(0, ID_IMAGE_EXTERNAL_BROWSER, 0, context.getString(R.string.context_menu_open_external_browser)).setOnMenuItemClickListener(handler); - menu.add(0, ID_SHARE_IMAGE, 0, context.getString(R.string.context_menu_share_image)).setOnMenuItemClickListener(handler); - menu.add(0, ID_COPY_IMAGE_LINK, 0, context.getString(R.string.context_menu_copy_image_link)).setOnMenuItemClickListener(handler); + menu.add(0, ID_SAVE_IMAGE, 0, context.getString(R.string.save_image)).setOnMenuItemClickListener(handler); + menu.add(0, ID_IMAGE_EXTERNAL_BROWSER, 0, context.getString(R.string.open_in_external_browser)).setOnMenuItemClickListener(handler); + menu.add(0, ID_SHARE_IMAGE, 0, context.getString(R.string.share_image)).setOnMenuItemClickListener(handler); + menu.add(0, ID_COPY_IMAGE_LINK, 0, context.getString(R.string.copy_image_address_to_clipboard)).setOnMenuItemClickListener(handler); } else if (result.getType() == HitTestResult.ANCHOR_TYPE || result.getType() == HitTestResult.SRC_ANCHOR_TYPE) { // Menu options for a hyperlink. menu.setHeaderTitle(result.getExtra()); - menu.add(0, ID_COPY_LINK, 0, context.getString(R.string.copy_link_to_clipboard)).setOnMenuItemClickListener(handler); - menu.add(0, ID_SHARE_LINK, 0, context.getString(R.string.context_menu_share_link)).setOnMenuItemClickListener(handler); + menu.add(0, ID_COPY_LINK, 0, context.getString(R.string.copy_link_adress_to_clipboard)).setOnMenuItemClickListener(handler); + menu.add(0, ID_SHARE_LINK, 0, context.getString(R.string.share_link_address)).setOnMenuItemClickListener(handler); } } diff --git a/app/src/main/java/com/github/dfa/diaspora_android/web/CustomWebViewClient.java b/app/src/main/java/com/github/dfa/diaspora_android/web/CustomWebViewClient.java index c6af6f85..5e0011d8 100644 --- a/app/src/main/java/com/github/dfa/diaspora_android/web/CustomWebViewClient.java +++ b/app/src/main/java/com/github/dfa/diaspora_android/web/CustomWebViewClient.java @@ -20,6 +20,7 @@ package com.github.dfa.diaspora_android.web; import android.annotation.TargetApi; import android.content.Intent; +import android.net.Uri; import android.os.Build; import android.support.v4.content.LocalBroadcastManager; import android.webkit.CookieManager; @@ -38,6 +39,7 @@ public class CustomWebViewClient extends WebViewClient { private final App app; private String lastLoadUrl = ""; private boolean isAdBlockEnabled = false; + AppSettings appSettings = AppSettings.get(); public CustomWebViewClient(App app, WebView webView) { this.app = app; @@ -56,6 +58,11 @@ public class CustomWebViewClient extends WebViewClient { || (host != null && (url.startsWith("https://" + host) || url.startsWith("http://" + host)))) { return false; + }//make youtube links open external-->never customtab + else if (appSettings.isOpenYoutubeExternalEnabled() && (url.startsWith("https://youtube.com/") || url.startsWith("https://www.youtube.com/") || url.startsWith("https://m.youtube.com/") || url.startsWith("https://youtu.be/"))) { + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + view.getContext().startActivity(intent); + return true; } else { Intent i = new Intent(MainActivity.ACTION_OPEN_EXTERNAL_URL); i.putExtra(MainActivity.EXTRA_URL, url); diff --git a/app/src/main/java/com/github/dfa/diaspora_android/web/WebHelper.java b/app/src/main/java/com/github/dfa/diaspora_android/web/WebHelper.java index 44cf8ad3..49d10340 100644 --- a/app/src/main/java/com/github/dfa/diaspora_android/web/WebHelper.java +++ b/app/src/main/java/com/github/dfa/diaspora_android/web/WebHelper.java @@ -32,7 +32,6 @@ import com.github.dfa.diaspora_android.activity.MainActivity; /** * Created by Gregor Santner on 07.08.16. - * http://gsantner.net */ public class WebHelper { diff --git a/app/src/main/java/net/gsantner/opoc/activity/GsFragmentBase.java b/app/src/main/java/net/gsantner/opoc/activity/GsFragmentBase.java index 93019ce9..479551d9 100644 --- a/app/src/main/java/net/gsantner/opoc/activity/GsFragmentBase.java +++ b/app/src/main/java/net/gsantner/opoc/activity/GsFragmentBase.java @@ -1,7 +1,6 @@ /*####################################################### * - * Maintained by Gregor Santner, 2017- - * https://gsantner.net/ + * Maintained 2017-2023 by Gregor Santner * * License: Apache 2.0 * https://github.com/gsantner/opoc/#licensing @@ -16,10 +15,15 @@ import android.support.annotation.LayoutRes; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; +import android.support.v7.widget.Toolbar; +import android.util.Log; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; import android.view.View; import android.view.ViewGroup; +import net.gsantner.opoc.android.dummy.MenuItemDummy; import net.gsantner.opoc.util.ContextUtils; import butterknife.ButterKnife; @@ -33,6 +37,7 @@ public abstract class GsFragmentBase extends Fragment { protected ContextUtils _cu; protected Bundle _savedInstanceState = null; + protected Menu _fragmentMenu = new MenuItemDummy.Menu(); @Override public void onCreate(Bundle savedInstanceState) { @@ -51,6 +56,9 @@ public abstract class GsFragmentBase extends Fragment { _cu = new ContextUtils(inflater.getContext()); _cu.setAppLanguage(getAppLanguage()); _savedInstanceState = savedInstanceState; + if (getLayoutResId() == 0) { + Log.e(getClass().getCanonicalName(), "Error: GsFragmentbase.onCreateview: Returned 0 for getLayoutResId"); + } View view = inflater.inflate(getLayoutResId(), container, false); ButterKnife.bind(this, view); return view; @@ -126,4 +134,27 @@ public abstract class GsFragmentBase extends Fragment { } } } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + _fragmentMenu = menu; + } + + public Menu getFragmentMenu() { + return _fragmentMenu; + } + + /** + * Get the toolbar from activity + * Requires id to be set to @+id/toolbar + */ + @SuppressWarnings("ConstantConditions") + protected Toolbar getToolbar() { + try { + return (Toolbar) getActivity().findViewById(new ContextUtils(getActivity()).getResId(ContextUtils.ResType.ID, "toolbar")); + } catch (Exception e) { + return null; + } + } } diff --git a/app/src/main/java/net/gsantner/opoc/android/dummy/MenuItemDummy.java b/app/src/main/java/net/gsantner/opoc/android/dummy/MenuItemDummy.java new file mode 100644 index 00000000..5d3b5f97 --- /dev/null +++ b/app/src/main/java/net/gsantner/opoc/android/dummy/MenuItemDummy.java @@ -0,0 +1,351 @@ +/* + * Maintained 2017-2023 by Gregor Santner + * License: Creative Commons Zero (CC0 1.0) / Public Domain + * http://creativecommons.org/publicdomain/zero/1.0/ + * + * You can do whatever you want with this. If we meet some day, and you think it is worth it, + * you can buy me a drink in return. Provided as is without any kind of warranty. Do not blame + * or ask for support if something goes wrong. - Gregor Santner + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package net.gsantner.opoc.android.dummy; + +import android.content.ComponentName; +import android.content.Intent; +import android.graphics.drawable.Drawable; +import android.view.ActionProvider; +import android.view.ContextMenu; +import android.view.KeyEvent; +import android.view.MenuItem; +import android.view.SubMenu; +import android.view.View; + +public class MenuItemDummy implements MenuItem { + private final int _itemId; + + public MenuItemDummy(final int itemId) { + _itemId = itemId; + } + + @Override + public int getItemId() { + return _itemId; + } + + @Override + public int getGroupId() { + return 0; + } + + @Override + public int getOrder() { + return 0; + } + + @Override + public MenuItem setTitle(CharSequence title) { + return null; + } + + @Override + public MenuItem setTitle(int title) { + return null; + } + + @Override + public CharSequence getTitle() { + return null; + } + + @Override + public MenuItem setTitleCondensed(CharSequence title) { + return null; + } + + @Override + public CharSequence getTitleCondensed() { + return null; + } + + @Override + public MenuItem setIcon(Drawable icon) { + return null; + } + + @Override + public MenuItem setIcon(int iconRes) { + return null; + } + + @Override + public Drawable getIcon() { + return null; + } + + @Override + public MenuItem setIntent(Intent intent) { + return null; + } + + @Override + public Intent getIntent() { + return null; + } + + @Override + public MenuItem setShortcut(char numericChar, char alphaChar) { + return null; + } + + @Override + public MenuItem setNumericShortcut(char numericChar) { + return null; + } + + @Override + public char getNumericShortcut() { + return 0; + } + + @Override + public MenuItem setAlphabeticShortcut(char alphaChar) { + return null; + } + + @Override + public char getAlphabeticShortcut() { + return 0; + } + + @Override + public MenuItem setCheckable(boolean checkable) { + return null; + } + + @Override + public boolean isCheckable() { + return false; + } + + @Override + public MenuItem setChecked(boolean checked) { + return null; + } + + @Override + public boolean isChecked() { + return false; + } + + @Override + public MenuItem setVisible(boolean visible) { + return null; + } + + @Override + public boolean isVisible() { + return false; + } + + @Override + public MenuItem setEnabled(boolean enabled) { + return null; + } + + @Override + public boolean isEnabled() { + return false; + } + + @Override + public boolean hasSubMenu() { + return false; + } + + @Override + public SubMenu getSubMenu() { + return null; + } + + @Override + public MenuItem setOnMenuItemClickListener(OnMenuItemClickListener menuItemClickListener) { + return null; + } + + @Override + public ContextMenu.ContextMenuInfo getMenuInfo() { + return null; + } + + @Override + public void setShowAsAction(int actionEnum) { + } + + @Override + public MenuItem setShowAsActionFlags(int actionEnum) { + return null; + } + + @Override + public MenuItem setActionView(View view) { + return null; + } + + @Override + public MenuItem setActionView(int resId) { + return null; + } + + @Override + public View getActionView() { + return null; + } + + @Override + public MenuItem setActionProvider(ActionProvider actionProvider) { + return null; + } + + @Override + public ActionProvider getActionProvider() { + return null; + } + + @Override + public boolean expandActionView() { + return false; + } + + @Override + public boolean collapseActionView() { + return false; + } + + @Override + public boolean isActionViewExpanded() { + return false; + } + + @Override + public MenuItem setOnActionExpandListener(OnActionExpandListener listener) { + return null; + } + + + public static class Menu implements android.view.Menu { + @Override + public MenuItem add(CharSequence title) { + return add(0, 0, 0, ""); + } + + @Override + public MenuItem add(int titleRes) { + return add(0, 0, 0, ""); + } + + @Override + public MenuItem add(int groupId, int itemId, int order, CharSequence title) { + return new MenuItemDummy(itemId); + } + + @Override + public MenuItem add(int groupId, int itemId, int order, int titleRes) { + return add(0, 0, 0, ""); + } + + @Override + public SubMenu addSubMenu(CharSequence title) { + return null; + } + + @Override + public SubMenu addSubMenu(int titleRes) { + return null; + } + + @Override + public SubMenu addSubMenu(int groupId, int itemId, int order, CharSequence title) { + return null; + } + + @Override + public SubMenu addSubMenu(int groupId, int itemId, int order, int titleRes) { + return null; + } + + @Override + public int addIntentOptions(int groupId, int itemId, int order, ComponentName caller, Intent[] specifics, Intent intent, int flags, MenuItem[] outSpecificItems) { + return 0; + } + + @Override + public void removeItem(int id) { + } + + @Override + public void removeGroup(int groupId) { + } + + @Override + public void clear() { + } + + @Override + public void setGroupCheckable(int group, boolean checkable, boolean exclusive) { + } + + @Override + public void setGroupVisible(int group, boolean visible) { + } + + @Override + public void setGroupEnabled(int group, boolean enabled) { + } + + @Override + public boolean hasVisibleItems() { + return false; + } + + @Override + public MenuItem findItem(int id) { + return null; + } + + @Override + public int size() { + return 0; + } + + @Override + public MenuItem getItem(int index) { + return null; + } + + @Override + public void close() { + } + + @Override + public boolean performShortcut(int keyCode, KeyEvent event, int flags) { + return false; + } + + @Override + public boolean isShortcutKey(int keyCode, KeyEvent event) { + return false; + } + + @Override + public boolean performIdentifierAction(int id, int flags) { + return false; + } + + @Override + public void setQwertyMode(boolean isQwerty) { + } + } +} diff --git a/app/src/main/java/net/gsantner/opoc/android/dummy/TextWatcherDummy.java b/app/src/main/java/net/gsantner/opoc/android/dummy/TextWatcherDummy.java new file mode 100644 index 00000000..e6a5ea94 --- /dev/null +++ b/app/src/main/java/net/gsantner/opoc/android/dummy/TextWatcherDummy.java @@ -0,0 +1,58 @@ +/* + * Maintained 2017-2023 by Gregor Santner + * License: Creative Commons Zero (CC0 1.0) / Public Domain + * http://creativecommons.org/publicdomain/zero/1.0/ + * + * You can do whatever you want with this. If we meet some day, and you think it is worth it, + * you can buy me a drink in return. Provided as is without any kind of warranty. Do not blame + * or ask for support if something goes wrong. - Gregor Santner + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package net.gsantner.opoc.android.dummy; + +import android.text.Editable; +import android.text.TextWatcher; + +import net.gsantner.opoc.util.Callback; + +@SuppressWarnings({"unused", "SpellCheckingInspection"}) +public class TextWatcherDummy implements TextWatcher { + @Override + public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) { + } + + @Override + public void onTextChanged(final CharSequence s, final int start, final int before, final int count) { + } + + @Override + public void afterTextChanged(final Editable s) { + } + + public static TextWatcher before(final Callback.a4 impl) { + return new TextWatcherDummy() { + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + impl.callback(s, start, count, after); + } + }; + } + + public static TextWatcher on(final Callback.a4 impl) { + return new TextWatcherDummy() { + public void onTextChanged(final CharSequence s, final int start, final int before, final int count) { + impl.callback(s, start, before, count); + } + }; + } + + public static TextWatcher after(final Callback.a1 impl) { + return new TextWatcherDummy() { + public void afterTextChanged(final Editable s) { + impl.callback(s); + } + }; + } +} diff --git a/app/src/main/java/net/gsantner/opoc/format/markdown/SimpleMarkdownParser.java b/app/src/main/java/net/gsantner/opoc/format/markdown/SimpleMarkdownParser.java index adaafa3b..ea117a8a 100644 --- a/app/src/main/java/net/gsantner/opoc/format/markdown/SimpleMarkdownParser.java +++ b/app/src/main/java/net/gsantner/opoc/format/markdown/SimpleMarkdownParser.java @@ -1,7 +1,6 @@ /*####################################################### * - * Maintained by Gregor Santner, 2016- - * https://gsantner.net/ + * Maintained 2018-2023 by Gregor Santner * * License: Apache 2.0 * https://github.com/gsantner/opoc/#licensing @@ -93,8 +92,8 @@ public class SimpleMarkdownParser { .replaceAll("!\\[(.*?)\\]\\((.*?)\\)", "$1") // img .replaceAll("<(http|https):\\/\\/(.*)>", "$1://$2") // a href (DEP: img) .replaceAll("\\[(.*?)\\]\\((.*?)\\)", "$1") // a href (DEP: img) - .replaceAll("(?m)^([-*] )(.*)$", " $2 ") // unordered list + end line - .replaceAll("(?m)^ (-|\\*) ([^<]*)$", "   $2 ") // unordered list2 + end line + .replaceAll("(?m)^[-*] (.*)$", " $1 ") // unordered list + end line + .replaceAll("(?m)^ [-*] (.*)$", "   $1 ") // unordered list2 + end line .replaceAll("`([^<]*)`", "$1") // code .replace("\\*", "●") // temporary replace escaped star symbol .replaceAll("(?m)\\*\\*(.*)\\*\\*", "$1") // bold (DEP: temp star) @@ -111,6 +110,7 @@ public class SimpleMarkdownParser { public String filter(String text) { text = text .replace("New:", "New:") + .replace("New features:", "New:") .replace("Added:", "Added:") .replace("Add:", "Add:") .replace("Fixed:", "Fixed:") @@ -124,6 +124,26 @@ public class SimpleMarkdownParser { return text; } }; + public final static SmpFilter FILTER_H_TO_SUP = new SmpFilter() { + @Override + public String filter(String text) { + text = text + .replace("

", "") + .replace("

", "") + .replace("

", "") + .replace("

", "") + .replace("

", "") + .replace("

", "") + ; + return text; + } + }; + public final static SmpFilter FILTER_NONE = new SmpFilter() { + @Override + public String filter(String text) { + return text; + } + }; //######################## //## Singleton diff --git a/app/src/main/java/net/gsantner/opoc/preference/PropertyBackend.java b/app/src/main/java/net/gsantner/opoc/preference/PropertyBackend.java index c27d9bce..f96af499 100644 --- a/app/src/main/java/net/gsantner/opoc/preference/PropertyBackend.java +++ b/app/src/main/java/net/gsantner/opoc/preference/PropertyBackend.java @@ -1,7 +1,6 @@ /*####################################################### * - * Maintained by Gregor Santner, 2018- - * https://gsantner.net/ + * Maintained 2018-2023 by Gregor Santner * * License: Apache 2.0 * https://github.com/gsantner/opoc/#licensing @@ -45,5 +44,4 @@ public interface PropertyBackend { TTHIS setIntList(TKEY key, List value); TTHIS setStringList(TKEY key, List value); - } diff --git a/app/src/main/java/net/gsantner/opoc/preference/SharedPreferencesPropertyBackend.java b/app/src/main/java/net/gsantner/opoc/preference/SharedPreferencesPropertyBackend.java index 275d88a5..9f5c948c 100644 --- a/app/src/main/java/net/gsantner/opoc/preference/SharedPreferencesPropertyBackend.java +++ b/app/src/main/java/net/gsantner/opoc/preference/SharedPreferencesPropertyBackend.java @@ -1,7 +1,6 @@ /*####################################################### * - * Maintained by Gregor Santner, 2016- - * https://gsantner.net/ + * Maintained 2016-2023 by Gregor Santner * * License: Apache 2.0 * https://github.com/gsantner/opoc/#licensing @@ -42,8 +41,12 @@ import android.support.annotation.StringRes; import android.support.v4.content.ContextCompat; import android.text.TextUtils; +import java.io.File; import java.util.ArrayList; import java.util.Arrays; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; import java.util.List; @@ -56,6 +59,8 @@ public class SharedPreferencesPropertyBackend implements PropertyBackend 0 ? pref[0] : _prefApp); } + + public static void limitListTo(final List list, int maxSize, boolean removeDuplicates) { + Object o; + int pos; + + for (int i = 0; removeDuplicates && i < list.size(); i++) { + o = list.get(i); + while ((pos = list.lastIndexOf(o)) != i && pos >= 0) { + list.remove(pos); + } + } + while ((pos = list.size()) > maxSize && pos > 0) { + list.remove(list.size() - 1); + } + } + // // Getter for resources // @@ -147,6 +168,14 @@ public class SharedPreferencesPropertyBackend implements PropertyBackend values, final SharedPreferences pref) { @@ -190,10 +223,8 @@ public class SharedPreferencesPropertyBackend implements PropertyBackend getStringListOne(String key, final SharedPreferences pref) { ArrayList ret = new ArrayList<>(); - String value = pref - .getString(key, ARRAY_SEPARATOR) - .replace(ARRAY_SEPARATOR_SUBSTITUTE, ARRAY_SEPARATOR); - if (value.equals(ARRAY_SEPARATOR)) { + String value = getString(key, ARRAY_SEPARATOR).replace(ARRAY_SEPARATOR_SUBSTITUTE, ARRAY_SEPARATOR); + if (value.equals(ARRAY_SEPARATOR) || TextUtils.isEmpty(value)) { return ret; } ret.addAll(Arrays.asList(value.split(ARRAY_SEPARATOR))); @@ -227,6 +258,7 @@ public class SharedPreferencesPropertyBackend implements PropertyBackend getStringList(@StringRes int keyResourceId, final SharedPreferences... pref) { return getStringListOne(rstr(keyResourceId), gp(pref)); } @@ -247,11 +279,15 @@ public class SharedPreferencesPropertyBackend implements PropertyBackend getIntListOne(String key, final SharedPreferences pref) { ArrayList ret = new ArrayList<>(); - String value = pref.getString(key, ARRAY_SEPARATOR); + String value = getString(key, ARRAY_SEPARATOR); if (value.equals(ARRAY_SEPARATOR)) { return ret; } @@ -331,11 +367,15 @@ public class SharedPreferencesPropertyBackend implements PropertyBackend= 23 || begin < 0) ? 0 : begin; + end = (end >= 23 || end < 0) ? 0 : end; + int h = Calendar.getInstance().get(Calendar.HOUR_OF_DAY); + return h >= begin && h <= end; + } + + /** + * Substract current datetime by given amount of days + */ + public Date getDateOfDaysAgo(int days) { + Calendar cal = new GregorianCalendar(); + cal.add(Calendar.DATE, -days); + return cal.getTime(); + } + + /** + * Substract current datetime by given amount of days and check if the given date passed + */ + public boolean didDaysPassedSince(Date date, int days) { + if (date == null || days < 0) { + return false; + } + return date.before(getDateOfDaysAgo(days)); + } + + public boolean afterDaysTrue(String key, int daysSinceLastTime, int firstTime, final SharedPreferences... pref) { + Date d = new Date(System.currentTimeMillis()); + if (!contains(key)) { + d = getDateOfDaysAgo(daysSinceLastTime - firstTime); + setLong(key, d.getTime()); + return firstTime < 1; + } else { + d = new Date(getLong(key, d.getTime())); + } + boolean trigger = didDaysPassedSince(d, daysSinceLastTime); + if (trigger) { + setLong(key, new Date(System.currentTimeMillis()).getTime()); + } + return trigger; + } + + public static void clearDebugLog() { + _debugLog = ""; + } + + public static String getDebugLog() { + return _debugLog; + } + + public static synchronized void appendDebugLog(String text) { + _debugLog += "[" + new Date().toString() + "] " + text + "\n"; + } + + public static boolean ne(final String str) { + return str != null && !str.trim().isEmpty(); + } + + public static boolean fexists(final String fp) { + return ne(fp) && (new File(fp)).exists(); + } } diff --git a/app/src/main/java/net/gsantner/opoc/preference/nonsupport/LanguagePreference.java b/app/src/main/java/net/gsantner/opoc/preference/nonsupport/LanguagePreference.java index 76284a11..04c3be4f 100644 --- a/app/src/main/java/net/gsantner/opoc/preference/nonsupport/LanguagePreference.java +++ b/app/src/main/java/net/gsantner/opoc/preference/nonsupport/LanguagePreference.java @@ -1,7 +1,6 @@ /*####################################################### * - * Maintained by Gregor Santner, 2017- - * https://gsantner.net/ + * Maintained 2017-2023 by Gregor Santner * * License: Apache 2.0 * https://github.com/gsantner/opoc/#licensing diff --git a/app/src/main/java/net/gsantner/opoc/ui/SearchOrCustomTextDialog.java b/app/src/main/java/net/gsantner/opoc/ui/SearchOrCustomTextDialog.java new file mode 100644 index 00000000..cc703f98 --- /dev/null +++ b/app/src/main/java/net/gsantner/opoc/ui/SearchOrCustomTextDialog.java @@ -0,0 +1,394 @@ +/*####################################################### + * + * Maintained 2017-2023 by Gregor Santner + * + * License: Apache 2.0 + * https://github.com/gsantner/opoc/#licensing + * https://www.apache.org/licenses/LICENSE-2.0 + * +#########################################################*/ +package net.gsantner.opoc.ui; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.Dialog; +import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.Color; +import android.graphics.Typeface; +import android.os.Build; +import android.support.annotation.ColorInt; +import android.support.annotation.DrawableRes; +import android.support.annotation.LayoutRes; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.StringRes; +import android.support.v7.app.AlertDialog; +import android.support.v7.widget.AppCompatEditText; +import android.support.v7.widget.TooltipCompat; +import android.text.InputType; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.TextUtils; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.Checkable; +import android.widget.Filter; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ListView; +import android.widget.TextView; + +import net.gsantner.opoc.android.dummy.TextWatcherDummy; +import net.gsantner.opoc.util.Callback; +import net.gsantner.opoc.util.ContextUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.regex.Pattern; + +@SuppressLint("SetTextI18n") +public class SearchOrCustomTextDialog { + + public static class DialogOptions { + + // Callback for search text or text of single item + @Nullable + public Callback.a1 callback = null; + + // Callback for indices of selected items. + // List will contain single item if isMultiSelectEnabled == false; + @Nullable + public Callback.a1> positionCallback = null; + + public boolean isMultiSelectEnabled = false; + public List data = null; + public List highlightData = null; + public List iconsForData; + public String messageText = ""; + public String defaultText = ""; + public boolean isSearchEnabled = true; + public boolean isDarkDialog = false; + public int dialogWidthDp = WindowManager.LayoutParams.MATCH_PARENT; + public int dialogHeightDp = WindowManager.LayoutParams.WRAP_CONTENT; + public int gravity = Gravity.NO_GRAVITY; + public int searchInputType = InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS; + public boolean searchIsRegex = false; + public Callback.a1 highlighter = null; + public String extraFilter = null; + public List preSelected = null; + + public Callback.a0 neutralButtonCallback = null; + + @ColorInt + public int textColor = 0xFF000000; + @ColorInt + public int highlightColor = 0xFF00FF00; + @StringRes + public int cancelButtonText = android.R.string.cancel; + @StringRes + public int neutralButtonText = 0; + @StringRes + public int okButtonText = android.R.string.ok; + @StringRes + public int titleText = 0; + @StringRes + public int searchHintText = android.R.string.search_go; + @DrawableRes + public int clearInputIcon = android.R.drawable.ic_input_delete; + } + + private static class Adapter extends ArrayAdapter { + @LayoutRes + private final int _layout; + private final int _layoutHeight; + private final LayoutInflater _inflater; + private final DialogOptions _dopt; + private final List _filteredItems; + private final Set _selectedItems; + private final Pattern _extraPattern; + + public static Adapter create(final Context context, final DialogOptions dopt) { + return new Adapter(context, dopt, dopt.isMultiSelectEnabled ? android.R.layout.simple_list_item_multiple_choice : android.R.layout.simple_list_item_1, new ArrayList<>()); + } + + private Adapter(final Context context, final DialogOptions dopt, final int layout, final List filteredItems) { + super(context, layout, filteredItems); + _layout = layout; + _filteredItems = filteredItems; + _inflater = LayoutInflater.from(context); + _dopt = dopt; + _extraPattern = (_dopt.extraFilter == null ? null : Pattern.compile(_dopt.extraFilter)); + _selectedItems = new HashSet<>(_dopt.preSelected != null ? _dopt.preSelected : Collections.emptyList()); + ContextUtils cu = new ContextUtils(context); + _layoutHeight = (int) cu.convertDpToPx(36); + cu.freeContextRef(); + } + + @NonNull + @Override + public View getView(int pos, @Nullable View convertView, @NonNull ViewGroup parent) { + final int index = getItem(pos); + + final TextView textView; + if (convertView == null) { + textView = (TextView) _inflater.inflate(_layout, parent, false); + textView.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)); + textView.setMinHeight(_layoutHeight); + } else { + textView = (TextView) convertView; + } + + if (textView instanceof Checkable) { + ((Checkable) textView).setChecked(_selectedItems.contains(index)); + } + + if (index >= 0 && _dopt.iconsForData != null && index < _dopt.iconsForData.size() && _dopt.iconsForData.get(index) != 0) { + textView.setCompoundDrawablesWithIntrinsicBounds(_dopt.iconsForData.get(index), 0, 0, 0); + textView.setCompoundDrawablePadding(32); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + textView.setCompoundDrawableTintList(ColorStateList.valueOf(_dopt.isDarkDialog ? Color.WHITE : Color.BLACK)); + } + } else { + textView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); + } + + final CharSequence text = _dopt.data.get(index).toString(); + if (_dopt.highlightData != null) { + final boolean hl = _dopt.highlightData.contains(text); + textView.setTextColor(hl ? _dopt.highlightColor : _dopt.textColor); + textView.setTypeface(null, hl ? Typeface.BOLD : Typeface.NORMAL); + } + + if (_dopt.highlighter != null) { + Spannable s = new SpannableString(text); + _dopt.highlighter.callback(s); + textView.setText(s); + } else { + textView.setText(text); + } + + return textView; + } + + @NonNull + @Override + public Filter getFilter() { + return new Filter() { + @SuppressWarnings("unchecked") + @Override + protected void publishResults(final CharSequence constraint, final FilterResults results) { + _filteredItems.clear(); + _filteredItems.addAll((List) results.values); + notifyDataSetChanged(); + } + + @Override + protected FilterResults performFiltering(final CharSequence constraint) { + final List resList = new ArrayList<>(); + + if (_dopt.data != null) { + final String fil = constraint.toString(); + final boolean emptySearch = fil.isEmpty(); + for (int i = 0; i < _dopt.data.size(); i++) { + final String str = _dopt.data.get(i).toString(); + final boolean matchExtra = (_extraPattern == null) || _extraPattern.matcher(str).find(); + final Locale locale = Locale.getDefault(); + final boolean matchNormal = str.toLowerCase(locale).contains(fil.toLowerCase(locale)); + final boolean matchRegex = _dopt.searchIsRegex && (str.matches(fil)); + if (matchExtra && (matchNormal || matchRegex || emptySearch)) { + resList.add(i); + } + } + } + + final FilterResults res = new FilterResults(); + res.values = resList; + res.count = resList.size(); + return res; + } + }; + } + } + + public static void showMultiChoiceDialogWithSearchFilterUI(final Activity activity, final DialogOptions dopt) { + final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(activity, dopt.isDarkDialog + ? android.support.v7.appcompat.R.style.Theme_AppCompat_Dialog + : android.support.v7.appcompat.R.style.Theme_AppCompat_Light_Dialog + ); + final Adapter listAdapter = Adapter.create(activity, dopt); + + final AppCompatEditText searchEditText = new AppCompatEditText(activity); + searchEditText.setText(dopt.defaultText); + searchEditText.setSingleLine(true); + searchEditText.setMaxLines(1); + searchEditText.setTextColor(dopt.textColor); + searchEditText.setHintTextColor((dopt.textColor & 0x00FFFFFF) | 0x99000000); + searchEditText.setHint(dopt.searchHintText); + searchEditText.setInputType(dopt.searchInputType == 0 ? searchEditText.getInputType() : dopt.searchInputType); + searchEditText.addTextChangedListener(TextWatcherDummy.after((cbEditable) -> listAdapter.getFilter().filter(cbEditable))); + + final ContextUtils cu = new ContextUtils(activity); + final int margin = (int) cu.convertDpToPx(8); + cu.freeContextRef(); + + final LinearLayout searchLayout = new LinearLayout(activity); + searchLayout.setOrientation(LinearLayout.HORIZONTAL); + + LinearLayout.LayoutParams lp; + lp = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.MATCH_PARENT, 1); + lp.gravity = Gravity.START | Gravity.BOTTOM; + searchLayout.addView(searchEditText, lp); + + // 'Button to clear the search box' + final ImageView clearButton = new ImageView(activity); + clearButton.setImageResource(dopt.clearInputIcon); + TooltipCompat.setTooltipText(clearButton, activity.getString(android.R.string.cancel)); + clearButton.setColorFilter(dopt.isDarkDialog ? Color.WHITE : Color.parseColor("#ff505050")); + lp = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.MATCH_PARENT, 0); + lp.gravity = Gravity.END | Gravity.CENTER_VERTICAL; + lp.setMargins(margin, 0, (int) (margin * 1.5), 0); + searchLayout.addView(clearButton, lp); + clearButton.setOnClickListener((v) -> searchEditText.setText("")); + + final ListView listView = new ListView(activity); + final LinearLayout linearLayout = new LinearLayout(activity); + listView.setAdapter(listAdapter); + listView.setVisibility(dopt.data != null && !dopt.data.isEmpty() ? View.VISIBLE : View.GONE); + linearLayout.setOrientation(LinearLayout.VERTICAL); + + if (dopt.isSearchEnabled) { + lp = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); + lp.setMargins(margin, margin / 2, margin, margin / 2); + linearLayout.addView(searchLayout, lp); + } + + final LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 0); + layoutParams.weight = 1; + linearLayout.addView(listView, layoutParams); + if (!TextUtils.isEmpty(dopt.messageText)) { + dialogBuilder.setMessage(dopt.messageText); + } + + dialogBuilder.setView(linearLayout) + .setOnCancelListener(null) + .setNegativeButton(dopt.cancelButtonText, (dialogInterface, i) -> dialogInterface.dismiss()); + + if (dopt.titleText != 0) { + dialogBuilder.setTitle(dopt.titleText); + } + + // Ok button action + if ((dopt.isSearchEnabled && dopt.callback != null) || (dopt.isMultiSelectEnabled)) { + dialogBuilder.setPositiveButton(dopt.okButtonText, (dialogInterface, i) -> { + final String searchText = dopt.isSearchEnabled ? searchEditText.getText().toString() : null; + if (dopt.positionCallback != null && !listAdapter._selectedItems.isEmpty()) { + final List sel = new ArrayList<>(listAdapter._selectedItems); + Collections.sort(sel); + dopt.positionCallback.callback(sel); + } else if (dopt.callback != null && !TextUtils.isEmpty(searchText)) { + dopt.callback.callback(searchText); + } + }); + } + + // Setup neutralbutton + if (dopt.neutralButtonCallback != null && dopt.neutralButtonText != 0) { + dialogBuilder.setNeutralButton(dopt.neutralButtonText, (dialogInterface, i) -> { + dopt.neutralButtonCallback.callback(); + }); + } + + final AlertDialog dialog = dialogBuilder.create(); + + searchEditText.setOnKeyListener((keyView, keyCode, keyEvent) -> { + if ((keyEvent.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) { + dialog.dismiss(); + if (dopt.callback != null && !TextUtils.isEmpty(searchEditText.getText().toString())) { + dopt.callback.callback(searchEditText.getText().toString()); + } + return true; + } + return false; + }); + + Window w; + if ((w = dialog.getWindow()) != null && dopt.isSearchEnabled) { + w.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE | WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); + } + dialog.show(); + if ((w = dialog.getWindow()) != null) { + int ds_w = dopt.dialogWidthDp < 100 ? dopt.dialogWidthDp : ((int) (dopt.dialogWidthDp * activity.getResources().getDisplayMetrics().density)); + int ds_h = dopt.dialogHeightDp < 100 ? dopt.dialogHeightDp : ((int) (dopt.dialogHeightDp * activity.getResources().getDisplayMetrics().density)); + w.setLayout(ds_w, ds_h); + } + + if ((w = dialog.getWindow()) != null && dopt.gravity != Gravity.NO_GRAVITY) { + WindowManager.LayoutParams wlp = w.getAttributes(); + wlp.gravity = dopt.gravity; + w.setAttributes(wlp); + } + + if (dopt.isSearchEnabled) { + searchEditText.requestFocus(); + } + if (dopt.defaultText != null) { + listAdapter.getFilter().filter(searchEditText.getText()); + } + + // Helper function to trigger callback with single item + final Callback.b1 directActivate = (position) -> { + final int index = listAdapter._filteredItems.get(position); + dialog.dismiss(); + if (dopt.callback != null) { + dopt.callback.callback(dopt.data.get(index).toString()); + } + if (dopt.positionCallback != null) { + dopt.positionCallback.callback(Collections.singletonList(index)); + } + return true; + }; + + // Helper function to append selection count to OK button + final Button okButton = dialog.getButton(Dialog.BUTTON_POSITIVE); + final String okText = activity.getString(dopt.okButtonText) + (dopt.isMultiSelectEnabled ? " (%d)" : ""); + final Callback.a0 setOkButtonState = () -> { + okButton.setText(okText.replace("%d", Integer.toString(listAdapter._selectedItems.size()))); + }; + + // Set ok button text initially + setOkButtonState.callback(); + + // Item click action + listView.setOnItemClickListener((parent, textView, pos, id) -> { + if (dopt.isMultiSelectEnabled) { + final int index = listAdapter._filteredItems.get(pos); + if (listAdapter._selectedItems.contains(index)) { + listAdapter._selectedItems.remove(index); + } else { + listAdapter._selectedItems.add(index); + } + if (textView instanceof Checkable) { + ((Checkable) textView).setChecked(listAdapter._selectedItems.contains(index)); + } + setOkButtonState.callback(); + } else { + directActivate.callback(pos); + } + }); + + // long click always activates + listView.setOnItemLongClickListener((parent, view, pos, id) -> directActivate.callback(pos)); + } +} diff --git a/app/src/main/java/net/gsantner/opoc/util/ActivityUtils.java b/app/src/main/java/net/gsantner/opoc/util/ActivityUtils.java index 53a86cc1..741b3b76 100644 --- a/app/src/main/java/net/gsantner/opoc/util/ActivityUtils.java +++ b/app/src/main/java/net/gsantner/opoc/util/ActivityUtils.java @@ -1,23 +1,30 @@ /*####################################################### * - * Maintained by Gregor Santner, 2016- - * https://gsantner.net/ + * Maintained 2016-2023 by Gregor Santner * - * License: Apache 2.0 - * https://github.com/gsantner/opoc/#licensing - * https://www.apache.org/licenses/LICENSE-2.0 + * License of this file: Apache 2.0 + * https://www.apache.org/licenses/LICENSE-2.0 + * https://github.com/gsantner/opoc/#licensing * #########################################################*/ package net.gsantner.opoc.util; import android.app.Activity; +import android.app.ActivityManager; import android.content.ActivityNotFoundException; +import android.content.ComponentName; +import android.content.Context; import android.content.DialogInterface; import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.res.TypedArray; import android.net.Uri; import android.os.Build; +import android.provider.CalendarContract; +import android.support.annotation.ColorInt; import android.support.annotation.StringRes; import android.support.design.widget.Snackbar; +import android.support.v4.content.ContextCompat; import android.support.v7.app.AlertDialog; import android.support.v7.widget.AppCompatTextView; import android.text.Html; @@ -25,12 +32,16 @@ import android.text.SpannableString; import android.text.method.LinkMovementMethod; import android.util.TypedValue; import android.view.View; +import android.view.Window; import android.view.WindowManager; import android.view.inputmethod.InputMethodManager; import android.webkit.WebView; +import android.widget.ScrollView; + +import java.util.List; -@SuppressWarnings({"WeakerAccess", "unused", "SameParameterValue", "SpellCheckingInspection"}) +@SuppressWarnings({"WeakerAccess", "unused", "SameParameterValue", "SpellCheckingInspection", "rawtypes", "UnusedReturnValue"}) public class ActivityUtils extends net.gsantner.opoc.util.ContextUtils { //######################## //## Members, Constructors @@ -42,6 +53,12 @@ public class ActivityUtils extends net.gsantner.opoc.util.ContextUtils { _activity = activity; } + @Override + public void freeContextRef() { + super.freeContextRef(); + _activity = null; + } + //######################## //## Methods //######################## @@ -80,9 +97,11 @@ public class ActivityUtils extends net.gsantner.opoc.util.ContextUtils { } - public void showSnackBar(@StringRes int stringResId, boolean showLong) { - Snackbar.make(_activity.findViewById(android.R.id.content), stringResId, - showLong ? Snackbar.LENGTH_LONG : Snackbar.LENGTH_SHORT).show(); + public Snackbar showSnackBar(@StringRes int stringResId, boolean showLong) { + Snackbar s = Snackbar.make(_activity.findViewById(android.R.id.content), stringResId, + showLong ? Snackbar.LENGTH_LONG : Snackbar.LENGTH_SHORT); + s.show(); + return s; } public void showSnackBar(@StringRes int stringResId, boolean showLong, @StringRes int actionResId, View.OnClickListener listener) { @@ -92,18 +111,58 @@ public class ActivityUtils extends net.gsantner.opoc.util.ContextUtils { .show(); } - public void hideSoftKeyboard() { - InputMethodManager imm = (InputMethodManager) _activity.getSystemService(Activity.INPUT_METHOD_SERVICE); - if (imm != null && _activity.getCurrentFocus() != null && _activity.getCurrentFocus().getWindowToken() != null) { - imm.hideSoftInputFromWindow(_activity.getCurrentFocus().getWindowToken(), 0); + public ActivityUtils setSoftKeyboardVisibile(boolean visible, View... editView) { + final Activity activity = _activity; + if (activity != null) { + final View v = (editView != null && editView.length > 0) ? (editView[0]) : (activity.getCurrentFocus() != null && activity.getCurrentFocus().getWindowToken() != null ? activity.getCurrentFocus() : null); + final InputMethodManager imm = (InputMethodManager) activity.getSystemService(Activity.INPUT_METHOD_SERVICE); + if (v != null && imm != null) { + Runnable r = () -> { + if (visible) { + v.requestFocus(); + imm.showSoftInput(v, InputMethodManager.SHOW_FORCED); + } else { + imm.hideSoftInputFromWindow(v.getWindowToken(), 0); + } + }; + r.run(); + for (int d : new int[]{100, 350}) { + v.postDelayed(r, d); + } + } } + return this; } - public void showSoftKeyboard() { - InputMethodManager imm = (InputMethodManager) _activity.getSystemService(Activity.INPUT_METHOD_SERVICE); - if (imm != null && _activity.getCurrentFocus() != null && _activity.getCurrentFocus().getWindowToken() != null) { - imm.showSoftInput(_activity.getCurrentFocus(), InputMethodManager.SHOW_FORCED); + public ActivityUtils hideSoftKeyboard() { + if (_activity != null) { + InputMethodManager imm = (InputMethodManager) _activity.getSystemService(Activity.INPUT_METHOD_SERVICE); + if (imm != null && _activity.getCurrentFocus() != null && _activity.getCurrentFocus().getWindowToken() != null) { + imm.hideSoftInputFromWindow(_activity.getCurrentFocus().getWindowToken(), 0); + } } + return this; + } + + public ActivityUtils showSoftKeyboard() { + if (_activity != null) { + InputMethodManager imm = (InputMethodManager) _activity.getSystemService(Activity.INPUT_METHOD_SERVICE); + if (imm != null && _activity.getCurrentFocus() != null && _activity.getCurrentFocus().getWindowToken() != null) { + showSoftKeyboard(_activity.getCurrentFocus()); + } + } + return this; + } + + + public ActivityUtils showSoftKeyboard(View textInputView) { + if (_activity != null) { + InputMethodManager imm = (InputMethodManager) _activity.getSystemService(Activity.INPUT_METHOD_SERVICE); + if (imm != null && textInputView != null) { + imm.showSoftInput(textInputView, InputMethodManager.SHOW_FORCED); + } + } + return this; } public void showDialogWithHtmlTextView(@StringRes int resTitleId, String html) { @@ -111,19 +170,23 @@ public class ActivityUtils extends net.gsantner.opoc.util.ContextUtils { } public void showDialogWithHtmlTextView(@StringRes int resTitleId, String text, boolean isHtml, DialogInterface.OnDismissListener dismissedListener) { + ScrollView scroll = new ScrollView(_context); AppCompatTextView textView = new AppCompatTextView(_context); - int padding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16, - _context.getResources().getDisplayMetrics()); - textView.setMovementMethod(new LinkMovementMethod()); - textView.setPadding(padding, 0, padding, 0); + int padding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16, _context.getResources().getDisplayMetrics()); + scroll.setPadding(padding, 0, padding, 0); + scroll.addView(textView); + textView.setMovementMethod(new LinkMovementMethod()); textView.setText(isHtml ? new SpannableString(Html.fromHtml(text)) : text); + textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 17); + AlertDialog.Builder dialog = new AlertDialog.Builder(_context) - .setPositiveButton(android.R.string.ok, null) - .setOnDismissListener(dismissedListener) - .setTitle(resTitleId) - .setView(textView); - dialog.show(); + .setPositiveButton(android.R.string.ok, null).setOnDismissListener(dismissedListener) + .setView(scroll); + if (resTitleId != 0) { + dialog.setTitle(resTitleId); + } + dialogFullWidth(dialog.show(), true, false); } public void showDialogWithRawFileInWebView(String fileInRaw, @StringRes int resTitleId) { @@ -133,11 +196,11 @@ public class ActivityUtils extends net.gsantner.opoc.util.ContextUtils { .setPositiveButton(android.R.string.ok, null) .setTitle(resTitleId) .setView(wv); - dialog.show(); + dialogFullWidth(dialog.show(), true, false); } // Toggle with no param, else set visibility according to first bool - public void toggleStatusbarVisibility(boolean... optionalForceVisible) { + public ActivityUtils toggleStatusbarVisibility(boolean... optionalForceVisible) { WindowManager.LayoutParams attrs = _activity.getWindow().getAttributes(); int flag = WindowManager.LayoutParams.FLAG_FULLSCREEN; if (optionalForceVisible.length == 0) { @@ -148,9 +211,10 @@ public class ActivityUtils extends net.gsantner.opoc.util.ContextUtils { attrs.flags |= flag; } _activity.getWindow().setAttributes(attrs); + return this; } - public void showGooglePlayEntryForThisApp() { + public ActivityUtils showGooglePlayEntryForThisApp() { String pkgId = "details?id=" + _activity.getPackageName(); Intent goToMarket = new Intent(Intent.ACTION_VIEW, Uri.parse("market://" + pkgId)); goToMarket.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY | @@ -160,7 +224,123 @@ public class ActivityUtils extends net.gsantner.opoc.util.ContextUtils { _activity.startActivity(goToMarket); } catch (ActivityNotFoundException e) { _activity.startActivity(new Intent(Intent.ACTION_VIEW, - Uri.parse("http://play.google.com/store/apps/" + pkgId))); + Uri.parse("https://play.google.com/store/apps/" + pkgId))); + } + return this; + } + + public ActivityUtils setStatusbarColor(int color, boolean... fromRes) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + if (fromRes != null && fromRes.length > 0 && fromRes[0]) { + color = ContextCompat.getColor(_context, color); + } + + _activity.getWindow().setStatusBarColor(color); + } + return this; + } + + public ActivityUtils setLauncherActivityEnabled(Class activityClass, boolean enable) { + try { + ComponentName component = new ComponentName(_context, activityClass); + _context.getPackageManager().setComponentEnabledSetting(component, enable ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP); + } catch (Exception ignored) { + } + return this; + } + + public boolean isLauncherEnabled(Class activityClass) { + try { + ComponentName component = new ComponentName(_context, activityClass); + return _context.getPackageManager().getComponentEnabledSetting(component) != PackageManager.COMPONENT_ENABLED_STATE_DISABLED; + } catch (Exception ignored) { + } + return false; + } + + @ColorInt + public Integer getCurrentPrimaryColor() { + TypedValue typedValue = new TypedValue(); + _context.getTheme().resolveAttribute(getResId(ResType.ATTR, "colorPrimary"), typedValue, true); + return typedValue.data; + } + + @ColorInt + public Integer getCurrentPrimaryDarkColor() { + TypedValue typedValue = new TypedValue(); + _context.getTheme().resolveAttribute(getResId(ResType.ATTR, "colorPrimaryDark"), typedValue, true); + return typedValue.data; + } + + @ColorInt + public Integer getCurrentAccentColor() { + TypedValue typedValue = new TypedValue(); + _context.getTheme().resolveAttribute(getResId(ResType.ATTR, "colorAccent"), typedValue, true); + return typedValue.data; + } + + @ColorInt + public Integer getActivityBackgroundColor() { + TypedArray array = _activity.getTheme().obtainStyledAttributes(new int[]{ + android.R.attr.colorBackground, + }); + int c = array.getColor(0, 0xFF0000); + array.recycle(); + return c; + } + + public ActivityUtils startCalendarApp() { + Uri.Builder builder = CalendarContract.CONTENT_URI.buildUpon(); + builder.appendPath("time"); + builder.appendPath(Long.toString(System.currentTimeMillis())); + Intent intent = new Intent(Intent.ACTION_VIEW, builder.build()); + _activity.startActivity(intent); + return this; + } + + /** + * Detect if the activity is currently in splitscreen/multiwindow mode + */ + public boolean isInSplitScreenMode() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + return _activity.isInMultiWindowMode(); + } + return false; + } + + /** + * Show dialog in full width / show keyboard + * + * @param dialog Get via dialog.show() + */ + public void dialogFullWidth(AlertDialog dialog, boolean fullWidth, boolean showKeyboard) { + try { + Window w; + if (dialog != null && (w = dialog.getWindow()) != null) { + if (fullWidth) { + w.setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT); + } + if (showKeyboard) { + w.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); + } + } + } catch (Exception ignored) { } } + + // Make activity/app not show up in the recents history - call before finish / System.exit + public ActivityUtils removeActivityFromHistory() { + try { + ActivityManager am = (ActivityManager) _activity.getSystemService(Context.ACTIVITY_SERVICE); + if (am != null && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { + List tasks = am.getAppTasks(); + if (tasks != null && !tasks.isEmpty()) { + tasks.get(0).setExcludeFromRecents(true); + } + } + + } catch (Exception ignored) { + } + return this; + } } diff --git a/app/src/main/java/net/gsantner/opoc/util/AdBlock.java b/app/src/main/java/net/gsantner/opoc/util/AdBlock.java index 304a4b85..c5c81678 100644 --- a/app/src/main/java/net/gsantner/opoc/util/AdBlock.java +++ b/app/src/main/java/net/gsantner/opoc/util/AdBlock.java @@ -1,7 +1,6 @@ /*####################################################### * - * Maintained by Gregor Santner, 2017- - * https://gsantner.net/ + * Maintained 2017-2023 by Gregor Santner * * License: Apache 2.0 * https://github.com/gsantner/opoc/#licensing @@ -46,8 +45,9 @@ import java.util.Set; /** * Simple Host-Based AdBlocker */ -@SuppressWarnings({"WeakerAccess", "SpellCheckingInspection", "unused"}) +@SuppressWarnings({"WeakerAccess", "SpellCheckingInspection", "unused", "TryFinallyCanBeTryWithResources"}) public class AdBlock { + private static final Object synchronizeObj = new Object(); private static final AdBlock instance = new AdBlock(); public static AdBlock getInstance() { @@ -61,7 +61,9 @@ public class AdBlock { //######################## private final Set _adblockHostsFromRaw = new HashSet<>(); private final Set _adblockHosts = new HashSet<>(); - private boolean _isLoaded; + private final List> _customBlockCallbacks = new ArrayList<>(); + private boolean _isLoaded = false; + private boolean _isAdblockLogging = false; //######################## //## @@ -72,25 +74,47 @@ public class AdBlock { } public boolean isAdHost(String urlS) { + boolean block = false; if (urlS != null && !urlS.isEmpty() && urlS.startsWith("http")) { try { - URI url = new URI(urlS); + URI url; + try { + url = new URI(urlS); + } catch (Exception e) { + url = new URI(urlS.replaceFirst("[?].*", "")); + } String host = url.getHost().trim(); if (host.startsWith("www.") && host.length() >= 4) { host = host.substring(4); } - return _adblockHosts.contains(host) || _adblockHosts.contains("www." + host); + block = _adblockHosts.contains(host) || _adblockHosts.contains("www." + host); + for (Callback.b3 cb : _customBlockCallbacks) { + if (block) { + break; + } + try { + block = cb.callback(url, urlS, host); + } catch (Exception ignored) { + } + } } catch (URISyntaxException e) { e.printStackTrace(); } } - return false; + + if (_isAdblockLogging) { + Log.d(getClass().getSimpleName(), "UrlAllowed-" + (block ? "N" : "Y") + " " + urlS); + } + return block; } public AdBlock reset() { - _adblockHosts.clear(); - _adblockHosts.addAll(_adblockHostsFromRaw); + synchronized (synchronizeObj) { + _adblockHosts.clear(); + _adblockHosts.addAll(_adblockHostsFromRaw); + _customBlockCallbacks.clear(); + } return this; } @@ -102,7 +126,7 @@ public class AdBlock { return new WebResourceResponse("text/plain", "utf-8", new ByteArrayInputStream("".getBytes())); } - public void addBlockedHosts(String... hosts) { + public AdBlock addBlockedHosts(String... hosts) { for (String host : hosts) { if (host != null) { host = host.trim(); @@ -110,23 +134,29 @@ public class AdBlock { host = host.substring(4); } if (!host.startsWith("#") && !host.startsWith("\"")) { - _adblockHosts.add(host); + synchronized (synchronizeObj) { + _adblockHosts.add(host); + } } } } - + return this; } - public void loadHostsFromRawAssetsAsync(final Context context) { - new Thread(new Runnable() { - @Override - public void run() { - try { + public void loadHostsFromRawAssetsAsync(final Context context, final boolean... debugIgnoreAssets) { + if (debugIgnoreAssets != null && debugIgnoreAssets.length > 0 && debugIgnoreAssets[0]) { + _isLoaded = true; + return; + } + + new Thread(() -> { + try { + synchronized (synchronizeObj) { loadHostsFromRawAssets(context); _isLoaded = true; - } catch (IOException e) { - e.printStackTrace(); } + } catch (IOException e) { + e.printStackTrace(); } }).start(); } @@ -172,4 +202,17 @@ public class AdBlock { } return adblockResIds; } + + // URI uri, String url, String host + public AdBlock addCustomBlockCallback(Callback.b3 cb) { + synchronized (synchronizeObj) { + _customBlockCallbacks.add(cb); + } + return this; + } + + public AdBlock setLogEnabled(boolean isAdblockLogging) { + _isAdblockLogging = isAdblockLogging; + return this; + } } diff --git a/app/src/main/java/net/gsantner/opoc/util/Callback.java b/app/src/main/java/net/gsantner/opoc/util/Callback.java index a08ff682..012cb634 100644 --- a/app/src/main/java/net/gsantner/opoc/util/Callback.java +++ b/app/src/main/java/net/gsantner/opoc/util/Callback.java @@ -1,17 +1,21 @@ /*####################################################### * - * Maintained by Gregor Santner, 2018- - * https://gsantner.net/ + * Maintained 2018-2023 by Gregor Santner * - * License: Apache 2.0 - * https://github.com/gsantner/opoc/#licensing - * https://www.apache.org/licenses/LICENSE-2.0 + * License of this file: Apache 2.0 + * https://www.apache.org/licenses/LICENSE-2.0 + * https://github.com/gsantner/opoc/#licensing * #########################################################*/ package net.gsantner.opoc.util; @SuppressWarnings("unused") public class Callback { + + public interface a0 { + void callback(); + } + public interface a1 { void callback(A arg1); } @@ -31,4 +35,52 @@ public class Callback { public interface a5 { void callback(A arg1, B arg2, C arg3, D arg4, E arg5); } + + public interface b0 { + boolean callback(); + } + + public interface b1 { + boolean callback(A arg1); + } + + public interface b2 { + boolean callback(A arg1, B arg2); + } + + public interface b3 { + boolean callback(A arg1, B arg2, C arg3); + } + + public interface b4 { + boolean callback(A arg1, B arg2, C arg3, D arg4); + } + + public interface b5 { + boolean callback(A arg1, B arg2, C arg3, D arg4, E arg5); + } + + public interface s0 { + String callback(); + } + + public interface s1 { + String callback(A arg1); + } + + public interface s2 { + String callback(A arg1, B arg2); + } + + public interface s3 { + String callback(A arg1, B arg2, C arg3); + } + + public interface s4 { + String callback(A arg1, B arg2, C arg3, D arg4); + } + + public interface s5 { + String callback(A arg1, B arg2, C arg3, D arg4, E arg5); + } } diff --git a/app/src/main/java/net/gsantner/opoc/util/ContextUtils.java b/app/src/main/java/net/gsantner/opoc/util/ContextUtils.java index 17d64424..483461d8 100644 --- a/app/src/main/java/net/gsantner/opoc/util/ContextUtils.java +++ b/app/src/main/java/net/gsantner/opoc/util/ContextUtils.java @@ -1,19 +1,21 @@ /*####################################################### * - * Maintained by Gregor Santner, 2016- - * https://gsantner.net/ + * Maintained 2016-2023 by Gregor Santner * - * License: Apache 2.0 - * https://github.com/gsantner/opoc/#licensing - * https://www.apache.org/licenses/LICENSE-2.0 + * License of this file: Apache 2.0 + * https://www.apache.org/licenses/LICENSE-2.0 + * https://github.com/gsantner/opoc/#licensing * #########################################################*/ package net.gsantner.opoc.util; import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.ActivityManager; import android.app.AlarmManager; import android.app.PendingIntent; import android.content.ActivityNotFoundException; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.pm.PackageInfo; @@ -36,6 +38,10 @@ import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; import android.os.Build; +import android.os.Environment; +import android.os.SystemClock; +import android.os.VibrationEffect; +import android.os.Vibrator; import android.support.annotation.ColorInt; import android.support.annotation.ColorRes; import android.support.annotation.DrawableRes; @@ -43,9 +49,14 @@ import android.support.annotation.Nullable; import android.support.annotation.RawRes; import android.support.annotation.StringRes; import android.support.graphics.drawable.VectorDrawableCompat; +import android.support.v4.app.ActivityManagerCompat; import android.support.v4.content.ContextCompat; import android.support.v4.graphics.drawable.DrawableCompat; +import android.support.v4.text.TextUtilsCompat; +import android.support.v4.util.Pair; +import android.support.v4.view.ViewCompat; import android.text.Html; +import android.text.InputFilter; import android.text.SpannableString; import android.text.Spanned; import android.text.TextUtils; @@ -54,6 +65,11 @@ import android.util.DisplayMetrics; import android.util.Log; import android.view.Menu; import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.Surface; +import android.view.View; +import android.view.WindowManager; +import android.webkit.MimeTypeMap; import android.widget.ImageView; import android.widget.TextView; @@ -61,17 +77,21 @@ import net.gsantner.opoc.format.markdown.SimpleMarkdownParser; import java.io.BufferedReader; import java.io.File; -import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; +import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.List; import java.util.Locale; +import static android.content.Context.VIBRATOR_SERVICE; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.graphics.Bitmap.CompressFormat; -@SuppressWarnings({"WeakerAccess", "unused", "SameParameterValue", "ObsoleteSdkInt", "deprecation", "SpellCheckingInspection"}) +@SuppressWarnings({"WeakerAccess", "unused", "SameParameterValue", "ObsoleteSdkInt", "deprecation", "SpellCheckingInspection", "TryFinallyCanBeTryWithResources", "UnusedAssignment", "UnusedReturnValue"}) public class ContextUtils { // // Members, Constructors @@ -86,6 +106,9 @@ public class ContextUtils { return _context; } + public void freeContextRef() { + _context = null; + } // // Class Methods @@ -100,39 +123,55 @@ public class ContextUtils { * * @return A valid id if the id could be found, else 0 */ - public int getResId(ResType resType, final String name) { - return _context.getResources().getIdentifier(name, resType.name().toLowerCase(), _context.getPackageName()); + public int getResId(final ResType resType, final String name) { + try { + return _context.getResources().getIdentifier(name, resType.name().toLowerCase(), _context.getPackageName()); + } catch (Exception e) { + return 0; + } } /** * Get String by given string ressource id (nuermic) */ - public String rstr(@StringRes int strResId) { - return _context.getString(strResId); + public String rstr(@StringRes final int strResId) { + try { + return _context.getString(strResId); + } catch (Exception e) { + return null; + } } /** * Get String by given string ressource identifier (textual) */ - public String rstr(String strResKey) { + public String rstr(final String strResKey, Object... a0getResKeyAsFallback) { try { return rstr(getResId(ResType.STRING, strResKey)); } catch (Resources.NotFoundException e) { - return null; + return a0getResKeyAsFallback != null && a0getResKeyAsFallback.length > 0 ? strResKey : null; } } /** * Get drawable from given ressource identifier */ - public Drawable rdrawable(@DrawableRes int resId) { - return ContextCompat.getDrawable(_context, resId); + public Drawable rdrawable(@DrawableRes final int resId) { + try { + return ContextCompat.getDrawable(_context, resId); + } catch (Exception e) { + return null; + } } /** * Get color by given color ressource id */ - public int rcolor(@ColorRes int resId) { + public int rcolor(@ColorRes final int resId) { + if (resId == 0) { + Log.e(getClass().getName(), "ContextUtils::rcolor: resId is 0!"); + return Color.BLACK; + } return ContextCompat.getColor(_context, resId); } @@ -158,35 +197,92 @@ public class ContextUtils { * @param intColor The color coded in int * @param withAlpha Optional; Set first bool parameter to true to also include alpha value */ - public String colorToHexString(int intColor, boolean... withAlpha) { + public static String colorToHexString(final int intColor, final boolean... withAlpha) { boolean a = withAlpha != null && withAlpha.length >= 1 && withAlpha[0]; return String.format(a ? "#%08X" : "#%06X", (a ? 0xFFFFFFFF : 0xFFFFFF) & intColor); } + public static String getAndroidVersion() { + return Build.VERSION.RELEASE + " (" + Build.VERSION.SDK_INT + ")"; + } + public String getAppVersionName() { + PackageManager manager = _context.getPackageManager(); try { - PackageManager manager = _context.getPackageManager(); - PackageInfo info = manager.getPackageInfo(_context.getPackageName(), 0); + PackageInfo info = manager.getPackageInfo(getPackageIdManifest(), 0); return info.versionName; } catch (PackageManager.NameNotFoundException e) { - e.printStackTrace(); - return "?"; + try { + PackageInfo info = manager.getPackageInfo(getPackageIdReal(), 0); + return info.versionName; + } catch (PackageManager.NameNotFoundException ignored) { + } } + return "?"; + } + + public String getAppInstallationSource() { + String src = null; + try { + src = _context.getPackageManager().getInstallerPackageName(getPackageIdManifest()); + } catch (Exception ignored) { + } + if (src == null || src.trim().isEmpty()) { + return "Sideloaded"; + } else if (src.toLowerCase().contains(".amazon.")) { + return "Amazon Appstore"; + } + switch (src) { + case "com.android.vending": + case "com.google.android.feedback": { + return "Google Play"; + } + case "org.fdroid.fdroid.privileged": + case "org.fdroid.fdroid": { + return "F-Droid"; + } + case "com.github.yeriomin.yalpstore": { + return "Yalp Store"; + } + case "cm.aptoide.pt": { + return "Aptoide"; + } + case "com.android.packageinstaller": { + return "Package Installer"; + } + } + return src; } /** * Send a {@link Intent#ACTION_VIEW} Intent with given paramter * If the parameter is an string a browser will get triggered */ - public void openWebpageInExternalBrowser(final String url) { - Uri uri = Uri.parse(url); - Intent intent = new Intent(Intent.ACTION_VIEW, uri); - intent.addFlags(FLAG_ACTIVITY_NEW_TASK); + public ContextUtils openWebpageInExternalBrowser(final String url) { try { + Uri uri = Uri.parse(url); + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + intent.addFlags(FLAG_ACTIVITY_NEW_TASK); _context.startActivity(intent); - } catch (ActivityNotFoundException e) { + } catch (Exception e) { e.printStackTrace(); } + return this; + } + + /** + * Get the apps base packagename, which is equal with all build flavors and variants + */ + public String getPackageIdManifest() { + String pkg = rstr("manifest_package_id"); + return !TextUtils.isEmpty(pkg) ? pkg : _context.getPackageName(); + } + + /** + * Get this apps package name, returns the flavor specific package name. + */ + public String getPackageIdReal() { + return _context.getPackageName(); } /** @@ -197,24 +293,36 @@ public class ContextUtils { * of the package set in manifest (root element). * Falls back to applicationId of the app which may differ from manifest. */ - public Object getBuildConfigValue(String fieldName) { - String pkg = rstr("manifest_package_id"); - pkg = (pkg != null ? pkg : _context.getPackageName()) + ".BuildConfig"; + public Object getBuildConfigValue(final String fieldName) { + final String pkg = getPackageIdManifest() + ".BuildConfig"; try { Class c = Class.forName(pkg); return c.getField(fieldName).get(null); } catch (Exception e) { e.printStackTrace(); - return null; } + return null; + } + + public List getBuildConfigFields() { + final String pkg = getPackageIdManifest() + ".BuildConfig"; + final List fields = new ArrayList<>(); + try { + for (Field f : Class.forName(pkg).getFields()) { + fields.add(f.getName()); + } + } catch (Exception e) { + e.printStackTrace(); + } + return fields; } /** * Get a BuildConfig bool value */ - public Boolean bcbool(String fieldName, Boolean defaultValue) { + public Boolean bcbool(final String fieldName, final Boolean defaultValue) { Object field = getBuildConfigValue(fieldName); - if (field != null && field instanceof Boolean) { + if (field instanceof Boolean) { return (Boolean) field; } return defaultValue; @@ -223,14 +331,25 @@ public class ContextUtils { /** * Get a BuildConfig string value */ - public String bcstr(String fieldName, String defaultValue) { + public String bcstr(final String fieldName, final String defaultValue) { Object field = getBuildConfigValue(fieldName); - if (field != null && field instanceof String) { + if (field instanceof String) { return (String) field; } return defaultValue; } + /** + * Get a BuildConfig string value + */ + public Integer bcint(final String fieldName, final int defaultValue) { + Object field = getBuildConfigValue(fieldName); + if (field instanceof Integer) { + return (Integer) field; + } + return defaultValue; + } + /** * Check if this is a gplay build (requires BuildConfig field) */ @@ -245,26 +364,6 @@ public class ContextUtils { return bcbool("IS_FOSS_BUILD", false); } - /** - * Request a bitcoin donation with given details. - * All parameters are awaited as string resource ids - */ - public void showDonateBitcoinRequest(@StringRes final int srBitcoinId, @StringRes final int srBitcoinAmount, @StringRes final int srBitcoinMessage, @StringRes final int srAlternativeDonateUrl) { - if (!isGooglePlayBuild()) { - String btcUri = String.format("bitcoin:%s?amount=%s&label=%s&message=%s", - rstr(srBitcoinId), rstr(srBitcoinAmount), - rstr(srBitcoinMessage), rstr(srBitcoinMessage)); - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setData(Uri.parse(btcUri)); - intent.addFlags(FLAG_ACTIVITY_NEW_TASK); - try { - _context.startActivity(intent); - } catch (ActivityNotFoundException e) { - openWebpageInExternalBrowser(rstr(srAlternativeDonateUrl)); - } - } - } - public String readTextfileFromRawRes(@RawRes int rawResId, String linePrefix, String linePostfix) { StringBuilder sb = new StringBuilder(); BufferedReader br = null; @@ -313,8 +412,8 @@ public class ContextUtils { * Check if app with given {@code packageName} is installed */ public boolean isAppInstalled(String packageName) { - PackageManager pm = _context.getApplicationContext().getPackageManager(); try { + PackageManager pm = _context.getApplicationContext().getPackageManager(); pm.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES); return true; } catch (PackageManager.NameNotFoundException e) { @@ -326,14 +425,17 @@ public class ContextUtils { * Restart the current app. Supply the class to start on startup */ public void restartApp(Class classToStart) { - Intent inte = new Intent(_context, classToStart); - PendingIntent inteP = PendingIntent.getActivity(_context, 555, inte, PendingIntent.FLAG_CANCEL_CURRENT); + Intent intent = new Intent(_context, classToStart); + PendingIntent pendi = PendingIntent.getActivity(_context, 555, intent, PendingIntent.FLAG_CANCEL_CURRENT); AlarmManager mgr = (AlarmManager) _context.getSystemService(Context.ALARM_SERVICE); + if (_context instanceof Activity) { + ((Activity) _context).finish(); + } if (mgr != null) { - mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 100, inteP); + mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 100, pendi); } else { - inte.addFlags(FLAG_ACTIVITY_NEW_TASK); - _context.startActivity(inte); + intent.addFlags(FLAG_ACTIVITY_NEW_TASK); + _context.startActivity(intent); } Runtime.getRuntime().exit(0); } @@ -402,19 +504,25 @@ public class ContextUtils { * {@code androidLC} may be in any of the forms: en, de, de-rAt * If given an empty string, the default (system) locale gets loaded */ - public void setAppLanguage(String androidLC) { + public void setAppLanguage(final String androidLC) { Locale locale = getLocaleByAndroidCode(androidLC); + locale = (locale != null && !androidLC.isEmpty()) ? locale : Resources.getSystem().getConfiguration().locale; + setLocale(locale); + } + + public ContextUtils setLocale(final Locale locale) { Configuration config = _context.getResources().getConfiguration(); - config.locale = (locale != null && !androidLC.isEmpty()) - ? locale : Resources.getSystem().getConfiguration().locale; + config.locale = (locale != null ? locale : Resources.getSystem().getConfiguration().locale); _context.getResources().updateConfiguration(config, null); + Locale.setDefault(locale); + return this; } /** * Try to guess if the color on top of the given {@code colorOnBottomInt} * should be light or dark. Returns true if top color should be light */ - public boolean shouldColorOnTopBeLight(@ColorInt int colorOnBottomInt) { + public boolean shouldColorOnTopBeLight(@ColorInt final int colorOnBottomInt) { return 186 > (((0.299 * Color.red(colorOnBottomInt)) + ((0.587 * Color.green(colorOnBottomInt)) + (0.114 * Color.blue(colorOnBottomInt))))); @@ -423,7 +531,7 @@ public class ContextUtils { /** * Convert a html string to an android {@link Spanned} object */ - public Spanned htmlToSpanned(String html) { + public Spanned htmlToSpanned(final String html) { Spanned result; if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { result = Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY); @@ -447,12 +555,87 @@ public class ContextUtils { return dp * _context.getResources().getDisplayMetrics().density; } + /** + * Get the private directory for the current package (usually /data/data/package.name/) + */ + @SuppressWarnings("StatementWithEmptyBody") + public File getAppDataPrivateDir() { + File filesDir; + try { + filesDir = new File(new File(_context.getPackageManager().getPackageInfo(getPackageIdReal(), 0).applicationInfo.dataDir), "files"); + } catch (PackageManager.NameNotFoundException e) { + filesDir = _context.getFilesDir(); + } + if (!filesDir.exists() && filesDir.mkdirs()) ; + return filesDir; + } + + /** + * Get public (accessible) appdata folders + */ + @SuppressWarnings("StatementWithEmptyBody") + public List> getAppDataPublicDirs(boolean internalStorageFolder, boolean sdcardFolders, boolean storageNameWithoutType) { + List> dirs = new ArrayList<>(); + for (File externalFileDir : ContextCompat.getExternalFilesDirs(_context, null)) { + if (externalFileDir == null || Environment.getExternalStorageDirectory() == null) { + continue; + } + boolean isInt = externalFileDir.getAbsolutePath().startsWith(Environment.getExternalStorageDirectory().getAbsolutePath()); + boolean add = (internalStorageFolder && isInt) || (sdcardFolders && !isInt); + if (add) { + dirs.add(new Pair<>(externalFileDir, getStorageName(externalFileDir, storageNameWithoutType))); + if (!externalFileDir.exists() && externalFileDir.mkdirs()) ; + } + } + return dirs; + } + + public String getStorageName(final File externalFileDir, final boolean storageNameWithoutType) { + boolean isInt = externalFileDir.getAbsolutePath().startsWith(Environment.getExternalStorageDirectory().getAbsolutePath()); + + String[] split = externalFileDir.getAbsolutePath().split("/"); + if (split.length > 2) { + return isInt ? (storageNameWithoutType ? "Internal Storage" : "") : (storageNameWithoutType ? split[2] : ("SD Card (" + split[2] + ")")); + } else { + return "Storage"; + } + } + + public List> getStorages(final boolean internalStorageFolder, final boolean sdcardFolders) { + List> storages = new ArrayList<>(); + for (Pair pair : getAppDataPublicDirs(internalStorageFolder, sdcardFolders, true)) { + if (pair.first != null && pair.first.getAbsolutePath().lastIndexOf("/Android/data") > 0) { + try { + storages.add(new Pair<>(new File(pair.first.getCanonicalPath().replaceFirst("/Android/data.*", "")), pair.second)); + } catch (IOException ignored) { + } + } + } + return storages; + } + + public File getStorageRootFolder(final File file) { + String filepath; + try { + filepath = file.getCanonicalPath(); + } catch (Exception ignored) { + return null; + } + for (Pair storage : getStorages(false, true)) { + //noinspection ConstantConditions + if (filepath.startsWith(storage.first.getAbsolutePath())) { + return storage.first; + } + } + return null; + } + /** * Request the givens paths to be scanned by MediaScanner * * @param files Files and folders to scan */ - public void mediaScannerScanFile(File... files) { + public void mediaScannerScanFile(final File... files) { if (android.os.Build.VERSION.SDK_INT > 19) { String[] paths = new String[files.length]; for (int i = 0; i < files.length; i++) { @@ -501,8 +684,12 @@ public class ContextUtils { /** * Get a {@link Bitmap} out of a {@link DrawableRes} */ - public Bitmap drawableToBitmap(@DrawableRes int drawableId) { - return drawableToBitmap(ContextCompat.getDrawable(_context, drawableId)); + public Bitmap drawableToBitmap(@DrawableRes final int drawableId) { + try { + return drawableToBitmap(ContextCompat.getDrawable(_context, drawableId)); + } catch (Exception e) { + return null; + } } /** @@ -510,7 +697,7 @@ public class ContextUtils { * Specifying a {@code maxDimen} is also possible and a value below 2000 * is recommended, otherwise a {@link OutOfMemoryError} may occur */ - public Bitmap loadImageFromFilesystem(File imagePath, int maxDimen) { + public Bitmap loadImageFromFilesystem(final File imagePath, final int maxDimen) { BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeFile(imagePath.getAbsolutePath(), options); @@ -526,7 +713,7 @@ public class ContextUtils { * @param maxDimen Max size of the Bitmap (width or height) * @return the scaling factor that needs to be applied to the bitmap */ - public int calculateInSampleSize(BitmapFactory.Options options, int maxDimen) { + public int calculateInSampleSize(final BitmapFactory.Options options, final int maxDimen) { // Raw height and width of image int height = options.outHeight; int width = options.outWidth; @@ -542,7 +729,7 @@ public class ContextUtils { * Scale the bitmap so both dimensions are lower or equal to {@code maxDimen} * This keeps the aspect ratio */ - public Bitmap scaleBitmap(Bitmap bitmap, int maxDimen) { + public Bitmap scaleBitmap(final Bitmap bitmap, final int maxDimen) { int picSize = Math.min(bitmap.getHeight(), bitmap.getWidth()); float scale = 1.f * maxDimen / picSize; Matrix matrix = new Matrix(); @@ -550,44 +737,27 @@ public class ContextUtils { return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); } - /** - * Write the given {@link Bitmap} to {@code imageFile}, in {@link CompressFormat#JPEG} format - */ - public boolean writeImageToFileJpeg(File imageFile, Bitmap image) { - return writeImageToFile(imageFile, image, Bitmap.CompressFormat.JPEG, 95); - } - /** * Write the given {@link Bitmap} to filesystem * * @param targetFile The file to be written in - * @param image The image as android {@link Bitmap} - * @param format One format of {@link CompressFormat}, null will determine based on filename - * @param quality Quality level, defaults to 95 + * @param image Android {@link Bitmap} * @return True if writing was successful */ - public boolean writeImageToFile(File targetFile, Bitmap image, CompressFormat format, Integer quality) { + public boolean writeImageToFile(final File targetFile, final Bitmap image, Integer... a0quality) { + final int quality = (a0quality != null && a0quality.length > 0 && a0quality[0] >= 0 && a0quality[0] <= 100) ? a0quality[0] : 70; + final String lc = targetFile.getAbsolutePath().toLowerCase(Locale.ROOT); + final CompressFormat format = lc.endsWith(".webp") ? CompressFormat.WEBP : (lc.endsWith(".png") ? CompressFormat.PNG : CompressFormat.JPEG); + + boolean ok = false; File folder = new File(targetFile.getParent()); - if (quality == null || quality < 0 || quality > 100) { - quality = 95; - } - if (format == null) { - format = CompressFormat.JPEG; - String lc = targetFile.getAbsolutePath().toLowerCase(Locale.ROOT); - if (lc.endsWith(".png")) { - format = CompressFormat.PNG; - } - if (lc.endsWith(".webp")) { - format = CompressFormat.WEBP; - } - } if (folder.exists() || folder.mkdirs()) { FileOutputStream stream = null; try { - stream = new FileOutputStream(targetFile); // overwrites this image every time + stream = new FileOutputStream(targetFile); image.compress(format, quality, stream); - return true; - } catch (FileNotFoundException ignored) { + ok = true; + } catch (Exception ignored) { } finally { try { if (stream != null) { @@ -597,14 +767,18 @@ public class ContextUtils { } } } - return false; + try { + image.recycle(); + } catch (Exception ignored) { + } + return ok; } /** * Draw text in the center of the given {@link DrawableRes} * This may be useful for e.g. badge counts */ - public Bitmap drawTextOnDrawable(@DrawableRes int drawableRes, String text, int textSize) { + public Bitmap drawTextOnDrawable(@DrawableRes final int drawableRes, final String text, final int textSize) { Resources resources = _context.getResources(); float scale = resources.getDisplayMetrics().density; Bitmap bitmap = drawableToBitmap(drawableRes); @@ -629,12 +803,16 @@ public class ContextUtils { * Try to tint all {@link Menu}s {@link MenuItem}s with given color */ @SuppressWarnings("ConstantConditions") - public void tintMenuItems(Menu menu, boolean recurse, @ColorInt int iconColor) { + public void tintMenuItems(final Menu menu, final boolean recurse, @ColorInt final int iconColor) { for (int i = 0; i < menu.size(); i++) { MenuItem item = menu.getItem(i); - tintDrawable(item.getIcon(), iconColor); - if (item.hasSubMenu() && recurse) { - tintMenuItems(item.getSubMenu(), recurse, iconColor); + try { + tintDrawable(item.getIcon(), iconColor); + if (item.hasSubMenu() && recurse) { + tintMenuItems(item.getSubMenu(), recurse, iconColor); + } + } catch (Exception ignored) { + // This should not happen at all, but may in bad menu.xml configuration } } } @@ -642,14 +820,14 @@ public class ContextUtils { /** * Loads {@link Drawable} by given {@link DrawableRes} and applies a color */ - public Drawable tintDrawable(@DrawableRes int drawableRes, @ColorInt int color) { + public Drawable tintDrawable(@DrawableRes final int drawableRes, @ColorInt final int color) { return tintDrawable(rdrawable(drawableRes), color); } /** * Tint a {@link Drawable} with given {@code color} */ - public Drawable tintDrawable(@Nullable Drawable drawable, @ColorInt int color) { + public Drawable tintDrawable(@Nullable Drawable drawable, @ColorInt final int color) { if (drawable != null) { drawable = DrawableCompat.wrap(drawable); DrawableCompat.setTint(drawable.mutate(), color); @@ -661,7 +839,10 @@ public class ContextUtils { * Try to make icons in Toolbar/ActionBars SubMenus visible * This may not work on some devices and it maybe won't work on future android updates */ - public void setSubMenuIconsVisiblity(Menu menu, boolean visible) { + public void setSubMenuIconsVisiblity(final Menu menu, final boolean visible) { + if (TextUtilsCompat.getLayoutDirectionFromLocale(Locale.getDefault()) == ViewCompat.LAYOUT_DIRECTION_RTL) { + return; + } if (menu.getClass().getSimpleName().equals("MenuBuilder")) { try { @SuppressLint("PrivateApi") Method m = menu.getClass().getDeclaredMethod("setOptionalIconsVisible", Boolean.TYPE); @@ -672,4 +853,165 @@ public class ContextUtils { } } } + + + public String getLocalizedDateFormat() { + return ((SimpleDateFormat) android.text.format.DateFormat.getDateFormat(_context)).toPattern(); + } + + public String getLocalizedTimeFormat() { + return ((SimpleDateFormat) android.text.format.DateFormat.getTimeFormat(_context)).toPattern(); + } + + public String getLocalizedDateTimeFormat() { + return getLocalizedDateFormat() + " " + getLocalizedTimeFormat(); + } + + /** + * A {@link InputFilter} for filenames + */ + @SuppressWarnings("Convert2Lambda") + public static final InputFilter INPUTFILTER_FILENAME = new InputFilter() { + public CharSequence filter(CharSequence src, int start, int end, Spanned dest, int dstart, int dend) { + if (src.length() < 1) return null; + char last = src.charAt(src.length() - 1); + String illegal = "|\\?*<\":>[]/'"; + if (illegal.indexOf(last) > -1) return src.subSequence(0, src.length() - 1); + return null; + } + }; + + /** + * A simple {@link Runnable} which does a touch event on a view. + * This pops up e.g. the keyboard on a {@link android.widget.EditText} + *

+ * Example: new Handler().postDelayed(new DoTouchView(editView), 200); + */ + public static class DoTouchView implements Runnable { + View _view; + + public DoTouchView(View view) { + _view = view; + } + + @Override + public void run() { + _view.dispatchTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN, 0, 0, 0)); + _view.dispatchTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, 0, 0, 0)); + } + } + + + public String getMimeType(final File file) { + return getMimeType(Uri.fromFile(file)); + } + + /** + * Detect MimeType of given file + * Android/Java's own MimeType map is very very small and detection barely works at all + * Hence use custom map for some file extensions + */ + public String getMimeType(final Uri uri) { + String mimeType = null; + if (ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) { + ContentResolver cr = _context.getContentResolver(); + mimeType = cr.getType(uri); + } else { + String filename = uri.toString(); + if (filename.endsWith(".jenc")) { + filename = filename.replace(".jenc", ""); + } + String ext = MimeTypeMap.getFileExtensionFromUrl(filename); + mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.toLowerCase()); + + // Try to guess if the recommended methods fail + if (TextUtils.isEmpty(mimeType)) { + switch (ext) { + case "md": + case "markdown": + case "mkd": + case "mdown": + case "mkdn": + case "mdwn": + case "rmd": + mimeType = "text/markdown"; + break; + case "yaml": + case "yml": + mimeType = "text/yaml"; + break; + case "json": + mimeType = "text/json"; + break; + case "txt": + mimeType = "text/plain"; + break; + } + } + } + + if (TextUtils.isEmpty(mimeType)) { + mimeType = "*/*"; + } + return mimeType; + } + + public Integer parseColor(final String colorstr) { + if (colorstr == null || colorstr.trim().isEmpty()) { + return null; + } + try { + return Color.parseColor(colorstr); + } catch (IllegalArgumentException ignored) { + return null; + } + } + + public boolean isDeviceGoodHardware() { + try { + ActivityManager activityManager = (ActivityManager) _context.getSystemService(Context.ACTIVITY_SERVICE); + return !ActivityManagerCompat.isLowRamDevice(activityManager) && + Runtime.getRuntime().availableProcessors() >= 4 && + activityManager.getMemoryClass() >= 128; + } catch (Exception ignored) { + return true; + } + } + + // Vibrate device one time by given amount of time, defaulting to 50ms + // Requires in AndroidManifest to work + @SuppressWarnings("UnnecessaryReturnStatement") + @SuppressLint("MissingPermission") + public void vibrate(final int... ms) { + int ms_v = ms != null && ms.length > 0 ? ms[0] : 50; + Vibrator vibrator = ((Vibrator) _context.getSystemService(VIBRATOR_SERVICE)); + if (vibrator == null) { + return; + } else if (Build.VERSION.SDK_INT >= 26) { + vibrator.vibrate(VibrationEffect.createOneShot(ms_v, VibrationEffect.DEFAULT_AMPLITUDE)); + } else { + vibrator.vibrate(ms_v); + } + } + + /* + Check if Wifi is connected. Requires these permissions in AndroidManifest: + + + */ + @SuppressLint("MissingPermission") + public boolean isWifiConnected(boolean... enabledOnly) { + final boolean doEnabledCheckOnly = enabledOnly != null && enabledOnly.length > 0 && enabledOnly[0]; + final ConnectivityManager connectivityManager = (ConnectivityManager) _context.getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE); + final NetworkInfo wifiInfo = connectivityManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI); + return wifiInfo != null && (doEnabledCheckOnly ? wifiInfo.isAvailable() : wifiInfo.isConnected()); + } + + // Returns if the device is currently in portrait orientation (landscape=false) + public boolean isDeviceOrientationPortrait() { + final int rotation = ((WindowManager) _context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getOrientation(); + return (rotation == Surface.ROTATION_0) || (rotation == Surface.ROTATION_180); + } } + + diff --git a/app/src/main/java/net/gsantner/opoc/util/FileUtils.java b/app/src/main/java/net/gsantner/opoc/util/FileUtils.java index 13cb66d5..7dbd1658 100644 --- a/app/src/main/java/net/gsantner/opoc/util/FileUtils.java +++ b/app/src/main/java/net/gsantner/opoc/util/FileUtils.java @@ -1,41 +1,66 @@ /*####################################################### * - * Maintained by Gregor Santner, 2017- - * https://gsantner.net/ + * Maintained 2017-2023 by Gregor Santner * - * License: Apache 2.0 - * https://github.com/gsantner/opoc/#licensing - * https://www.apache.org/licenses/LICENSE-2.0 + * License of this file: Apache 2.0 + * https://www.apache.org/licenses/LICENSE-2.0 + * https://github.com/gsantner/opoc/#licensing * #########################################################*/ package net.gsantner.opoc.util; +import android.text.TextUtils; + import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; +import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; +import java.net.URLConnection; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Pattern; -@SuppressWarnings({"WeakerAccess", "unused", "SameParameterValue", "SpellCheckingInspection", "deprecation"}) +@SuppressWarnings({"WeakerAccess", "unused", "SameParameterValue", "SpellCheckingInspection", "deprecation", "TryFinallyCanBeTryWithResources"}) public class FileUtils { // Used on methods like copyFile(src, dst) private static final int BUFFER_SIZE = 4096; + public static String readTextFileFast(final File file) { + try { + return new String(readCloseStreamWithSize(new FileInputStream(file), (int) file.length())); + } catch (FileNotFoundException e) { + System.err.println("readTextFileFast: File " + file + " not found."); + } + return ""; + } + + public static byte[] readCloseStreamWithSize(final InputStream stream, int size) { + byte[] data = new byte[size]; + try (DataInputStream dis = new DataInputStream(stream)) { + dis.readFully(data); + } catch (IOException ignored) { + } + return data; + } + public static String readTextFile(final File file) { try { return readCloseTextStream(new FileInputStream(file)); @@ -217,6 +242,30 @@ public class FileUtils { } } + public static boolean copyFile(final File src, final FileOutputStream os) { + InputStream is = null; + try { + try { + is = new FileInputStream(src); + byte[] buf = new byte[BUFFER_SIZE]; + int len; + while ((len = is.read(buf)) > 0) { + os.write(buf, 0, len); + } + return true; + } finally { + if (is != null) { + is.close(); + } + if (os != null) { + os.close(); + } + } + } catch (IOException ex) { + return false; + } + } + // Returns -1 if the file did not contain any of the needles, otherwise, // the index of which needle was found in the contents of the file. // @@ -337,4 +386,142 @@ public class FileUtils { return null; } } + + /** + * Try to detect MimeType by backwards compatible methods + */ + public static String getMimeType(File file) { + String guess = null; + if (file != null) { + if (file.exists() && file.isFile()) { + InputStream is = null; + try { + is = new BufferedInputStream(new FileInputStream(file)); + guess = URLConnection.guessContentTypeFromStream(is); + } catch (Exception ignored) { + } finally { + if (is != null) { + try { + is.close(); + } catch (Exception ignored) { + } + } + } + } + + String filename = file.getName().replace(".jenc", ""); + int dot = filename.lastIndexOf(".") + 1; + if (dot > 0 && dot < filename.length()) { + switch (filename.substring(dot)) { + case "md": + case "markdown": + case "mkd": + case "mdown": + case "mkdn": + case "mdwn": + case "rmd": + guess = "text/markdown"; + break; + case "txt": + guess = "text/plain"; + break; + case "webp": + guess = "image/webp"; + break; + case "jpg": + case "jpeg": + guess = "image/jpeg"; + break; + case "png": + guess = "image/png"; + break; + } + } + + if (TextUtils.isEmpty(guess)) { + guess = URLConnection.guessContentTypeFromName(filename); + } + } + + return TextUtils.isEmpty(guess) ? "*/*" : guess; + } + + public static boolean isTextFile(File file) { + String mime = getMimeType(file); + return mime != null && mime.startsWith("text/"); + } + + /** + * Analyze given textfile and retrieve multiple information from it + * Information is written back to the {@link AtomicInteger} parameters + */ + public static void retrieveTextFileSummary(File file, AtomicInteger numCharacters, AtomicInteger numLines, AtomicInteger numWords) { + BufferedReader br = null; + try { + br = new BufferedReader(new FileReader(file)); + String line; + while ((line = br.readLine()) != null) { + numLines.getAndIncrement(); + numCharacters.getAndSet(numCharacters.get() + line.length()); + if (!line.equals("")) { + numWords.getAndSet(numWords.get() + line.split("\\s+").length); + } + } + } catch (Exception e) { + e.printStackTrace(); + numCharacters.set(-1); + numLines.set(-1); + numWords.set(-1); + } finally { + if (br != null) { + try { + br.close(); + } catch (IOException ignored) { + } + } + } + } + + /** + * Format filesize to human readable format + * Get size in bytes e.g. from {@link File} using {@code File#length()} + */ + public static String getReadableFileSize(long size, boolean abbreviation) { + if (size <= 0) { + return "0B"; + } + String[] units = abbreviation ? new String[]{"B", "kB", "MB", "GB", "TB"} : new String[]{"Bytes", "Kilobytes", "Megabytes", "Gigabytes", "Terabytes"}; + int unit = (int) (Math.log10(size) / Math.log10(1024)); + return new DecimalFormat("#,##0.#", DecimalFormatSymbols.getInstance(Locale.ENGLISH)).format(size / Math.pow(1024, unit)) + " " + units[unit]; + } + + public static int[] getTimeDiffHMS(long now, long past) { + int[] ret = new int[3]; + long diff = Math.abs(now - past); + ret[0] = (int) (diff / (1000 * 60 * 60)); // hours + ret[1] = (int) (diff / (1000 * 60)) % 60; // min + ret[2] = (int) (diff / 1000) % 60; // sec + return ret; + } + + public static String getHumanReadableByteCountSI(final long bytes) { + if (bytes < 1000) { + return String.format(Locale.getDefault(), "%d%s", bytes, "B"); + } else if (bytes < 1000000) { + return String.format(Locale.getDefault(), "%.2f%s", (bytes / 1000f), "KB"); + } else if (bytes < 1000000000) { + return String.format(Locale.getDefault(), "%.2f%s", (bytes / 1000000f), "MB"); + } else if (bytes < 1000000000000L) { + return String.format(Locale.getDefault(), "%.2f%s", (bytes / 1000000000f), "GB"); + } else { + return String.format(Locale.getDefault(), "%.2f%s", (bytes / 1000000000000f), "TB"); + } + } + + public static File join(File file, String... childSegments) { + for (final String s : childSegments != null ? childSegments : new String[0]) { + file = new File(file, s); + } + return file; + } } diff --git a/app/src/main/java/net/gsantner/opoc/util/NetworkUtils.java b/app/src/main/java/net/gsantner/opoc/util/NetworkUtils.java index 5212e5b7..dc06674e 100644 --- a/app/src/main/java/net/gsantner/opoc/util/NetworkUtils.java +++ b/app/src/main/java/net/gsantner/opoc/util/NetworkUtils.java @@ -1,11 +1,10 @@ /*####################################################### * - * Maintained by Gregor Santner, 2017- - * https://gsantner.net/ + * Maintained 2017-2023 by Gregor Santner * - * License: Apache 2.0 - * https://github.com/gsantner/opoc/#licensing - * https://www.apache.org/licenses/LICENSE-2.0 + * License of this file: Apache 2.0 + * https://www.apache.org/licenses/LICENSE-2.0 + * https://github.com/gsantner/opoc/#licensing * #########################################################*/ package net.gsantner.opoc.util; @@ -77,7 +76,7 @@ public class NetworkUtils { int written = 0; final float invLength = 1f / connection.getContentLength(); - byte data[] = new byte[BUFFER_SIZE]; + byte[] data = new byte[BUFFER_SIZE]; while ((count = input.read(data)) != -1) { output.write(data, 0, count); if (invLength != -1f && progressCallback != null) { @@ -150,6 +149,7 @@ public class NetworkUtils { return performCall(url, method, data, null); } + @SuppressWarnings("CharsetObjectCanBeUsed") private static String performCall(final URL url, final String method, final String data, final HttpURLConnection existingConnection) { try { final HttpURLConnection connection = existingConnection != null @@ -160,7 +160,7 @@ public class NetworkUtils { if (data != null && !data.isEmpty()) { connection.setDoOutput(true); final OutputStream output = connection.getOutputStream(); - output.write(data.getBytes(Charset.forName(UTF8))); + output.write(data.getBytes(Charset.forName("UTF-8"))); output.flush(); output.close(); } @@ -220,4 +220,14 @@ public class NetworkUtils { return result; } + + public static void httpGetAsync(final String url, final Callback.a1 callback) { + new Thread(() -> { + try { + String c = NetworkUtils.performCall(url, GET); + callback.callback(c); + } catch (Exception ignored) { + } + }).start(); + } } diff --git a/app/src/main/java/net/gsantner/opoc/util/PermissionChecker.java b/app/src/main/java/net/gsantner/opoc/util/PermissionChecker.java index 524c3065..be156745 100644 --- a/app/src/main/java/net/gsantner/opoc/util/PermissionChecker.java +++ b/app/src/main/java/net/gsantner/opoc/util/PermissionChecker.java @@ -1,11 +1,10 @@ /*####################################################### * - * Maintained by Gregor Santner, 2017- - * https://gsantner.net/ + * Maintained 2017-2023 by Gregor Santner * - * License: Apache 2.0 - * https://github.com/gsantner/opoc/#licensing - * https://www.apache.org/licenses/LICENSE-2.0 + * License of this file: Apache 2.0 + * https://www.apache.org/licenses/LICENSE-2.0 + * https://github.com/gsantner/opoc/#licensing * #########################################################*/ package net.gsantner.opoc.util; @@ -21,9 +20,9 @@ import java.io.File; @SuppressWarnings({"unused", "WeakerAccess"}) public class PermissionChecker { - private static final int CODE_PERMISSION_EXTERNAL_STORAGE = 4000; + protected static final int CODE_PERMISSION_EXTERNAL_STORAGE = 4000; - private Activity _activity; + protected Activity _activity; public PermissionChecker(Activity activity) { _activity = activity; diff --git a/app/src/main/java/net/gsantner/opoc/util/ShareUtil.java b/app/src/main/java/net/gsantner/opoc/util/ShareUtil.java index 21932cd6..dc467500 100644 --- a/app/src/main/java/net/gsantner/opoc/util/ShareUtil.java +++ b/app/src/main/java/net/gsantner/opoc/util/ShareUtil.java @@ -1,19 +1,26 @@ /*####################################################### * - * Maintained by Gregor Santner, 2017- - * https://gsantner.net/ + * Maintained 2017-2023 by Gregor Santner * - * License: Apache 2.0 - * https://github.com/gsantner/opoc/#licensing - * https://www.apache.org/licenses/LICENSE-2.0 + * License of this file: Apache 2.0 + * https://www.apache.org/licenses/LICENSE-2.0 + * https://github.com/gsantner/opoc/#licensing * #########################################################*/ package net.gsantner.opoc.util; +import android.Manifest; +import android.annotation.SuppressLint; import android.app.Activity; +import android.content.ActivityNotFoundException; +import android.content.BroadcastReceiver; import android.content.ClipData; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; @@ -21,50 +28,89 @@ import android.net.Uri; import android.os.Build; import android.os.Environment; import android.os.Handler; +import android.os.ParcelFileDescriptor; import android.print.PrintAttributes; import android.print.PrintDocumentAdapter; import android.print.PrintJob; import android.print.PrintManager; +import android.provider.CalendarContract; +import android.provider.MediaStore; import android.support.annotation.DrawableRes; import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; +import android.support.annotation.StringRes; +import android.support.v4.app.ActivityCompat; +import android.support.v4.content.ContextCompat; import android.support.v4.content.FileProvider; +import android.support.v4.content.LocalBroadcastManager; import android.support.v4.content.pm.ShortcutInfoCompat; import android.support.v4.content.pm.ShortcutManagerCompat; import android.support.v4.graphics.drawable.IconCompat; +import android.support.v4.provider.DocumentFile; +import android.support.v4.util.Pair; +import android.support.v7.app.AlertDialog; +import android.support.v7.preference.PreferenceManager; import android.text.TextUtils; import android.util.Log; import android.view.View; import android.webkit.WebView; +import android.widget.ImageView; +import android.widget.Toast; import java.io.File; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; import java.util.List; import java.util.Locale; import java.util.Random; +import static android.app.Activity.RESULT_OK; + /** - * A utility class to ease information sharing on Android - * Also allows to parse/fetch information out of shared information + * A utility class to ease information sharing on Android. + * Also allows to parse/fetch information out of shared information. + * (M)Permissions are not checked, wrap ShareUtils methods if neccessary */ -@SuppressWarnings({"UnusedReturnValue", "WeakerAccess", "SameParameterValue", "unused", "deprecation", "ConstantConditions", "ObsoleteSdkInt", "SpellCheckingInspection"}) +@SuppressWarnings({"UnusedReturnValue", "WeakerAccess", "SameParameterValue", "unused", "deprecation", "ConstantConditions", "ObsoleteSdkInt", "SpellCheckingInspection", "JavadocReference", "ConstantLocale"}) public class ShareUtil { public final static String EXTRA_FILEPATH = "real_file_path_2"; - public final static SimpleDateFormat SDF_RFC3339_ISH = new SimpleDateFormat("yyyy-MM-dd'T'HH-mm", Locale.getDefault()); - public final static SimpleDateFormat SDF_SHORT = new SimpleDateFormat("yyMMdd-HHmm", Locale.getDefault()); + public final static SimpleDateFormat SDF_RFC3339_ISH = new SimpleDateFormat("yyyy-MM-dd'T'HH-mm-ss", Locale.getDefault()); + public final static SimpleDateFormat SDF_SHORT = new SimpleDateFormat("yyMMdd-HHmmss", Locale.getDefault()); + public final static SimpleDateFormat SDF_IMAGES = new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.getDefault()); //20190511-230845 + public final static String MIME_TEXT_PLAIN = "text/plain"; + public final static String PREF_KEY__SAF_TREE_URI = "pref_key__saf_tree_uri"; + public final static int REQUEST_CAMERA_PICTURE = 50001; + public final static int REQUEST_PICK_PICTURE = 50002; + public final static int REQUEST_SAF = 50003; + + public final static int MIN_OVERWRITE_LENGTH = 5; + + protected static String _lastCameraPictureFilepath; + protected static String _fileProviderAuthority; protected Context _context; - protected String _fileProviderAuthority; protected String _chooserTitle; - public ShareUtil(Context context) { + public ShareUtil(final Context context) { _context = context; _chooserTitle = "➥"; } + public void setContext(final Context c) { + _context = c; + } + + public void freeContextRef() { + _context = null; + } + public String getFileProviderAuthority() { if (TextUtils.isEmpty(_fileProviderAuthority)) { throw new RuntimeException("Error at ShareUtil.getFileProviderAuthority(): No FileProvider authority provided"); @@ -72,13 +118,12 @@ public class ShareUtil { return _fileProviderAuthority; } - public ShareUtil setFileProviderAuthority(String fileProviderAuthority) { + public static void setFileProviderAuthority(final String fileProviderAuthority) { _fileProviderAuthority = fileProviderAuthority; - return this; } - public ShareUtil setChooserTitle(String title) { + public ShareUtil setChooserTitle(final String title) { _chooserTitle = title; return this; } @@ -89,7 +134,7 @@ public class ShareUtil { * @param file the file * @return Uri for this file */ - public Uri getUriByFileProviderAuthority(File file) { + public Uri getUriByFileProviderAuthority(final File file) { return FileProvider.getUriForFile(_context, getFileProviderAuthority(), file); } @@ -99,9 +144,11 @@ public class ShareUtil { * @param intent Thing to be shared * @param chooserText The title text for the chooser, or null for default */ - public void showChooser(Intent intent, String chooserText) { - _context.startActivity(Intent.createChooser(intent, - chooserText != null ? chooserText : _chooserTitle)); + public void showChooser(final Intent intent, final String chooserText) { + try { + _context.startActivity(Intent.createChooser(intent, chooserText != null ? chooserText : _chooserTitle)); + } catch (Exception ignored) { + } } /** @@ -113,7 +160,7 @@ public class ShareUtil { * @param iconRes Icon resource for the item * @param title Title of the item */ - public void createLauncherDesktopShortcut(Intent intent, @DrawableRes int iconRes, String title) { + public void createLauncherDesktopShortcut(final Intent intent, @DrawableRes final int iconRes, final String title) { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); if (intent.getAction() == null) { @@ -138,7 +185,7 @@ public class ShareUtil { * @param iconRes Icon resource for the item * @param title Title of the item */ - public void createLauncherDesktopShortcutLegacy(Intent intent, @DrawableRes int iconRes, String title) { + public void createLauncherDesktopShortcutLegacy(final Intent intent, @DrawableRes final int iconRes, final String title) { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); if (intent.getAction() == null) { @@ -159,10 +206,10 @@ public class ShareUtil { * @param text The text to share * @param mimeType MimeType or null (uses text/plain) */ - public void shareText(String text, @Nullable String mimeType) { + public void shareText(final String text, @Nullable final String mimeType) { Intent intent = new Intent(Intent.ACTION_SEND); intent.putExtra(Intent.EXTRA_TEXT, text); - intent.setType(mimeType != null ? mimeType : "text/plain"); + intent.setType(mimeType != null ? mimeType : MIME_TEXT_PLAIN); showChooser(intent, null); } @@ -172,24 +219,104 @@ public class ShareUtil { * @param file The file to share * @param mimeType The files mime type */ - public void shareStream(File file, String mimeType) { - Uri fileUri = FileProvider.getUriForFile(_context, getFileProviderAuthority(), file); + public boolean shareStream(final File file, final String mimeType) { Intent intent = new Intent(Intent.ACTION_SEND); - intent.putExtra(Intent.EXTRA_STREAM, fileUri); intent.putExtra(EXTRA_FILEPATH, file.getAbsolutePath()); intent.setType(mimeType); - showChooser(intent, null); + + try { + Uri fileUri = FileProvider.getUriForFile(_context, getFileProviderAuthority(), file); + intent.putExtra(Intent.EXTRA_STREAM, fileUri); + showChooser(intent, null); + return true; + } catch (Exception ignored) { // FileUriExposed(API24) / IllegalArgument + } + return false; } /** - * Share the given bitmap with given format + * Share the given files as stream with given mime-type * - * @param bitmap Image - * @param format A {@link Bitmap.CompressFormat}, supporting JPEG,PNG,WEBP - * @return if success, true + * @param files The files to share + * @param mimeType The files mime type. Usally * / * is the best option */ - public boolean shareImage(Bitmap bitmap, Bitmap.CompressFormat format) { - return shareImage(bitmap, format, 95, "SharedImage"); + public boolean shareStreamMultiple(final Collection files, final String mimeType) { + ArrayList uris = new ArrayList<>(); + for (File file : files) { + File uri = new File(file.toString()); + uris.add(FileProvider.getUriForFile(_context, getFileProviderAuthority(), file)); + } + + try { + Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE); + intent.setType(mimeType); + intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris); + showChooser(intent, null); + return true; + } catch (Exception e) { // FileUriExposed(API24) / IllegalArgument + return false; + } + } + + /** + * Start calendar application to add new event, with given details prefilled + */ + public boolean createCalendarAppointment(@Nullable final String title, @Nullable final String description, @Nullable final String location, @Nullable final Long... startAndEndTime) { + Intent intent = new Intent(Intent.ACTION_INSERT).setData(CalendarContract.Events.CONTENT_URI); + if (title != null) { + intent.putExtra(CalendarContract.Events.TITLE, title); + } + if (description != null) { + intent.putExtra(CalendarContract.Events.DESCRIPTION, (description.length() > 800 ? description.substring(0, 800) : description)); + } + if (location != null) { + intent.putExtra(CalendarContract.Events.EVENT_LOCATION, location); + } + if (startAndEndTime != null) { + if (startAndEndTime.length > 0 && startAndEndTime[0] > 0) { + intent.putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, startAndEndTime[0]); + } + if (startAndEndTime.length > 1 && startAndEndTime[1] > 0) { + intent.putExtra(CalendarContract.EXTRA_EVENT_END_TIME, startAndEndTime[1]); + } + } + + try { + _context.startActivity(intent); + return true; + } catch (ActivityNotFoundException e) { + return false; + } + } + + /** + * Open a View intent for given file + * + * @param file The file to share + */ + public boolean viewFileInOtherApp(final File file, @Nullable final String type) { + // On some specific devices the first won't work + Uri fileUri = null; + try { + fileUri = FileProvider.getUriForFile(_context, getFileProviderAuthority(), file); + } catch (Exception ignored) { + try { + fileUri = Uri.fromFile(file); + } catch (Exception ignored2) { + } + } + + if (fileUri != null) { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.putExtra(Intent.EXTRA_STREAM, fileUri); + intent.setData(fileUri); + intent.putExtra(EXTRA_FILEPATH, file.getAbsolutePath()); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + intent.setDataAndType(fileUri, type); + showChooser(intent, null); + return true; + } + return false; } /** @@ -201,20 +328,36 @@ public class ShareUtil { * @param quality Quality of the exported image [0-100] * @return if success, true */ - public boolean shareImage(Bitmap bitmap, Bitmap.CompressFormat format, int quality, String imageName) { + public boolean shareImage(final Bitmap bitmap, final Integer... quality) { try { - String ext = format.name().toLowerCase(); - File file = File.createTempFile(imageName, "." + ext.replace("jpeg", "jpg"), _context.getExternalCacheDir()); - if (bitmap != null && new ContextUtils(_context).writeImageToFile(file, bitmap, format, quality)) { - shareStream(file, "image/" + ext); + File file = new File(_context.getCacheDir(), getFilenameWithTimestamp()); + if (bitmap != null && new ContextUtils(_context).writeImageToFile(file, bitmap, quality)) { + String x = FileUtils.getMimeType(file); + shareStream(file, FileUtils.getMimeType(file)); return true; } - } catch (IOException e) { - e.printStackTrace(); + } catch (Exception ignored) { } return false; } + /** + * Generate a filename based off current datetime in filename (year, month, day, hour, minute, second) + * Examples: Screenshot_20210208-184301_Trebuchet.png IMG_20190511-230845.jpg + * + * @param A0prefixA1postfixA2ext All arguments are optional and default values are taken for null + * [0] = Prefix [Screenshot/IMG] + * [1] = Postfix [Trebuchet] + * [2] = File extensions [jpg/png/txt] + * @return Filename + */ + public static String getFilenameWithTimestamp(String... A0prefixA1postfixA2ext) { + final String prefix = (((A0prefixA1postfixA2ext != null && A0prefixA1postfixA2ext.length > 0 && !TextUtils.isEmpty(A0prefixA1postfixA2ext[0])) ? A0prefixA1postfixA2ext[0] : "Screenshot") + "_").trim().replaceFirst("^_$", ""); + final String postfix = ("_" + ((A0prefixA1postfixA2ext != null && A0prefixA1postfixA2ext.length > 1 && !TextUtils.isEmpty(A0prefixA1postfixA2ext[1])) ? A0prefixA1postfixA2ext[1] : "")).trim().replaceFirst("^_$", ""); + final String ext = (A0prefixA1postfixA2ext != null && A0prefixA1postfixA2ext.length > 2 && !TextUtils.isEmpty(A0prefixA1postfixA2ext[2])) ? A0prefixA1postfixA2ext[2] : "jpg"; + return String.format("%s%s%s.%s", prefix.trim(), SDF_IMAGES.format(new Date()), postfix.trim(), ext.toLowerCase().replace(".", "").replace("jpeg", "jpg")); + } + /** * Print a {@link WebView}'s contents, also allows to create a PDF * @@ -223,18 +366,25 @@ public class ShareUtil { * @return {{@link PrintJob}} or null */ @RequiresApi(api = Build.VERSION_CODES.KITKAT) - @SuppressWarnings("deprecation") - public PrintJob print(WebView webview, String jobName) { + public PrintJob print(final WebView webview, final String jobName, final boolean... landscape) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - PrintDocumentAdapter printAdapter; - PrintManager printManager = (PrintManager) webview.getContext().getSystemService(Context.PRINT_SERVICE); + final PrintDocumentAdapter printAdapter; + final PrintManager printManager = (PrintManager) _context.getSystemService(Context.PRINT_SERVICE); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { printAdapter = webview.createPrintDocumentAdapter(jobName); } else { printAdapter = webview.createPrintDocumentAdapter(); } + final PrintAttributes.Builder attrib = new PrintAttributes.Builder(); + if (landscape != null && landscape.length > 0 && landscape[0]) { + attrib.setMediaSize(new PrintAttributes.MediaSize("ISO_A4", "android", 11690, 8270)); + attrib.setMinMargins(new PrintAttributes.Margins(0, 0, 0, 0)); + } if (printManager != null) { - return printManager.print(jobName, printAdapter, new PrintAttributes.Builder().build()); + try { + return printManager.print(jobName, printAdapter, attrib.build()); + } catch (Exception ignored) { + } } } else { Log.e(getClass().getName(), "ERROR: Method called on too low Android API version"); @@ -247,8 +397,7 @@ public class ShareUtil { * See {@link #print(WebView, String) print method} */ @RequiresApi(api = Build.VERSION_CODES.KITKAT) - @SuppressWarnings("deprecation") - public PrintJob createPdf(WebView webview, String jobName) { + public PrintJob createPdf(final WebView webview, final String jobName) { return print(webview, jobName); } @@ -260,21 +409,21 @@ public class ShareUtil { * @return A {@link Bitmap} or null */ @Nullable - public static Bitmap getBitmapFromWebView(WebView webView) { + public static Bitmap getBitmapFromWebView(final WebView webView, final boolean... a0fullpage) { try { //Measure WebView's content - int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); - int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); - webView.measure(widthMeasureSpec, heightMeasureSpec); - webView.layout(0, 0, webView.getMeasuredWidth(), webView.getMeasuredHeight()); + if (a0fullpage != null && a0fullpage.length > 0 && a0fullpage[0]) { + int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); + int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); + webView.measure(widthMeasureSpec, heightMeasureSpec); + webView.layout(0, 0, webView.getMeasuredWidth(), webView.getMeasuredHeight()); + } //Build drawing cache and store its size webView.buildDrawingCache(); - int measuredWidth = webView.getMeasuredWidth(); - int measuredHeight = webView.getMeasuredHeight(); //Creates the bitmap and draw WebView's content on in - Bitmap bitmap = Bitmap.createBitmap(measuredWidth, measuredHeight, Bitmap.Config.ARGB_8888); + Bitmap bitmap = Bitmap.createBitmap(webView.getMeasuredWidth(), webView.getMeasuredHeight(), Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); canvas.drawBitmap(bitmap, 0, bitmap.getHeight(), new Paint()); @@ -293,7 +442,7 @@ public class ShareUtil { * Replace (primary) clipboard contents with given {@code text} * @param text Text to be set */ - public boolean setClipboard(CharSequence text) { + public boolean setClipboard(final CharSequence text) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { android.text.ClipboardManager cm = ((android.text.ClipboardManager) _context.getSystemService(Context.CLIPBOARD_SERVICE)); if (cm != null) { @@ -304,7 +453,10 @@ public class ShareUtil { android.content.ClipboardManager cm = ((android.content.ClipboardManager) _context.getSystemService(Context.CLIPBOARD_SERVICE)); if (cm != null) { ClipData clip = ClipData.newPlainText(_context.getPackageName(), text); - cm.setPrimaryClip(clip); + try { + cm.setPrimaryClip(clip); + } catch (Exception ignored) { + } return true; } } @@ -346,7 +498,7 @@ public class ShareUtil { * @param callback Callback after paste try * @param serverOrNothing Supply one or no hastebin server. If empty, the default gets taken */ - public void pasteOnHastebin(final String text, final Callback.a2 callback, String... serverOrNothing) { + public void pasteOnHastebin(final String text, final Callback.a2 callback, final String... serverOrNothing) { final Handler handler = new Handler(); final String server = (serverOrNothing != null && serverOrNothing.length > 0 && serverOrNothing[0] != null) ? serverOrNothing[0] : "https://hastebin.com"; @@ -368,7 +520,7 @@ public class ShareUtil { * @param body Body (content) text to be prefilled in the mail * @param to recipients to be prefilled in the mail */ - public void draftEmail(String subject, String body, String... to) { + public void draftEmail(final String subject, final String body, final String... to) { Intent intent = new Intent(Intent.ACTION_SENDTO); intent.setData(Uri.parse("mailto:")); if (subject != null) { @@ -389,14 +541,14 @@ public class ShareUtil { * @param receivingIntent The intent from {@link Activity#getIntent()} * @return A file or null if extraction did not succeed */ - public File extractFileFromIntent(Intent receivingIntent) { + public File extractFileFromIntent(final Intent receivingIntent) { String action = receivingIntent.getAction(); String type = receivingIntent.getType(); File tmpf; String tmps; String fileStr; - if ((Intent.ACTION_VIEW.equals(action) || Intent.ACTION_EDIT.equals(action))) { + if ((Intent.ACTION_VIEW.equals(action) || Intent.ACTION_EDIT.equals(action)) || Intent.ACTION_SEND.equals(action)) { // Markor, S.M.T FileManager if (receivingIntent.hasExtra((tmps = EXTRA_FILEPATH))) { return new File(receivingIntent.getStringExtra(tmps)); @@ -419,11 +571,22 @@ public class ShareUtil { fileStr = "/" + fileStr; } // Some do add some custom prefix - for (String prefix : new String[]{"file", "document", "root_files"}) { + for (String prefix : new String[]{"file", "document", "root_files", "name"}) { if (fileStr.startsWith(prefix)) { fileStr = fileStr.substring(prefix.length()); } } + + // prefix for External storage (/storage/emulated/0 /// /sdcard/) --> e.g. "content://com.amaze.filemanager/storage_root/file.txt" = "/sdcard/file.txt" + for (String prefix : new String[]{"external/", "media/", "storage_root/"}) { + if (fileStr.startsWith((tmps = prefix))) { + File f = new File(Uri.decode(Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + fileStr.substring(tmps.length()))); + if (f.exists()) { + return f; + } + } + } + // Next/OwnCloud Fileprovider for (String fp : new String[]{"org.nextcloud.files", "org.nextcloud.beta.files", "org.owncloud.files"}) { if (fileProvider.equals(fp) && fileStr.startsWith(tmps = "external_files/")) { @@ -438,16 +601,615 @@ public class ShareUtil { if (fileProvider.equals("com.mi.android.globalFileexplorer.myprovider") && fileStr.startsWith(tmps = "external_files")) { return new File(Uri.decode(Environment.getExternalStorageDirectory().getAbsolutePath() + fileStr.substring(tmps.length()))); } + + if (fileStr.startsWith(tmps = "external_files/")) { + for (String prefix : new String[]{Environment.getExternalStorageDirectory().getAbsolutePath(), "/storage", ""}) { + File f = new File(Uri.decode(prefix + "/" + fileStr.substring(tmps.length()))); + if (f.exists()) { + return f; + } + } + + } + // URI Encoded paths with full path after content://package/ if (fileStr.startsWith("/") || fileStr.startsWith("%2F")) { tmpf = new File(Uri.decode(fileStr)); if (tmpf.exists()) { return tmpf; + } else if ((tmpf = new File(fileStr)).exists()) { + return tmpf; } } } } + fileUri = receivingIntent.getParcelableExtra(Intent.EXTRA_STREAM); + if (fileUri != null && !TextUtils.isEmpty(tmps = fileUri.getPath()) && tmps.startsWith("/") && (tmpf = new File(tmps)).exists()) { + return tmpf; + } } return null; } + + /** + * Request a picture from gallery + * Result will be available from {@link Activity#onActivityResult(int, int, Intent)}. + * It will return the path to the image if locally stored. If retrieved from e.g. a cloud + * service, the image will get copied to app-cache folder and it's path returned. + */ + public void requestGalleryPicture() { + if (!(_context instanceof Activity)) { + throw new RuntimeException("Error: ShareUtil.requestGalleryPicture needs an Activity Context."); + } + Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); + try { + ((Activity) _context).startActivityForResult(intent, REQUEST_PICK_PICTURE); + } catch (Exception ex) { + Toast.makeText(_context, "No gallery app installed!", Toast.LENGTH_SHORT).show(); + } + } + + public String extractFileFromIntentStr(final Intent receivingIntent) { + File f = extractFileFromIntent(receivingIntent); + return f != null ? f.getAbsolutePath() : null; + } + + /** + * Request a picture from camera-like apps + * Result ({@link String}) will be available from {@link Activity#onActivityResult(int, int, Intent)}. + * It has set resultCode to {@link Activity#RESULT_OK} with same requestCode, if successfully + * The requested image savepath has to be stored at caller side (not contained in intent), + * it can be retrieved using {@link #extractResultFromActivityResult(int, int, Intent, Activity...)} + * returns null if an error happened. + * + * @param target Path to file to write to, if folder the filename gets app_name + millis + random filename. If null DCIM folder is used. + */ + @SuppressWarnings("RegExpRedundantEscape") + public String requestCameraPicture(final File target) { + if (!(_context instanceof Activity)) { + throw new RuntimeException("Error: ShareUtil.requestCameraPicture needs an Activity Context."); + } + String cameraPictureFilepath = null; + Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + if (takePictureIntent.resolveActivity(_context.getPackageManager()) != null) { + File photoFile; + try { + // Create an image file name + if (target != null && !target.isDirectory()) { + photoFile = target; + } else { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH-mm-ss", Locale.ENGLISH); + File storageDir = target != null ? target : new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM), "Camera"); + String imageFileName = ((new ContextUtils(_context).rstr("app_name")).replaceAll("[^a-zA-Z0-9\\.\\-]", "_") + "_").replace("__", "_") + sdf.format(new Date()); + photoFile = new File(storageDir, imageFileName + ".jpg"); + if (!photoFile.getParentFile().exists() && !photoFile.getParentFile().mkdirs()) { + photoFile = File.createTempFile(imageFileName + "_", ".jpg", storageDir); + } + } + + //noinspection StatementWithEmptyBody + if (!photoFile.getParentFile().exists() && photoFile.getParentFile().mkdirs()) ; + + // Save a file: path for use with ACTION_VIEW intents + cameraPictureFilepath = photoFile.getAbsolutePath(); + } catch (IOException ex) { + return null; + } + + // Continue only if the File was successfully created + if (photoFile != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + Uri uri = FileProvider.getUriForFile(_context, getFileProviderAuthority(), photoFile); + takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, uri); + } else { + takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(photoFile)); + } + ((Activity) _context).startActivityForResult(takePictureIntent, REQUEST_CAMERA_PICTURE); + } + } + _lastCameraPictureFilepath = cameraPictureFilepath; + return cameraPictureFilepath; + } + + /** + * Extract result data from {@link Activity#onActivityResult(int, int, Intent)}. + * Forward all arguments from activity. Only requestCodes from {@link ShareUtil} get analyzed. + * Also may forward results via local broadcast + */ + @SuppressLint("ApplySharedPref") + public Object extractResultFromActivityResult(final int requestCode, final int resultCode, final Intent data, final Activity... activityOrNull) { + Activity activity = greedyGetActivity(activityOrNull); + switch (requestCode) { + case REQUEST_CAMERA_PICTURE: { + String picturePath = (resultCode == RESULT_OK) ? _lastCameraPictureFilepath : null; + if (picturePath != null) { + sendLocalBroadcastWithStringExtra(REQUEST_CAMERA_PICTURE + "", EXTRA_FILEPATH, picturePath); + } + return picturePath; + } + case REQUEST_PICK_PICTURE: { + if (resultCode == RESULT_OK && data != null) { + Uri selectedImage = data.getData(); + String[] filePathColumn = {MediaStore.Images.Media.DATA}; + String picturePath = null; + + Cursor cursor = _context.getContentResolver().query(selectedImage, filePathColumn, null, null, null); + if (cursor != null && cursor.moveToFirst()) { + for (String column : filePathColumn) { + int curColIndex = cursor.getColumnIndex(column); + if (curColIndex == -1) { + continue; + } + picturePath = cursor.getString(curColIndex); + if (!TextUtils.isEmpty(picturePath)) { + break; + } + } + cursor.close(); + } + + // Try to grab via file extraction method + data.setAction(Intent.ACTION_VIEW); + picturePath = picturePath != null ? picturePath : extractFileFromIntentStr(data); + + // Retrieve image from file descriptor / Cloud, e.g.: Google Drive, Picasa + if (picturePath == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + try { + ParcelFileDescriptor parcelFileDescriptor = _context.getContentResolver().openFileDescriptor(selectedImage, "r"); + if (parcelFileDescriptor != null) { + FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor(); + FileInputStream input = new FileInputStream(fileDescriptor); + + // Create temporary file in cache directory + picturePath = File.createTempFile("image", "tmp", _context.getCacheDir()).getAbsolutePath(); + FileUtils.writeFile(new File(picturePath), FileUtils.readCloseBinaryStream(input)); + } + } catch (IOException ignored) { + // nothing we can do here, null value will be handled below + } + } + + // Return path to picture on success, else null + if (picturePath != null) { + sendLocalBroadcastWithStringExtra(REQUEST_CAMERA_PICTURE + "", EXTRA_FILEPATH, picturePath); + } + return picturePath; + } + break; + } + + case REQUEST_SAF: { + if (resultCode == RESULT_OK && data != null && data.getData() != null) { + Uri treeUri = data.getData(); + PreferenceManager.getDefaultSharedPreferences(_context).edit().putString(PREF_KEY__SAF_TREE_URI, treeUri.toString()).commit(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + activity.getContentResolver().takePersistableUriPermission(treeUri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + } + return treeUri; + } + break; + } + } + return null; + } + + /** + * Send a local broadcast (to receive within app), with given action and string-extra+value. + * This is a convenience method for quickly sending just one thing. + */ + public void sendLocalBroadcastWithStringExtra(final String action, final String extra, final CharSequence value) { + Intent intent = new Intent(action); + intent.putExtra(extra, value); + LocalBroadcastManager.getInstance(_context).sendBroadcast(intent); + } + + /** + * Receive broadcast results via a callback method + * + * @param callback Function to call with received {@link Intent} + * @param autoUnregister wether or not to automatically unregister receiver after first match + * @param filterActions All {@link IntentFilter} actions to filter for + * @return The created instance. Has to be unregistered on {@link Activity} lifecycle events. + */ + public BroadcastReceiver receiveResultFromLocalBroadcast(final Callback.a2 callback, final boolean autoUnregister, final String... filterActions) { + IntentFilter intentFilter = new IntentFilter(); + for (String filterAction : filterActions) { + intentFilter.addAction(filterAction); + } + final BroadcastReceiver br = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent != null) { + if (autoUnregister) { + LocalBroadcastManager.getInstance(_context).unregisterReceiver(this); + } + try { + callback.callback(intent, this); + } catch (Exception ignored) { + } + } + } + }; + LocalBroadcastManager.getInstance(_context).registerReceiver(br, intentFilter); + return br; + } + + /** + * Request edit of image (by image editor/viewer - for example to crop image) + * + * @param file File that should be edited + */ + public void requestPictureEdit(final File file) { + Uri uri = getUriByFileProviderAuthority(file); + int flags = Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION; + + Intent intent = new Intent(Intent.ACTION_EDIT); + intent.setDataAndType(uri, "image/*"); + intent.addFlags(flags); + intent.putExtra(MediaStore.EXTRA_OUTPUT, uri); + intent.putExtra(EXTRA_FILEPATH, file.getAbsolutePath()); + + for (ResolveInfo resolveInfo : _context.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)) { + String packageName = resolveInfo.activityInfo.packageName; + _context.grantUriPermission(packageName, uri, flags); + } + _context.startActivity(Intent.createChooser(intent, null)); + } + + /** + * Get content://media/ Uri for given file, or null if not indexed + * + * @param file Target file + * @param mode 1 for picture, 2 for video, anything else for other + * @return Media URI + */ + @SuppressWarnings("TryFinallyCanBeTryWithResources") + public Uri getMediaUri(final File file, final int mode) { + Uri uri = MediaStore.Files.getContentUri("external"); + uri = (mode != 0) ? (mode == 1 ? MediaStore.Images.Media.EXTERNAL_CONTENT_URI : MediaStore.Video.Media.EXTERNAL_CONTENT_URI) : uri; + + Cursor cursor = null; + try { + cursor = _context.getContentResolver().query(uri, new String[]{MediaStore.Images.Media._ID}, MediaStore.Images.Media.DATA + "= ?", new String[]{file.getAbsolutePath()}, null); + if (cursor != null && cursor.moveToFirst()) { + int mediaid = cursor.getInt(cursor.getColumnIndex(MediaStore.Images.Media._ID)); + return Uri.withAppendedPath(uri, mediaid + ""); + } + } catch (Exception ignored) { + } finally { + if (cursor != null) { + cursor.close(); + } + } + return null; + } + + /** + * By default Chrome Custom Tabs only uses Chrome Stable to open links + * There are also other packages (like Chrome Beta, Chromium, Firefox, ..) + * which implement the Chrome Custom Tab interface. This method changes + * the customtab intent to use an available compatible browser, if available. + */ + public void enableChromeCustomTabsForOtherBrowsers(final Intent customTabIntent) { + String[] checkpkgs = new String[]{ + "com.android.chrome", "com.chrome.beta", "com.chrome.dev", "com.google.android.apps.chrome", "org.chromium.chrome", + "org.mozilla.fennec_fdroid", "org.mozilla.firefox", "org.mozilla.firefox_beta", "org.mozilla.fennec_aurora", + "org.mozilla.klar", "org.mozilla.focus", + }; + + // Get all intent handlers for web links + PackageManager pm = _context.getPackageManager(); + Intent urlIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://www.example.com")); + List browsers = new ArrayList<>(); + for (ResolveInfo ri : pm.queryIntentActivities(urlIntent, 0)) { + Intent i = new Intent("android.support.customtabs.action.CustomTabsService"); + i.setPackage(ri.activityInfo.packageName); + if (pm.resolveService(i, 0) != null) { + browsers.add(ri.activityInfo.packageName); + } + } + + // Check if the user has a "default browser" selected + ResolveInfo ri = pm.resolveActivity(urlIntent, 0); + String userDefaultBrowser = (ri == null) ? null : ri.activityInfo.packageName; + + // Select which browser to use out of all installed customtab supporting browsers + String pkg = null; + if (browsers.isEmpty()) { + pkg = null; + } else if (browsers.size() == 1) { + pkg = browsers.get(0); + } else if (!TextUtils.isEmpty(userDefaultBrowser) && browsers.contains(userDefaultBrowser)) { + pkg = userDefaultBrowser; + } else { + for (String checkpkg : checkpkgs) { + if (browsers.contains(checkpkg)) { + pkg = checkpkg; + break; + } + } + if (pkg == null && !browsers.isEmpty()) { + pkg = browsers.get(0); + } + } + if (pkg != null && customTabIntent != null) { + customTabIntent.setPackage(pkg); + } + } + + /*** + * Request storage access. The user needs to press "Select storage" at the correct storage. + * @param activity The activity which will receive the result from startActivityForResult + */ + public void requestStorageAccessFramework(final Activity... activity) { + Activity a = greedyGetActivity(activity); + if (a != null && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION + | Intent.FLAG_GRANT_WRITE_URI_PERMISSION + | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION + | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION + ); + a.startActivityForResult(intent, REQUEST_SAF); + } + } + + /** + * Get storage access framework tree uri. The user must have granted access via {@link #requestStorageAccessFramework(Activity...)} + * + * @return Uri or null if not granted yet + */ + public Uri getStorageAccessFrameworkTreeUri() { + String treeStr = PreferenceManager.getDefaultSharedPreferences(_context).getString(PREF_KEY__SAF_TREE_URI, null); + if (!TextUtils.isEmpty(treeStr)) { + try { + return Uri.parse(treeStr); + } catch (Exception ignored) { + } + } + return null; + } + + /** + * Get mounted storage folder root (by tree uri). The user must have granted access via {@link #requestStorageAccessFramework(Activity...)} + * + * @return File or null if SD not mounted + */ + public File getStorageAccessFolder() { + Uri safUri = getStorageAccessFrameworkTreeUri(); + if (safUri != null) { + String safUriStr = safUri.toString(); + ContextUtils cu = new ContextUtils(_context); + for (Pair storage : cu.getStorages(false, true)) { + @SuppressWarnings("ConstantConditions") String storageFolderName = storage.first.getName(); + if (safUriStr.contains(storageFolderName)) { + return storage.first; + } + } + cu.freeContextRef(); + } + return null; + } + + /** + * Check whether or not a file is under a storage access folder (external storage / SD) + * + * @param file The file object (file/folder) + * @return Wether or not the file is under storage access folder + */ + public boolean isUnderStorageAccessFolder(final File file) { + if (file != null) { + // When file writeable as is, it's the fastest way to learn SAF isn't required + if (file.canWrite()) { + return false; + } + ContextUtils cu = new ContextUtils(_context); + for (Pair storage : cu.getStorages(false, true)) { + if (file.getAbsolutePath().startsWith(storage.first.getAbsolutePath())) { + cu.freeContextRef(); + return true; + } + } + cu.freeContextRef(); + } + return false; + } + + /** + * Greedy extract Activity from parameter or convert context if it's a activity + */ + private Activity greedyGetActivity(final Activity... activity) { + if (activity != null && activity.length != 0 && activity[0] != null) { + return activity[0]; + } + if (_context instanceof Activity) { + return (Activity) _context; + } + return null; + } + + /** + * Check whether or not a file can be written. + * Requires storage access framework permission for external storage (SD) + * + * @param file The file object (file/folder) + * @param isDir Wether or not the given file parameter is a directory + * @return Wether or not the file can be written + */ + public boolean canWriteFile(final File file, final boolean isDir) { + if (file == null) { + return false; + } else if (file.getAbsolutePath().startsWith(Environment.getExternalStorageDirectory().getAbsolutePath()) + || file.getAbsolutePath().startsWith(_context.getFilesDir().getAbsolutePath())) { + boolean s1 = isDir && file.getParentFile().canWrite(); + return !isDir && file.getParentFile() != null ? file.getParentFile().canWrite() : file.canWrite(); + } else { + DocumentFile dof = getDocumentFile(file, isDir); + return dof != null && dof.canWrite(); + } + } + + /** + * Get a {@link DocumentFile} object out of a normal java {@link File}. + * When used on a external storage (SD), use {@link #requestStorageAccessFramework(Activity...)} + * first to get access. Otherwise this will fail. + * + * @param file The file/folder to convert + * @param isDir Wether or not file is a directory. For non-existing (to be created) files this info is not known hence required. + * @return A {@link DocumentFile} object or null if file cannot be converted + */ + @SuppressWarnings("RegExpRedundantEscape") + public DocumentFile getDocumentFile(final File file, final boolean isDir) { + // On older versions use fromFile + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { + return DocumentFile.fromFile(file); + } + + // Get ContextUtils to find storageRootFolder + ContextUtils cu = new ContextUtils(_context); + File baseFolderFile = cu.getStorageRootFolder(file); + cu.freeContextRef(); + + String baseFolder = baseFolderFile == null ? null : baseFolderFile.getAbsolutePath(); + boolean originalDirectory = false; + if (baseFolder == null) { + return null; + } + + String relPath = null; + try { + String fullPath = file.getCanonicalPath(); + if (!baseFolder.equals(fullPath)) { + relPath = fullPath.substring(baseFolder.length() + 1); + } else { + originalDirectory = true; + } + } catch (IOException e) { + return null; + } catch (Exception ignored) { + originalDirectory = true; + } + Uri treeUri; + if ((treeUri = getStorageAccessFrameworkTreeUri()) == null) { + return null; + } + DocumentFile dof = DocumentFile.fromTreeUri(_context, treeUri); + if (originalDirectory) { + return dof; + } + String[] parts = relPath.split("\\/"); + for (int i = 0; i < parts.length; i++) { + DocumentFile nextDof = dof.findFile(parts[i]); + if (nextDof == null) { + try { + nextDof = ((i < parts.length - 1) || isDir) ? dof.createDirectory(parts[i]) : dof.createFile("image", parts[i]); + } catch (Exception ignored) { + nextDof = null; + } + } + dof = nextDof; + } + return dof; + } + + public void showMountSdDialog(@StringRes final int title, @StringRes final int description, @DrawableRes final int mountDescriptionGraphic, final Activity... activityOrNull) { + Activity activity = greedyGetActivity(activityOrNull); + if (activity == null) { + return; + } + + // Image viewer + ImageView imv = new ImageView(activity); + imv.setImageResource(mountDescriptionGraphic); + imv.setAdjustViewBounds(true); + + AlertDialog.Builder dialog = new AlertDialog.Builder(activity); + dialog.setView(imv); + dialog.setTitle(title); + dialog.setMessage(_context.getString(description) + "\n\n"); + dialog.setNegativeButton(android.R.string.cancel, null); + dialog.setPositiveButton(android.R.string.yes, (dialogInterface, i) -> requestStorageAccessFramework(activity)); + AlertDialog dialogi = dialog.create(); + dialogi.show(); + } + + @SuppressWarnings({"ResultOfMethodCallIgnored", "StatementWithEmptyBody"}) + public void writeFile(final File file, final boolean isDirectory, final Callback.a2 writeFileCallback) { + try { + FileOutputStream fileOutputStream = null; + ParcelFileDescriptor pfd = null; + final boolean existingEmptyFile = file.canWrite() && file.length() < MIN_OVERWRITE_LENGTH; + final boolean nonExistingCreatableFile = !file.exists() && file.getParentFile().canWrite(); + if (existingEmptyFile || nonExistingCreatableFile) { + if (isDirectory) { + file.mkdirs(); + } else { + fileOutputStream = new FileOutputStream(file); + } + } else { + DocumentFile dof = getDocumentFile(file, isDirectory); + if (dof != null && dof.getUri() != null && dof.canWrite()) { + if (isDirectory) { + // Nothing to do + } else { + pfd = _context.getContentResolver().openFileDescriptor(dof.getUri(), "rwt"); + fileOutputStream = new FileOutputStream(pfd.getFileDescriptor()); + } + } + } + if (writeFileCallback != null) { + writeFileCallback.callback(fileOutputStream != null || (isDirectory && file.exists()), fileOutputStream); + } + if (fileOutputStream != null) { + try { + fileOutputStream.close(); + } catch (Exception ignored) { + } + } + if (pfd != null) { + pfd.close(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Call telephone number. + * Non direct call, opens up the dialer and pre-sets the telephone number. User needs to press manually. + * Direct call requires M permission granted, also add permissions to manifest: + * + * + * @param telNo The telephone number to call + * @param directCall Direct call number if possible + */ + @SuppressWarnings("SimplifiableConditionalExpression") + public void callTelephoneNumber(final String telNo, final boolean... directCall) { + Activity activity = greedyGetActivity(); + if (activity == null) { + throw new RuntimeException("Error: ShareUtil::callTelephoneNumber needs to be contstructed with activity context"); + } + boolean ldirectCall = (directCall != null && directCall.length > 0) ? directCall[0] : true; + + + if (android.os.Build.VERSION.SDK_INT >= 23 && ldirectCall && activity != null) { + if (ContextCompat.checkSelfPermission(activity, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.CALL_PHONE}, 4001); + ldirectCall = false; + } else { + try { + Intent callIntent = new Intent(Intent.ACTION_CALL); + callIntent.setData(Uri.parse("tel:" + telNo)); + activity.startActivity(callIntent); + } catch (Exception ignored) { + ldirectCall = false; + } + } + } + // Show dialer up with telephone number pre-inserted + if (!ldirectCall) { + Intent intent = new Intent(Intent.ACTION_DIAL, Uri.fromParts("tel", telNo, null)); + activity.startActivity(intent); + } + } } diff --git a/app/src/main/res/drawable-anydpi-v26/ic_launcher_test.xml b/app/src/main/res/drawable-anydpi-v26/ic_launcher_test.xml deleted file mode 100644 index f1b3aaa2..00000000 --- a/app/src/main/res/drawable-anydpi-v26/ic_launcher_test.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable-anydpi-v26/ic_launcher_test_round.xml b/app/src/main/res/drawable-anydpi-v26/ic_launcher_test_round.xml deleted file mode 100644 index f1b3aaa2..00000000 --- a/app/src/main/res/drawable-anydpi-v26/ic_launcher_test_round.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable-ldpi/ic_launcher.png b/app/src/main/res/drawable-ldpi/ic_launcher.png index 8e89a6a9..43583ec9 100644 Binary files a/app/src/main/res/drawable-ldpi/ic_launcher.png and b/app/src/main/res/drawable-ldpi/ic_launcher.png differ diff --git a/app/src/main/res/drawable-ldpi/ic_launcher_test.png b/app/src/main/res/drawable-ldpi/ic_launcher_test.png deleted file mode 100644 index a4c4ac0b..00000000 Binary files a/app/src/main/res/drawable-ldpi/ic_launcher_test.png and /dev/null differ diff --git a/app/src/main/res/drawable/ic_open_yt_external_black_24px.xml b/app/src/main/res/drawable/ic_open_yt_external_black_24px.xml new file mode 100644 index 00000000..14e620e1 --- /dev/null +++ b/app/src/main/res/drawable/ic_open_yt_external_black_24px.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/about__fragment_about.xml b/app/src/main/res/layout/about__fragment_about.xml index 61ecc491..4a997dc6 100644 --- a/app/src/main/res/layout/about__fragment_about.xml +++ b/app/src/main/res/layout/about__fragment_about.xml @@ -43,13 +43,13 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="8dp" - android:text="@string/fragment_debug__app_version"/> + android:text="@string/app_version_with_arg"/> @@ -57,7 +57,7 @@ android:textAppearance="@style/TextAppearance.AppCompat" android:layout_width="match_parent" android:layout_height="wrap_content" - android:text="@string/fragment_about__about_text" + android:text="@string/dandelion_is_your_companion__appspecific" android:layout_marginBottom="16dp"/> @@ -86,14 +86,14 @@ style="@android:style/TextAppearance.DeviceDefault.Large" android:layout_width="match_parent" android:layout_height="wrap_content" - android:text="@string/fragment_about__contribute" + android:text="@string/contribute_code" android:layout_marginBottom="8dp"/> + android:text="@string/dandelion_is_developed_as_foss__appspecific"/>