diff --git a/.atomignore b/.atomignore old mode 100644 new mode 100755 index e915f848..033e25f6 --- a/.atomignore +++ b/.atomignore @@ -15,3 +15,9 @@ app/build app/.gitignore app/pom.xml app/proguard-rules.pro +.hidden +.travis.yml +circle.yml +CODE_OF_CONDUCT* +gradle.properties +LICENSE* diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index ac049b92..5ac91667 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,53 +1,24 @@ #### General information +* **App version:** +* **System:** +* **Pod:** -* **Android Device:** -* **Android API Version:** -* **App version:** -* **App source:** -* **Pod version:** +#### Description +#### Log + - -#### Steps to reproduce - -1. … -2. … -3. … - - -#### Expected result -**What is the expected output?** - -**What do you see instead?** - - -Upload screenshots via drag&drop if needed and apply resizing: -`` - - -#### Debug output - -Please post the output of adb logcat. The log should begin with the start of dandelion* and include all the steps it takes to reproduce the problem. -IMPORTANT: In-App debug log must be activated and the app restarted first! - -```` -adb logcat -s com.github.dfa.diaspora_android -```` - -#### In-App debug log - -This is useful for visual bugs without application crashes - -```` -You can long click on the in-app debug log in order to copy it to the clipboard -```` diff --git a/.github/PULL_REQUEST_TEMPLATE b/.github/PULL_REQUEST_TEMPLATE new file mode 100644 index 00000000..863cd426 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE @@ -0,0 +1,27 @@ + + + + + + + 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 8407bc1d..59057232 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,3 @@ -############## -### Project ## -app/src/main/res/raw/changelog.md -app/src/main/res/raw/contributors.md -app/src/main/res/raw/license.md -app/src/main/res/raw/readme.md -app/src/main/res/raw/contributors.txt -#app/src/main/res/raw/podlist.json - ############## ### Common ### *~* @@ -45,6 +36,7 @@ tmp/ ### Gradle ### .gradle build/ +dist/ gradle-app.setting # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) @@ -99,3 +91,14 @@ proguard/ # Android Studio Stuff .navigation/ gen-external-apklibs + +### Project ## +app/src/main/res/raw/changelog.* +app/src/main/res/raw/license.* +app/src/main/res/raw/readme.* +app/src/main/res/raw/contributors.* +app/flavor* + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index c4683a96..00000000 --- a/.travis.yml +++ /dev/null @@ -1,38 +0,0 @@ -language: android -jdk: - - oraclejdk8 -android: - components: - - tools - - tools # TODO https://github.com/travis-ci/travis-ci/issues/6193 - - platform-tools - - build-tools-24.0.3 - - android-24 - - extra-android-m2repository -before_cache: - - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock -cache: - directories: - - $HOME/.gradle/caches/ - - $HOME/.gradle/wrapper/ -script: "./gradlew $TASK" -env: - - TASK="lintFlavorDefaultDebug" - - TASK="build check --stacktrace" - -branches: - except: - - l10n_master - - l10n_master2 - - crowdin -notifications: - webhooks: - urls: - - "https://webhooks.gitter.im/e/e462044d3105a7bb4b4f" - - "https://scalar.vector.im/api/neb/services/hooks/dHJhdmlzLWNpLyU0MGdzYW50bmVyJTNBbWF0cml4Lm9yZy8lMjFNUERTYURTaVBMZXdSQXBxa24lM0FtYXRyaXgub3Jn" - on_success: change # options: [always|never|change] default: always - on_failure: always # options: [always|never|change] default: always - on_start: never # options: [always|never|change] default: always - -after_success: - - bash <(curl -s https://codecov.io/bash) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea05afe3..a5861d19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,82 @@ +### 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 ;) +- Rework screenshot saving and sharing; add new share options: +- Merge license and changelog dialog on first start + +### v1.1.2 +- Fix: loading non-pod links outside customtab/external browser +- Fix: webview-js dialog not dismissing correctly + +### v1.1.0 +- Added: App shortcuts (Android 7+) +- Updated: podlist +- More supported languages +- File sharing fixes + +### v1.0.8 +- Modified: Navigation - Merge bottom toolbar into top +- Updated: Build for Android O/27 +- Updated: Language change preference +- Added: B/W coloring of toolbar popup + +### v1.0.5 +- Updated: Language preference + +### v1.0.4 +- Updated: README +- Added: Hide statusbar option +- Fixed: Language list +- Added: Sardinian,Malayalam,Turkish translation + +### v1.0.3 +- Update opoc +- Better visibility for counter badge +- Refactor DiasporaPod model +- Update PodList (many new pods!) +- Fix CustomTab bug + +### v1.0.2 (2017-08-05) +- Improve build script +- Update translation file license + +### v1.0.1 (2017-07-30) +- Update SimpleMarkdownParser +- Move untranslatable strings + ### v1.0.0 (2017-06-14) - Added AMOLED mode - Improve NavDrawer diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..47984ecf --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,74 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or +advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at . All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index bf621919..00000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,6 +0,0 @@ -We are always open for any kind of contribution. (PR's, bug reports, feature requests, translations, ..) -If you got any questions feel free to join our IRC conference at `#diaspora-android @irc.freenode.net`. -Note that the main project members are mostly busy with their job/university/school and may not react or start coding immediately. - -We use Crowdin to translate dandelion\*. Join our project here: -If your desired language is not listed please contact the maintainers/owner. diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md new file mode 100644 index 00000000..16e6a136 --- /dev/null +++ b/CONTRIBUTORS.md @@ -0,0 +1,32 @@ + +* **[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 +* **[Airon90](https://diasp.eu/u/airon90)**
~° Italian translation +* **[Nacho Fernández](nacho_f AT joindiaspora DOT com)**
~° Spanish translation +* **[Naofumi Fukue](https://github.com/naofum)**
~° Japanese translation +* **[pskosinski](email AT pskosinski DOT pl)**
~° Polish translation +* **[SansPseudoFix](https://github.com/SansPseudoFix)**
~° French translation +* **[Zsolt Szakács](maxigaz AT diaspora DOT zone)**
~° Hungarian translation +* **[Luís F.S. Rosa](https://github.com/luisfsr)**
~° Brazilian Portuguese translation +* **[Danilo Raffaelli](https://crowdin.com/profile/Daraf)**
~° Italian translation +* **[Âng Iōngchun](https://pubpod.alqualonde.org/u/iongchun)**
~° Chinese traditional translation +* **[Mikkel Kirkgaard Nielsen](http://www.mikini.dk)**
~° Danish translation +* **[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/CONTRIBUTORS.txt b/CONTRIBUTORS.txt deleted file mode 100644 index 7905a35a..00000000 --- a/CONTRIBUTORS.txt +++ /dev/null @@ -1,24 +0,0 @@ -00l>> This file contains references to people who contributed to the app. -01l>> If you helped by translating the app, please send a message on Crowdin. -02l>> You can also send a mail to [gdev AT live DOT de](https://gsantner.github.io/about/email/) to get included. -03l>> -04l>> Schemes: -05l>> Firstname Lastname (Link): Text -06l>> Firstname Lastname (E-Mail): Text -07l>> Username (Link): Text -08l>> Username (E-Mail): Text -## 99l CONTRIBUTORS -Abhijith Balan (abhijithb21 AT openmailbox DOT org): Malayalam translation -Airon90 (https://diasp.eu/u/airon90): Italian translation -Gaukler Faun (https://github.com/scoute-dich): Diaspora Native WebApp additions -Martín Vukovic (martinvukovic AT protonmail DOT com): Diaspora Native WebApp -Nacho Fernández (nacho_f AT joindiaspora DOT com): Spanish translation -Naofumi Fukue (https://github.com/naofum): Japanese translation -pskosinski (email AT pskosinski DOT pl): Polish translation -SansPseudoFix (https://github.com/SansPseudoFix): French translation -secitem (secitem AT tuta DOT io): Czech translation -Zsolt Szakács (maxigaz AT diaspora DOT zone): Hungarian translation -Danilo Raffaelli (https://crowdin.com/profile/Daraf): Italian translation -O'Loubám (loubam AT diasp DOT org): Galician translation -transifex3 (https://crowdin.com/profile/transifex3): Korean translation -Âng Iōngchun (https://pubpod.alqualonde.org/u/iongchun): Chinese traditional translation 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 f5c7687e..a061ee60 100644 --- a/README.md +++ b/README.md @@ -1,48 +1,81 @@ -[![F-Droid](https://f-droid.org/wiki/images/0/06/F-Droid-button_get-it-on.png)](https://f-droid.org/repository/browse/?fdid=com.github.dfa.diaspora_android) - -[![CircleCI](https://circleci.com/gh/Diaspora-for-Android/dandelion.svg?style=shield)](https://circleci.com/gh/Diaspora-for-Android/dandelion) -[![Crowdin](https://d322cqt584bo4o.cloudfront.net/diaspora-for-android/localized.svg)](https://crowdin.com/project/diaspora-for-android) -[![Chat - Matrix](https://img.shields.io/badge/chat-on%20matrix-blue.svg)](https://matrix.to/#/#dandelion:matrix.org) - - +[![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\* -FORMERLY KNOWN AS DIASPORA-FOR-ANDROID + +This is an unofficial webview based client for the community-run, distributed social network diaspora*. -This is an unofficial webview based client for the community-run, distributed social network **[diaspora*](https://diasporafoundation.org/)**. It's currently under development and should be used with that in mind. Please submit any bugs you might find. +
+ + Get it on F-Droid + + +

-- Download latest release on ([F-Droid](https://f-droid.org/repository/browse/?fdid=com.github.dfa.diaspora_android)) -- Download latest nightly - [secondlion\*](https://gsantner.gitlab.io/fdroid/latest/com.github.dfa.secondlion.apk) -- Watch [Changelog](https://github.com/Diaspora-for-Android/dandelion/blob/master/CHANGELOG.md) -- See [Screenshots](https://github.com/Diaspora-for-Android/dandelion/blob/master/SCREENSHOTS.md) -- Get updates from our diaspora\* account: [dandelion00@diasp.org](https://diasp.org/people/48b78420923501341ef3782bcb452bd5) -## Contributions -We are always open for any kind of contribution. (PR's, bug reports, feature requests, translations, ..) -If you got any questions feel free to contact us on IRC, XMPP or Gitter. You can start chatting by clicking on the [blue chat badges](#badgers) listed on top. +## Description +This is an unofficial webview based client for the community-run, distributed social network diaspora*. +It's currently under development and should be used with that in mind. Please submit any bugs you might find. -We use Crowdin to translate dandelion\*. Join our project here: . If your desired language is not listed please contact the maintainers/owner. - -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. - -### License -dandelion\* is released under GNU GENERAL PUBLIC LICENSE (see [LICENCE](https://github.com/Diaspora-for-Android/dandelion/blob/master/LICENSE.md)). - -### WebApp -The app is developed as a WebApp because currently diaspora\* doesn't have an API that can be used to create a native interface to retrieve the user's data, publications, direct messages and so on. That's why there are only WebApps for diaspora\* out there. +#### WebApp +The app is developed as a WebApp because currently diaspora\* doesn't have an functional API that can be used to create a native interface to retrieve the user's data, publications, direct messages and so on. That's why there are currently only WebApps for diaspora\* out there. [Stay tuned on diaspora\* issues](https://github.com/diaspora/diaspora/labels/api) about API. Why is a WebApp better than using the mobile site on a browser? Basically it provides better integration with the system (events coming into and going out of the app), notifications, customized interface and functions and a nice little icon that takes you directly to your favorite social network :) -### Device Requirements +#### Device Requirements The minimum Android version supported is Jelly Bean, Android v4.2.0 / API 17 -### App Permissions +### Privacy & Permissions dandelion\* requires access to the Internet and to external storage to be able to upload photos when creating a new post and for taking screenshots. -## Maintainers -- gsantner ([GitHub](https://github.com/gsantner), [Web](https://gsantner.github.io), [diaspora*](https://pod.geraspora.de/people/d1cbdd70095301341e834860008dbc6c)) - - Bitcoin: [1B9ZyYdQoY9BxMe9dRUEKaZbJWsbQqfXU5](https://gsantner.github.io/donate/#donate) + +## Contributions +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. 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/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/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), [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 8f443531..00000000 --- a/SCREENSHOTS.md +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - -
diff --git a/app/build.gradle b/app/build.gradle index 2fafc546..09c8527f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,33 +1,32 @@ apply plugin: 'com.android.application' -apply plugin: 'android-apt' +if (enable_plugin_kotlin) { + apply plugin: 'kotlin-android' + apply plugin: 'kotlin-android-extensions' + apply plugin: 'kotlin-kapt' +} android { - compileSdkVersion 24 - buildToolsVersion "24.0.3" + buildToolsVersion rootProject.ext.version_buildTools + compileSdkVersion rootProject.ext.version_compileSdk defaultConfig { - minSdkVersion 17 - targetSdkVersion 24 - - versionCode 20 - versionName "1.0.0" - + resValue "string", "manifest_package_id", "com.github.dfa.diaspora_android" applicationId "com.github.dfa.diaspora_android" - resValue 'string', 'app_name', "dandelion*" - manifestPlaceholders = [appIcon: "@drawable/ic_launcher"] + 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" - - vectorDrawables.useSupportLibrary = true - } - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } + buildConfigField "String[]", "DETECTED_ANDROID_LOCALES", "${findUsedAndroidLocales()}" + buildConfigField "String", "BUILD_DATE", "\"${getBuildDate()}\"" + buildConfigField "String", "GITHASH", "\"${getGitHash()}\"" + setProperty("archivesBaseName", applicationId + "-v" + versionCode + "-" + versionName) } + flavorDimensions "default" productFlavors { flavorDefault { } @@ -35,78 +34,103 @@ android { /* flavorGplay { 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" } } - lintOptions { - disable 'MissingTranslation' - } -} -// Additional repositories -repositories { - maven { - url "https://jitpack.io" + 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', 'InvalidPackage', 'ObsoleteLintCustomCheck', 'DefaultLocale', 'UnusedAttribute', 'VectorRaster', 'InflateParams', 'IconLocation', 'UnusedResources', 'TypographyEllipsis' + abortOnError false } } dependencies { // Sub-Projects - //compile project(':subprojectFromRoot') + //implementation project(':subprojectFromRoot') // Jars - compile fileTree(dir: 'libs', include: ['*.jar']) - testCompile 'junit:junit:4.12' + implementation fileTree(dir: 'libs', include: ['*.jar']) + testImplementation 'junit:junit:4.13' // Android standard libs - compile 'com.android.support:appcompat-v7:24.2.1' - compile 'com.android.support:design:24.1.0' //Don't update. Broken up to 25.0.0 - compile 'com.android.support:support-v4:24.2.1' - compile 'com.android.support:customtabs:24.2.1' - compile 'com.android.support:cardview-v7:24.2.1' + implementation "com.android.support:appcompat-v7:${version_library_appcompat}" + implementation "com.android.support:design:${version_library_appcompat}" + 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}" - // More libraries - compile 'com.jakewharton:butterknife:8.0.1' - compile 'info.guardianproject.netcipher:netcipher:2.0.0-alpha1' - compile 'info.guardianproject.netcipher:netcipher-webkit:2.0.0-alpha1' - compile 'com.github.DASAR:ShiftColorPicker:v0.5' - apt 'com.jakewharton:butterknife-compiler:8.0.1' -} + // UI libraries + implementation "com.github.DASAR:ShiftColorPicker:v0.5" -// ##################### -// Groovy Coding Area -// ##################### -final String RAW_DIR = "app/src/main/res/raw" -final String[] ROOT_TO_RAW_COPYFILES = ["README.md", "LICENSE.md", "CHANGELOG.md", "CONTRIBUTORS.txt"] - -// Called before building -task copyRepoFiles(type: Copy) { - - // Copy files over to raw dir - from rootProject.files(ROOT_TO_RAW_COPYFILES) - into RAW_DIR.replaceFirst("app/", "") - rename { String fileName -> - fileName.replace(fileName, fileName.toLowerCase()) + // 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-jdk8:${version_plugin_kotlin}" } - // Filter Contributors file - from(rootProject.file("CONTRIBUTORS.txt")) { - into '.' // Target already changed to 'src/main/res/raw' - rename { String fileName -> - fileName.replace(fileName, fileName.toLowerCase()) - } - filter { line -> - (line.toString().matches("..l>>.*") || line.toString().startsWith("## 99l CONTRIBUTORS")) ? null : line.toString().trim().replaceAll(" \\(.*\\)", "") + // Processors + def anpros = ["com.jakewharton:butterknife-compiler:${version_library_butterknife}"] + for (anpro in anpros) { + if (enable_plugin_kotlin) { + kapt anpro + } else { + annotationProcessor anpro } } } -tasks.copyRepoFiles.execute() 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/flavorAtest/res/drawable-hdpi/ic_launcher.png b/app/src/flavorAtest/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 00000000..c30acbbc Binary files /dev/null and b/app/src/flavorAtest/res/drawable-hdpi/ic_launcher.png differ diff --git a/app/src/flavorAtest/res/drawable-hdpi/ic_launcher_round.png b/app/src/flavorAtest/res/drawable-hdpi/ic_launcher_round.png new file mode 100644 index 00000000..c30acbbc Binary files /dev/null and b/app/src/flavorAtest/res/drawable-hdpi/ic_launcher_round.png differ 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/flavorAtest/res/drawable-mdpi/ic_launcher.png b/app/src/flavorAtest/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 00000000..f23d1091 Binary files /dev/null and b/app/src/flavorAtest/res/drawable-mdpi/ic_launcher.png differ diff --git a/app/src/flavorAtest/res/drawable-mdpi/ic_launcher_round.png b/app/src/flavorAtest/res/drawable-mdpi/ic_launcher_round.png new file mode 100644 index 00000000..f23d1091 Binary files /dev/null and b/app/src/flavorAtest/res/drawable-mdpi/ic_launcher_round.png differ diff --git a/app/src/flavorAtest/res/drawable-xhdpi/ic_launcher.png b/app/src/flavorAtest/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 00000000..43025584 Binary files /dev/null and b/app/src/flavorAtest/res/drawable-xhdpi/ic_launcher.png differ diff --git a/app/src/flavorAtest/res/drawable-xhdpi/ic_launcher_round.png b/app/src/flavorAtest/res/drawable-xhdpi/ic_launcher_round.png new file mode 100644 index 00000000..43025584 Binary files /dev/null and b/app/src/flavorAtest/res/drawable-xhdpi/ic_launcher_round.png differ diff --git a/app/src/flavorAtest/res/drawable-xxhdpi/ic_launcher.png b/app/src/flavorAtest/res/drawable-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..3993c4e4 Binary files /dev/null and b/app/src/flavorAtest/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/app/src/flavorAtest/res/drawable-xxhdpi/ic_launcher_round.png b/app/src/flavorAtest/res/drawable-xxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..3993c4e4 Binary files /dev/null and b/app/src/flavorAtest/res/drawable-xxhdpi/ic_launcher_round.png differ diff --git a/app/src/flavorAtest/res/drawable-xxxhdpi/ic_launcher.png b/app/src/flavorAtest/res/drawable-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..72aabea6 Binary files /dev/null and b/app/src/flavorAtest/res/drawable-xxxhdpi/ic_launcher.png differ diff --git a/app/src/flavorAtest/res/drawable-xxxhdpi/ic_launcher_round.png b/app/src/flavorAtest/res/drawable-xxxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..72aabea6 Binary files /dev/null and b/app/src/flavorAtest/res/drawable-xxxhdpi/ic_launcher_round.png differ diff --git a/app/src/flavorAtest/res/drawable/ic_launcher_background.xml b/app/src/flavorAtest/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..e7de1de8 --- /dev/null +++ b/app/src/flavorAtest/res/drawable/ic_launcher_background.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/flavorAtest/res/drawable/ic_launcher_foreground.xml b/app/src/flavorAtest/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 00000000..2610c60c --- /dev/null +++ b/app/src/flavorAtest/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/flavorAtest/res/ic_launcher-web.png b/app/src/flavorAtest/res/ic_launcher-web.png new file mode 100644 index 00000000..8f31241c Binary files /dev/null and b/app/src/flavorAtest/res/ic_launcher-web.png differ 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 150e8241..2e1a7bd1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -7,12 +7,15 @@ + + + + + + + + + + + + + @@ -59,7 +81,7 @@ - + @@ -79,151 +101,705 @@ android:scheme="https" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/ic_launcher-web.png b/app/src/main/ic_launcher-web.png index 39e28103..0a8685ce 100644 Binary files a/app/src/main/ic_launcher-web.png and b/app/src/main/ic_launcher-web.png differ 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 e57c1a55..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 @@ -34,7 +34,9 @@ import com.github.dfa.diaspora_android.util.AppLog; import com.github.dfa.diaspora_android.util.AppSettings; import com.github.dfa.diaspora_android.util.DiasporaUrlHelper; -import io.github.gsantner.opoc.util.AdBlock; +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 e46c73e6..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 @@ -20,12 +20,13 @@ package com.github.dfa.diaspora_android.activity; import android.content.ClipData; import android.content.ClipboardManager; -import android.content.Context; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.Build; import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.design.widget.AppBarLayout; import android.support.design.widget.TabLayout; import android.support.v4.app.Fragment; @@ -34,8 +35,6 @@ import android.support.v4.app.FragmentPagerAdapter; import android.support.v4.view.ViewPager; import android.support.v7.widget.Toolbar; import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; @@ -52,7 +51,7 @@ import com.github.dfa.diaspora_android.ui.theme.ThemedActivity; import com.github.dfa.diaspora_android.ui.theme.ThemedFragment; import com.github.dfa.diaspora_android.util.AppLog; import com.github.dfa.diaspora_android.util.AppSettings; -import com.github.dfa.diaspora_android.util.Helpers; +import com.github.dfa.diaspora_android.util.ContextUtils; import java.util.Observable; import java.util.Observer; @@ -68,16 +67,16 @@ public class AboutActivity extends ThemedActivity implements IntellihideToolbarActivityListener { @BindView(R.id.about__appbar) - protected AppBarLayout appBarLayout; + protected AppBarLayout _appBarLayout; @BindView(R.id.main__topbar) - protected Toolbar toolbar; + protected Toolbar _toolbar; @BindView(R.id.appbar_linear_layout) - protected LinearLayout linearLayout; + protected LinearLayout _linearLayout; @BindView(R.id.tabs) - protected TabLayout tabLayout; + protected TabLayout _tabLayout; @Override protected void onCreate(Bundle savedInstanceState) { @@ -85,9 +84,9 @@ public class AboutActivity extends ThemedActivity setContentView(R.layout.about__activity); ButterKnife.bind(this); - setSupportActionBar(toolbar); - toolbar.setNavigationIcon(getResources().getDrawable(R.drawable.ic_arrow_back_white_24px)); - toolbar.setNavigationOnClickListener(new View.OnClickListener() { + setSupportActionBar(_toolbar); + _toolbar.setNavigationIcon(getResources().getDrawable(R.drawable.ic_arrow_back_white_24px)); + _toolbar.setNavigationOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { AboutActivity.this.onBackPressed(); @@ -101,41 +100,33 @@ public class AboutActivity extends ThemedActivity ViewPager mViewPager = ButterKnife.findById(this, R.id.container); mViewPager.setAdapter(mSectionsPagerAdapter); - tabLayout.setupWithViewPager(mViewPager); + _tabLayout.setupWithViewPager(mViewPager); } @Override public void onResume() { super.onResume(); - if (getAppSettings().isIntellihideToolbars()) { - this.enableToolbarHiding(); - } else { - this.disableToolbarHiding(); - } + setToolbarIntellihide(getAppSettings().isIntellihideToolbars()); } @Override protected void applyColorToViews() { - ThemeHelper.updateToolbarColor(toolbar); - ThemeHelper.updateTabLayoutColor(tabLayout); - ThemeHelper.setPrimaryColorAsBackground(linearLayout); + ThemeHelper.updateToolbarColor(_toolbar); + ThemeHelper.updateTabLayoutColor(_tabLayout); + ThemeHelper.setPrimaryColorAsBackground(_linearLayout); } - @Override - public void enableToolbarHiding() { - AppLog.d(this, "Enable Intellihide"); - AppBarLayout.LayoutParams params = (AppBarLayout.LayoutParams) linearLayout.getLayoutParams(); - //scroll|enterAlways|snap - params.setScrollFlags(toolbarDefaultScrollFlags); - appBarLayout.setExpanded(true, true); - } + public void setToolbarIntellihide(boolean enable) { + AppBarLayout.LayoutParams params = (AppBarLayout.LayoutParams) _linearLayout.getLayoutParams(); + if (enable) { + AppLog.d(this, "Enable Intellihide"); + params.setScrollFlags(toolbarDefaultScrollFlags); - @Override - public void disableToolbarHiding() { - AppLog.d(this, "Disable Intellihide"); - AppBarLayout.LayoutParams params = (AppBarLayout.LayoutParams) linearLayout.getLayoutParams(); - params.setScrollFlags(0); // clear all scroll flags - appBarLayout.setExpanded(true, true); + } else { + AppLog.d(this, "Disable Intellihide"); + params.setScrollFlags(0); // clear all scroll flags + } + _appBarLayout.setExpanded(true, true); } /** @@ -167,20 +158,23 @@ public class AboutActivity extends ThemedActivity } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - View rootView = inflater.inflate(R.layout.about__fragment_about, container, false); - ButterKnife.bind(this, rootView); + protected int getLayoutResId() { + return R.layout.about__fragment_about; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + ButterKnife.bind(this, view); 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(); } } - return rootView; } @Override @@ -198,11 +192,6 @@ public class AboutActivity extends ThemedActivity return TAG; } - @Override - public void onCreateBottomOptionsMenu(Menu menu, MenuInflater inflater) { - /* Nothing to do */ - } - @Override public boolean onBackPressed() { return false; @@ -212,20 +201,20 @@ public class AboutActivity extends ThemedActivity public void buttonClicked(View view) { switch (view.getId()) { case R.id.fragment_about__contribute_button: - Helpers.get().openWebpageInExternalBrowser(getString(R.string.fragment_about__contribute_link)); + ContextUtils.get().openWebpageInExternalBrowser(getString(R.string.fragment_about__contribute_link)); break; case R.id.fragment_about__translate_button: - Helpers.get().openWebpageInExternalBrowser(getString(R.string.fragment_about__translate_link)); + ContextUtils.get().openWebpageInExternalBrowser(getString(R.string.fragment_about__translate_link)); break; case R.id.fragment_about__feedback_button: - Helpers.get().openWebpageInExternalBrowser(getString(R.string.fragment_About__feedback_link)); + ContextUtils.get().openWebpageInExternalBrowser(getString(R.string.fragment_About__feedback_link)); break; case R.id.fragment_about__spread_the_word_button: 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; } } @@ -259,29 +248,32 @@ public class AboutActivity extends ThemedActivity } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View rootView = inflater.inflate(R.layout.about__fragment_license, container, false); - ButterKnife.bind(this, rootView); - final Context context = rootView.getContext(); - accentColor = Helpers.get().colorToHexString(ThemeHelper.getAccentColor()); + protected int getLayoutResId() { + return R.layout.about__fragment_license; + } - maintainers.setTextFormatted(getString(R.string.fragment_license__maintainers_text, - Helpers.get().loadMarkdownForTextViewFromRaw(R.raw.maintainers, ""))); - contributors.setTextFormatted(getString(R.string.fragment_license__contributors_thank_you, - Helpers.get().loadMarkdownForTextViewFromRaw(R.raw.contributors, "* "))); + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + ButterKnife.bind(this, view); + accentColor = ContextUtils.get().colorToHexString(ThemeHelper.getAccentColor()); + + 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.thank_you_witharg, + ContextUtils.get().loadMarkdownForTextViewFromRaw(R.raw.contributors, ""))); thirdPartyLibs.setTextFormatted( - Helpers.get().loadMarkdownForTextViewFromRaw(R.raw.license_third_party, "")); - return rootView; + ContextUtils.get().loadMarkdownForTextViewFromRaw(R.raw.licenses_3rd_party, "")); } @OnClick({R.id.fragment_license__leafpic_button, R.id.fragment_license__license_button}) public void buttonClicked(View v) { switch (v.getId()) { case R.id.fragment_license__leafpic_button: - Helpers.get().openWebpageInExternalBrowser(getString(R.string.fragment_licesen__misc_leafpic_link)); + ContextUtils.get().openWebpageInExternalBrowser(getString(R.string.fragment_licesen__misc_leafpic_link)); break; case R.id.fragment_license__license_button: - Helpers.get().openWebpageInExternalBrowser(getString(R.string.fragment_license__license_gpl_link)); + ContextUtils.get().openWebpageInExternalBrowser(getString(R.string.fragment_license__license_gpl_link)); break; } } @@ -300,11 +292,6 @@ public class AboutActivity extends ThemedActivity return TAG; } - @Override - public void onCreateBottomOptionsMenu(Menu menu, MenuInflater inflater) { - /* Nothing to do */ - } - @Override public boolean onBackPressed() { return false; @@ -354,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!"); } @@ -369,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) { @@ -433,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 4d4f502f..00000000 --- a/app/src/main/java/com/github/dfa/diaspora_android/activity/AspectListFragment.java +++ /dev/null @@ -1,216 +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.Menu; -import android.view.MenuInflater; -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.AppLog; -import com.github.dfa.diaspora_android.util.AppSettings; -import com.github.dfa.diaspora_android.util.DiasporaUrlHelper; -import com.github.dfa.diaspora_android.util.Helpers; - -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 - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - AppLog.d(this, "onCreateView()"); - return inflater.inflate(R.layout.recycler_list__fragment, container, false); - } - - @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); - - 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 void onCreateBottomOptionsMenu(Menu menu, MenuInflater inflater) { - /* Nothing to do */ - } - - @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 : Helpers.get().color(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 f01bb835..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 @@ -25,11 +25,13 @@ import android.app.Activity; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager; +import android.graphics.Bitmap; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.provider.MediaStore; import android.support.design.widget.Snackbar; +import android.support.v4.content.ContextCompat; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; @@ -38,23 +40,30 @@ import android.webkit.JavascriptInterface; import android.webkit.ValueCallback; import android.webkit.WebChromeClient; import android.webkit.WebView; +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.DiasporaUserProfile; import com.github.dfa.diaspora_android.ui.theme.ThemedAlertDialogBuilder; import com.github.dfa.diaspora_android.util.AppLog; +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 com.github.dfa.diaspora_android.util.Helpers; import com.github.dfa.diaspora_android.web.BrowserFragment; import com.github.dfa.diaspora_android.web.DiasporaStreamWebChromeClient; import com.github.dfa.diaspora_android.web.FileUploadWebChromeClient; import com.github.dfa.diaspora_android.web.WebHelper; +import net.gsantner.opoc.util.PermissionChecker; +import net.gsantner.opoc.util.ShareUtil; + import org.json.JSONException; import java.io.File; import java.io.IOException; +import java.util.Date; /** * Fragment that displays the Stream of the diaspora* user @@ -92,13 +101,15 @@ public class DiasporaStreamFragment extends BrowserFragment { } public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - inflater.inflate(R.menu.stream__menu_top, menu); super.onCreateOptionsMenu(menu, inflater); - } + inflater.inflate(R.menu.stream__menu_top, menu); - @Override - public void onCreateBottomOptionsMenu(Menu menu, MenuInflater inflater) { - inflater.inflate(R.menu.stream__menu_bottom, menu); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + menu.findItem(R.id.action_share_pdf).setVisible(true); + } + + final boolean darkBg = ContextUtils.get().shouldColorOnTopBeLight(AppSettings.get().getPrimaryColor()); + ContextUtils.get().tintMenuItems(menu, true, ContextCompat.getColor(getActivity(), darkBg ? R.color.white : R.color.black)); } @Override @@ -117,6 +128,8 @@ public class DiasporaStreamFragment extends BrowserFragment { @Override public boolean onOptionsItemSelected(MenuItem item) { AppLog.d(this, "StreamFragment.onOptionsItemSelected()"); + ShareUtil shu = new ShareUtil(getContext()); + PermissionChecker permc = new PermissionChecker(getActivity()); switch (item.getItemId()) { case R.id.action_reload: { if (WebHelper.isOnline(getContext())) { @@ -139,17 +152,51 @@ 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; + } + + case R.id.action_share_pdf: { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + shu.createPdf(webView, "dandelion-" + ShareUtil.SDF_SHORT.format(new Date())); + } + return true; + } + + case R.id.action_share_link_to_clipboard: { + shu.setClipboard(webView.getUrl()); + Toast.makeText(getContext(), R.string.link_adress_copied, Toast.LENGTH_SHORT).show(); + return true; + } + + case R.id.action_create_launcher_shortcut: { + if (webView.getUrl() != null) { + Intent intent = new Intent(getContext(), MainActivity.class); + intent.setAction(Intent.ACTION_VIEW); + intent.setData(Uri.parse(webView.getUrl())); + shu.createLauncherDesktopShortcut(intent, R.drawable.ic_launcher, webView.getTitle()); + } return true; } case R.id.action_take_screenshot: { - makeScreenshotOfWebView(false); + 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.writeImageToFile(new File(fileSaveDirectory, filename), bmp); + Snackbar.make(webView, getString(R.string.saving_screenshot_as) + + " " + filename, Snackbar.LENGTH_LONG).show(); + } + } return true; } case R.id.action_share_screenshot: { - makeScreenshotOfWebView(true); + if (permc.doIfExtStoragePermissionGranted(getString(R.string.screenshot_permission__appspecific))) { + shu.shareImage(ShareUtil.getBitmapFromWebView(webView)); + } return true; } } @@ -246,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 @@ -271,7 +318,7 @@ public class DiasporaStreamFragment extends BrowserFragment { // Create the File where the photo should go File photoFile; try { - photoFile = Helpers.get().createImageFile(); + photoFile = ContextUtils.get().createImageFile(); takePictureIntent.putExtra("PhotoPath", mCameraPhotoPath); } catch (IOException ex) { AppLog.e(this, "ERROR creating temp file: " + ex.toString()); @@ -324,13 +371,30 @@ public class DiasporaStreamFragment extends BrowserFragment { @SuppressWarnings("unused") @JavascriptInterface public void setUserProfile(final String webMessage) throws JSONException { - final DiasporaUserProfile pup = ((App) getActivity().getApplication()).getDiasporaUserProfile(); - if (pup.isRefreshNeeded()) { - AppLog.v(this, "DiasporaUserProfile needs refresh; Try to parse JSON"); - pup.parseJson(webMessage); - getActivity().runOnUiThread(new Runnable() { + final Activity activity = getActivity(); + if (activity != null) { + activity.runOnUiThread(new Runnable() { + @Override public void run() { - pup.analyzeUrl(webView.getUrl()); + App app = ((App) activity.getApplication()); + final DiasporaUserProfile pup = app.getDiasporaUserProfile(); + if (pup.isRefreshNeeded()) { + try { + // Try to very fail-safe check if user information gets really loaded from correct pod + if (!webView.getUrl().startsWith(app.getSettings().getPod().getPodUrl().getBaseUrl())) { + return; + } + } catch (Exception ignored) { + return; + } + AppLog.v(this, "DiasporaUserProfile needs refresh; Try to parse JSON"); + pup.parseJson(webMessage); + getActivity().runOnUiThread(new Runnable() { + public void run() { + pup.analyzeUrl(webView.getUrl()); + } + }); + } } }); } 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 7dbd14d9..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,14 +36,15 @@ 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; import android.support.v4.view.GravityCompat; import android.support.v4.widget.DrawerLayout; import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBarDrawerToggle; import android.support.v7.app.AlertDialog; -import android.support.v7.widget.ActionMenuView; import android.support.v7.widget.Toolbar; import android.view.KeyEvent; import android.view.Menu; @@ -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,12 +72,16 @@ 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.theme.CustomFragment; +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; +import com.github.dfa.diaspora_android.ui.theme.ThemedFragment; +import com.github.dfa.diaspora_android.util.ActivityUtils; +import com.github.dfa.diaspora_android.util.AndroidBug5497Workaround; import com.github.dfa.diaspora_android.util.AppLog; 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 com.github.dfa.diaspora_android.web.BrowserFragment; import com.github.dfa.diaspora_android.web.ContextMenuWebView; @@ -84,13 +89,13 @@ import com.github.dfa.diaspora_android.web.ProxyHandler; import com.github.dfa.diaspora_android.web.WebHelper; import com.github.dfa.diaspora_android.web.custom_tab.CustomTabActivityHelper; +import net.gsantner.opoc.format.markdown.SimpleMarkdownParser; + import java.io.IOException; import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnClick; -import io.github.gsantner.opoc.util.HelpersA; -import io.github.gsantner.opoc.util.SimpleMarkdownParser; public class MainActivity extends ThemedActivity implements NavigationView.OnNavigationItemSelectedListener, @@ -116,7 +121,7 @@ public class MainActivity extends ThemedActivity private App app; private CustomTabActivityHelper customTabActivityHelper; - private AppSettings appSettings; + private AppSettings _appSettings; private DiasporaUrlHelper urls; private DiasporaUserProfile diasporaUserProfile; private final Handler uiHandler = new Handler(); @@ -135,9 +140,6 @@ public class MainActivity extends ThemedActivity @BindView(R.id.main__topbar) Toolbar toolbarTop; - @BindView(R.id.main__bottombar) - ActionMenuView toolbarBottom; - @BindView(R.id.fragment_container) FrameLayout fragmentContainer; @@ -169,16 +171,26 @@ public class MainActivity extends ThemedActivity super.onCreate(savedInstanceState); AppLog.v(this, "onCreate()"); + // Pre UI + ContextUtils.get().setAppLanguage(AppSettings.get().getLanguage()); + if (AppSettings.get().isEditorStatusBarHidden()) { + getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); + } + + // Bind UI setContentView(R.layout.main__activity); ButterKnife.bind(this); + if (AppSettings.get().isEditorStatusBarHidden()) { + AndroidBug5497Workaround.assistActivity(this); + } app = (App) getApplication(); - appSettings = app.getSettings(); + _appSettings = app.getSettings(); diasporaUserProfile = app.getDiasporaUserProfile(); diasporaUserProfile.setCallbackHandler(uiHandler); diasporaUserProfile.setListener(this); - urls = new DiasporaUrlHelper(appSettings); + urls = new DiasporaUrlHelper(_appSettings); customTabActivityHelper = new CustomTabActivityHelper(); customTabActivityHelper.setConnectionCallback(this); ProxyHandler.getInstance().updateProxySettings(this); @@ -189,7 +201,7 @@ public class MainActivity extends ThemedActivity brOpenExternalLink = new OpenExternalLinkReceiver(this); brSetTitle = new UpdateTitleReceiver(app, urls, new UpdateTitleReceiver.TitleCallback() { public void setTitle(String url, int resId) { - CustomFragment top = getTopFragment(); + ThemedFragment top = getTopFragment(); if (top != null && top.getFragmentTag().equals(DiasporaStreamFragment.TAG)) { MainActivity.this.setTitle(resId); showLastVisitedTimestampMessageIfNeeded(url); @@ -197,14 +209,14 @@ public class MainActivity extends ThemedActivity } public void setTitle(String url, String title) { - CustomFragment top = getTopFragment(); + ThemedFragment top = getTopFragment(); if (top != null && top.getFragmentTag().equals(DiasporaStreamFragment.TAG)) { MainActivity.this.setTitle(title); } } }); - if (!appSettings.hasPod()) { + if (!_appSettings.hasPod()) { AppLog.d(this, "We have no pod. Show PodSelectionFragment"); updateNavigationViewEntryVisibilities(); showFragment(getFragment(PodSelectionFragment.TAG)); @@ -219,25 +231,16 @@ public class MainActivity extends ThemedActivity } } - // Show first start dialog + // Show first start / update dialog try { - if (appSettings.isAppFirstStart()) { - SimpleMarkdownParser smp = new SimpleMarkdownParser().parse( - getResources().openRawResource(R.raw.license), - SimpleMarkdownParser.FILTER_ANDROID_TEXTVIEW, ""); - String html = smp.getHtml() - + "


" - + "

" + getString(R.string.fragment_license__thirdparty_libs) + "

" - + smp.parse(getResources().openRawResource(R.raw.license_third_party), - SimpleMarkdownParser.FILTER_ANDROID_TEXTVIEW, ""); - html = smp.setHtml(html).removeMultiNewlines().getHtml(); - HelpersA.get(this).showDialogWithHtmlTextView(R.string.about_activity__title_about_license, html); - appSettings.isAppCurrentVersionFirstStart(); - } else if (appSettings.isAppCurrentVersionFirstStart()) { - SimpleMarkdownParser smp = new SimpleMarkdownParser().parse( - getResources().openRawResource(R.raw.changelog), - SimpleMarkdownParser.FILTER_ANDROID_TEXTVIEW, ""); - HelpersA.get(this).showDialogWithHtmlTextView(R.string.changelog, smp.getHtml()); + if (_appSettings.isAppCurrentVersionFirstStart(true)) { + SimpleMarkdownParser smp = SimpleMarkdownParser.get().setDefaultSmpFilter(SimpleMarkdownParser.FILTER_ANDROID_TEXTVIEW); + String html = ""; + html += smp.parse(getString(R.string.copyright_license_text_official).replace("\n", " \n"), "").getHtml(); + html += "


" + getString(R.string.changelog) + "
" + smp.parse(getResources().openRawResource(R.raw.changelog), "", SimpleMarkdownParser.FILTER_ANDROID_TEXTVIEW, SimpleMarkdownParser.FILTER_CHANGELOG).getHtml(); + html += "


" + getString(R.string.licenses) + "
" + smp.parse(getResources().openRawResource(R.raw.licenses_3rd_party), "").getHtml(); + ActivityUtils _au = new ActivityUtils(this); + _au.showDialogWithHtmlTextView(R.string.licenses, html); } } catch (IOException e) { e.printStackTrace(); @@ -251,23 +254,17 @@ public class MainActivity extends ThemedActivity private void setupUI() { AppLog.i(this, "setupUI()"); - // Setup toolbar + // Setup _toolbar setSupportActionBar(toolbarTop); ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); } - toolbarBottom.setOnMenuItemClickListener(new ActionMenuView.OnMenuItemClickListener() { - public boolean onMenuItemClick(MenuItem item) { - CustomFragment top = getTopFragment(); - return MainActivity.this.onOptionsItemSelected(item) || (top != null && top.onOptionsItemSelected(item)); - } - }); setTitle(R.string.app_name); //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(); @@ -276,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(); @@ -290,15 +287,15 @@ public class MainActivity extends ThemedActivity } /** - * Get an instance of the CustomFragment with the tag fragmentTag. + * Get an instance of the ThemedFragment with the tag fragmentTag. * If there was no instance so far, create a new one and add it to the FragmentManagers pool. * If there is no Fragment with the corresponding Tag, return the top fragment. * * @param fragmentTag tag * @return corresponding Fragment */ - protected CustomFragment getFragment(String fragmentTag) { - CustomFragment fragment = (CustomFragment) fm.findFragmentByTag(fragmentTag); + protected ThemedFragment getFragment(String fragmentTag) { + ThemedFragment fragment = (ThemedFragment) fm.findFragmentByTag(fragmentTag); if (fragment != null) { return fragment; } else { @@ -311,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(); @@ -336,12 +325,30 @@ public class MainActivity extends ThemedActivity * * @param url URL to load in the DiasporaStreamFragment */ - public void openDiasporaUrl(String url) { + public void openDiasporaUrl(final String url) { AppLog.v(this, "openDiasporaUrl()"); - DiasporaStreamFragment streamFragment = (DiasporaStreamFragment) getFragment(DiasporaStreamFragment.TAG); - showFragment(streamFragment); - showLastVisitedTimestampMessageIfNeeded(url); - streamFragment.loadUrl(url); + if (url != null && url.startsWith("http://127.0.0.1")) { + // 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/gsantner/dandelion/blob/master/README.md"); + LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(i); + }, 1000); + return; + } + if (_appSettings.getPod() != null && _appSettings.getPod().getPodUrl() != null && _appSettings.getPod().getPodUrl().getBaseUrl() != null + && url.startsWith(_appSettings.getPod().getPodUrl().getBaseUrl()) && !url.startsWith("https://dia.so/")) { + DiasporaStreamFragment streamFragment = (DiasporaStreamFragment) getFragment(DiasporaStreamFragment.TAG); + showFragment(streamFragment); + showLastVisitedTimestampMessageIfNeeded(url); + streamFragment.loadUrl(url); + } else { + toolbarTop.postDelayed(() -> { + Intent i = new Intent(ACTION_OPEN_EXTERNAL_URL); + i.putExtra(EXTRA_URL, url); + LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(i); + }, 1000); + } } public void showLastVisitedTimestampMessageIfNeeded(String url) { @@ -356,18 +363,21 @@ public class MainActivity extends ThemedActivity * * @param fragment Fragment to show */ - protected void showFragment(CustomFragment fragment) { + 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()"); - CustomFragment currentTop = (CustomFragment) fm.findFragmentById(R.id.fragment_container); + ThemedFragment currentTop = (ThemedFragment) fm.findFragmentById(R.id.fragment_container); if (currentTop == null || !currentTop.getFragmentTag().equals(fragment.getFragmentTag())) { AppLog.v(this, "Fragment was not visible. Replace it."); fm.beginTransaction().addToBackStack(null).replace(R.id.fragment_container, fragment, fragment.getFragmentTag()).commit(); invalidateOptionsMenu(); - if (appSettings.isIntellihideToolbars() && fragment.isAllowedIntellihide()) { - this.enableToolbarHiding(); - } else { - this.disableToolbarHiding(); - } + setToolbarIntellihide(_appSettings.isIntellihideToolbars() && fragment.isAllowedIntellihide()); } else { AppLog.v(this, "Fragment was already visible. Do nothing."); } @@ -378,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(); @@ -392,7 +402,7 @@ public class MainActivity extends ThemedActivity navProfilePictureArea.setOnClickListener(new View.OnClickListener() { public void onClick(View view) { navDrawer.closeDrawer(GravityCompat.START); - if (!appSettings.getProfileId().equals("")) { + if (!_appSettings.getProfileId().equals("")) { openDiasporaUrl(urls.getProfileUrl()); } } @@ -401,13 +411,13 @@ public class MainActivity extends ThemedActivity navheaderDescription = ButterKnife.findById(navHeader, R.id.podselection__podupti_notice); navheaderImage = ButterKnife.findById(navHeader, R.id.navheader_user_image); - if (!appSettings.getName().equals("")) { - navheaderTitle.setText(appSettings.getName()); + if (!_appSettings.getName().equals("")) { + navheaderTitle.setText(_appSettings.getName()); } - if (appSettings.getPod() != null) { - navheaderDescription.setText(appSettings.getPod().getName()); + if (_appSettings.getPod() != null) { + navheaderDescription.setText(_appSettings.getPod().getName()); } - String avatarUrl = appSettings.getAvatarUrl(); + String avatarUrl = _appSettings.getAvatarUrl(); if (!avatarUrl.equals("")) { //Display app launcher icon instead of default avatar asset //(Which would by the way not load because of missing pod domain prefix in the url) @@ -422,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(); } @@ -435,38 +443,39 @@ public class MainActivity extends ThemedActivity navMenu.setGroupVisible(navMenu.findItem(R.id.nav_exit).getGroupId(), true); // Hide by app settings - navMenu.findItem(R.id.nav_exit).setVisible(appSettings.isVisibleInNavExit()); - navMenu.findItem(R.id.nav_activities).setVisible(appSettings.isVisibleInNavActivities()); - navMenu.findItem(R.id.nav_aspects).setVisible(appSettings.isVisibleInNavAspects()); - navMenu.findItem(R.id.nav_contacts).setVisible(appSettings.isVisibleInNavContacts()); - navMenu.findItem(R.id.nav_commented).setVisible(appSettings.isVisibleInNavCommented()); - navMenu.findItem(R.id.nav_followed_tags).setVisible(appSettings.isVisibleInNavFollowed_tags()); - navMenu.findItem(R.id.nav_about).setVisible(appSettings.isVisibleInNavHelp_license()); - navMenu.findItem(R.id.nav_liked).setVisible(appSettings.isVisibleInNavLiked()); - navMenu.findItem(R.id.nav_mentions).setVisible(appSettings.isVisibleInNavMentions()); - navMenu.findItem(R.id.nav_profile).setVisible(appSettings.isVisibleInNavProfile()); - navMenu.findItem(R.id.nav_public).setVisible(appSettings.isVisibleInNavPublic_activities()); + navMenu.findItem(R.id.nav_exit).setVisible(_appSettings.isVisibleInNavExit()); + navMenu.findItem(R.id.nav_activities).setVisible(_appSettings.isVisibleInNavActivities()); + navMenu.findItem(R.id.nav_aspects).setVisible(_appSettings.isVisibleInNavAspects()); + navMenu.findItem(R.id.nav_contacts).setVisible(_appSettings.isVisibleInNavContacts()); + navMenu.findItem(R.id.nav_commented).setVisible(_appSettings.isVisibleInNavCommented()); + navMenu.findItem(R.id.nav_followed_tags).setVisible(_appSettings.isVisibleInNavFollowed_tags()); + navMenu.findItem(R.id.nav_about).setVisible(_appSettings.isVisibleInNavHelp_license()); + navMenu.findItem(R.id.nav_liked).setVisible(_appSettings.isVisibleInNavLiked()); + navMenu.findItem(R.id.nav_mentions).setVisible(_appSettings.isVisibleInNavMentions()); + navMenu.findItem(R.id.nav_profile).setVisible(_appSettings.isVisibleInNavProfile()); + navMenu.findItem(R.id.nav_public).setVisible(_appSettings.isVisibleInNavPublic_activities()); navMenu.findItem(R.id.nav_stream).setVisible(true); - 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.isVisibleToggleMobileDesktop()); + 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_product_support).setVisible(_appSettings.isVisibleInNavGsantnerAccount()); // Hide whole group (for logged in use) if no pod was selected - if (!appSettings.hasPod()) { + if (!_appSettings.hasPod()) { navMenu.setGroupVisible(navMenu.findItem(R.id.nav_exit).getGroupId(), false); } } /** - * Open Stream when clicked on top toolbar AND preference stream shortcut is true + * Open Stream when clicked on top _toolbar AND preference stream shortcut is true * * @param view selected view */ @OnClick(R.id.main__topbar) public void onToolBarClicked(View view) { AppLog.i(this, "onToolBarClicked()"); - if (appSettings.isTopbarStreamShortcutEnabled() && appSettings.hasPod()) { + if (_appSettings.isTopbarStreamShortcutEnabled() && _appSettings.hasPod()) { onNavigationItemSelected(navView.getMenu().findItem(R.id.nav_stream)); } } @@ -513,7 +522,7 @@ public class MainActivity extends ThemedActivity } } else if (ACTION_CHANGE_ACCOUNT.equals(action)) { AppLog.v(this, "Reset pod data and show PodSelectionFragment"); - appSettings.setPod(null); + _appSettings.setPod(null); runOnUiThread(new Runnable() { public void run() { navheaderTitle.setText(R.string.app_name); @@ -550,6 +559,18 @@ public class MainActivity extends ThemedActivity } else if (Intent.ACTION_SEND_MULTIPLE.equals(action) && type != null) { /* TODO: Implement and add filter to manifest */ return; + } else if ("sc_new_post".equals(action)) { + openDiasporaUrl(urls.getNewPostUrl()); + 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) { @@ -580,8 +601,8 @@ public class MainActivity extends ThemedActivity * * @return top fragment or null if there is none displayed */ - private CustomFragment getTopFragment() { - return (CustomFragment) fm.findFragmentById(R.id.fragment_container); + private ThemedFragment getTopFragment() { + return (ThemedFragment) fm.findFragmentById(R.id.fragment_container); } /** @@ -594,7 +615,7 @@ public class MainActivity extends ThemedActivity navDrawer.closeDrawer(navView); return; } - CustomFragment top = getTopFragment(); + ThemedFragment top = getTopFragment(); if (top != null) { AppLog.v(this, "Top Fragment is not null"); if (!top.onBackPressed()) { @@ -646,48 +667,52 @@ public class MainActivity extends ThemedActivity LocalBroadcastManager.getInstance(this).registerReceiver(brSetTitle, new IntentFilter(ACTION_UPDATE_TITLE_FROM_URL)); LocalBroadcastManager.getInstance(this).registerReceiver(brOpenExternalLink, new IntentFilter(ACTION_OPEN_EXTERNAL_URL)); invalidateOptionsMenu(); - this.appSettings = getAppSettings(); - if (appSettings.isIntellihideToolbars()) { - enableToolbarHiding(); - } else { - disableToolbarHiding(); + _appSettings = getAppSettings(); + if (_appSettings.isRecreateMainActivity()) { + recreate(); + } + setToolbarIntellihide(_appSettings.isIntellihideToolbars()); + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayShowTitleEnabled(AppSettings.get().isShowTitleInMainView()); } updateNavigationViewEntryVisibilities(); } /** - * Clear and repopulate top and bottom toolbar. + * Clear and repopulate top and bottom _toolbar. * Also add menu items of the displayed fragment * - * @param menu top toolbar + * @param menu top _toolbar * @return boolean */ @Override public boolean onCreateOptionsMenu(Menu menu) { AppLog.v(this, "onCreateOptionsMenu()"); + boolean cache; + //Clear the menus menu.clear(); - toolbarBottom.getMenu().clear(); - toolbarBottom.setVisibility(View.VISIBLE); - CustomFragment top = getTopFragment(); + ThemedFragment top = getTopFragment(); if (top != null) { - //PodSelectionFragment? - if (top.getFragmentTag().equals(PodSelectionFragment.TAG)) { - ///Hide bottom toolbar - toolbarBottom.setVisibility(View.GONE); - } else { - getMenuInflater().inflate(appSettings.isExtendedNotificationsActivated() ? - R.menu.main__menu_top__notifications_dropdown : R.menu.main__menu_top, menu); - getMenuInflater().inflate(R.menu.main__menu_bottom, toolbarBottom.getMenu()); - top.onCreateBottomOptionsMenu(toolbarBottom.getMenu(), getMenuInflater()); + if (!top.getFragmentTag().equals(PodSelectionFragment.TAG)) { + cache = _appSettings.isExtendedNotificationsActivated(); + getMenuInflater().inflate(R.menu.main__menu_top, menu); + menu.findItem(R.id.action_notifications).setVisible(!cache); + menu.findItem(R.id.action_notifications_extended).setVisible(cache); } } + + ContextUtils cu = ContextUtils.get(); + final boolean darkBg = cu.get().shouldColorOnTopBeLight(AppSettings.get().getPrimaryColor()); + cu.tintMenuItems(menu, true, ContextCompat.getColor(this, darkBg ? R.color.white : R.color.black)); + cu.setSubMenuIconsVisiblity(menu, true); + return true; } /** - * Set the notification and messages counter in the top toolbar + * Set the notification and messages counter in the top _toolbar * * @param menu menu * @return boolean @@ -720,7 +745,7 @@ public class MainActivity extends ThemedActivity AppLog.i(this, "onOptionsItemSelected()"); switch (item.getItemId()) { case R.id.action_notifications: { - if (appSettings.isExtendedNotificationsActivated()) { + if (_appSettings.isExtendedNotificationsActivated()) { return true; } //Otherwise we execute the action of action_notifications_all @@ -820,7 +845,7 @@ public class MainActivity extends ThemedActivity final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); @SuppressLint("InflateParams") View layout = getLayoutInflater().inflate(R.layout.ui__dialog_search__people_tags, null, false); - final EditText input = (EditText) layout.findViewById(R.id.dialog_search__input); + final EditText input = layout.findViewById(R.id.dialog_search__input); input.setMaxLines(1); input.setSingleLine(true); ThemeHelper.updateEditTextColor(input); @@ -829,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)); } @@ -838,11 +863,11 @@ public class MainActivity extends ThemedActivity } }; - final AlertDialog dialog = new ThemedAlertDialogBuilder(this, appSettings) + 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() { @@ -944,7 +969,7 @@ public class MainActivity extends ThemedActivity private void setSharedTexts(String sharedSubject, String sharedBody) { AppLog.i(this, "setSharedTexts()"); String body = WebHelper.replaceUrlWithMarkdown(sharedBody); - if (appSettings.isAppendSharedViaApp()) { + if (_appSettings.isAppendSharedViaApp()) { AppLog.v(this, "Append app reference to shared text"); body = body + "\n\n" + getString(R.string.shared_via_app); } @@ -981,7 +1006,7 @@ public class MainActivity extends ThemedActivity public void onNotificationCountChanged(DiasporaUserProfile diasporaUserProfile, int notificationCount) { AppLog.i(this, "onNotificationCountChanged()"); // Count saved in DiasporaUserProfile - // Invalidate the top toolbar to update the unread messages counter + // Invalidate the top _toolbar to update the unread messages counter invalidateOptionsMenu(); } @@ -990,7 +1015,7 @@ public class MainActivity extends ThemedActivity public void onUnreadMessageCountChanged(DiasporaUserProfile diasporaUserProfile, int unreadMessageCount) { AppLog.i(this, "onUnreadMessageCountChanged()"); // Count saved in DiasporaUserProfile - // Invalidate the top toolbar to update the unread messages counter + // Invalidate the top _toolbar to update the unread messages counter invalidateOptionsMenu(); } @@ -1031,19 +1056,46 @@ public class MainActivity extends ThemedActivity break; case R.id.nav_profile: { - if (!appSettings.getProfileId().equals("")) { + if (!_appSettings.getProfileId().equals("")) { openDiasporaUrl(urls.getProfileUrl()); } } 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; @@ -1124,6 +1176,11 @@ public class MainActivity extends ThemedActivity } break; + case R.id.nav_product_support: { + openDiasporaUrl(urls.getProfileUrl("d1cbdd70095301341e834860008dbc6c")); + } + break; + case R.id.nav_exit: { moveTaskToBack(true); finish(); @@ -1192,32 +1249,31 @@ public class MainActivity extends ThemedActivity @Override protected void applyColorToViews() { ThemeHelper.updateToolbarColor(toolbarTop); - ThemeHelper.updateActionMenuViewColor(toolbarBottom); - navDrawerLayout.setBackgroundColor(appSettings.getPrimaryColor()); - navProfilePictureArea.setBackgroundColor(appSettings.getPrimaryColor()); - if (appSettings.isAmoledColorMode()) { + navDrawerLayout.setBackgroundColor(_appSettings.getPrimaryColor()); + navProfilePictureArea.setBackgroundColor(_appSettings.getPrimaryColor()); + if (_appSettings.isAmoledColorMode()) { navView.setItemTextColor(ColorStateList.valueOf(Color.GRAY)); navView.setItemIconTintList(ColorStateList.valueOf(Color.GRAY)); navView.setBackgroundColor(Color.BLACK); navheaderTitle.setTextColor(Color.GRAY); navheaderDescription.setTextColor(Color.DKGRAY); } + + int popupTheme = ContextUtils.get().shouldColorOnTopBeLight(AppSettings.get().getPrimaryColor()) + ? R.style.AppTheme_PopupOverlay_Dark : R.style.AppTheme_PopupOverlay_Light; + toolbarTop.setPopupTheme(popupTheme); } - @Override - public void enableToolbarHiding() { - AppLog.d(this, "Enable Intellihide"); + public void setToolbarIntellihide(boolean enable) { AppBarLayout.LayoutParams params = (AppBarLayout.LayoutParams) toolbarTop.getLayoutParams(); - //scroll|enterAlways|snap - params.setScrollFlags(toolbarDefaultScrollFlags); - appBarLayout.setExpanded(true, true); - } + if (enable) { + AppLog.d(this, "Enable Intellihide"); + params.setScrollFlags(toolbarDefaultScrollFlags); - @Override - public void disableToolbarHiding() { - AppLog.d(this, "Disable Intellihide"); - AppBarLayout.LayoutParams params = (AppBarLayout.LayoutParams) toolbarTop.getLayoutParams(); - params.setScrollFlags(0); // clear all scroll flags + } else { + AppLog.d(this, "Disable Intellihide"); + params.setScrollFlags(0); // clear all scroll flags + } appBarLayout.setExpanded(true, true); } } 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 ab102b61..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 @@ -22,17 +22,16 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.res.ColorStateList; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.os.Build; import android.os.Bundle; import android.support.design.widget.Snackbar; +import android.support.v4.content.ContextCompat; import android.support.v4.content.LocalBroadcastManager; import android.support.v4.view.MenuItemCompat; import android.support.v7.widget.AppCompatButton; import android.support.v7.widget.SearchView; -import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; @@ -52,10 +51,10 @@ import com.github.dfa.diaspora_android.data.DiasporaPodList.DiasporaPod; import com.github.dfa.diaspora_android.service.FetchPodsService; import com.github.dfa.diaspora_android.ui.PodSelectionDialog; import com.github.dfa.diaspora_android.ui.theme.ThemedFragment; -import com.github.dfa.diaspora_android.util.AppLog; +import com.github.dfa.diaspora_android.util.ActivityUtils; 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 com.github.dfa.diaspora_android.util.Helpers; import org.json.JSONException; import org.json.JSONObject; @@ -91,16 +90,14 @@ public class PodSelectionFragment extends ThemedFragment implements SearchView.O private String filterString = ""; @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - AppLog.d(this, "onCreateView()"); - View view = inflater.inflate(R.layout.podselection__fragment, container, false); - ButterKnife.bind(this, view); - return view; + protected int getLayoutResId() { + return R.layout.podselection__fragment; } @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); + ButterKnife.bind(this, view); app = (App) getActivity().getApplication(); appSettings = app.getSettings(); @@ -128,11 +125,11 @@ public class PodSelectionFragment extends ThemedFragment implements SearchView.O } }); LocalBroadcastManager.getInstance(getContext()).registerReceiver(podListReceiver, new IntentFilter(FetchPodsService.MESSAGE_PODS_RECEIVED)); - Helpers.get().showInfoIfUserNotConnectedToInternet(getActivity(), listViewPod); + ActivityUtils.get(getActivity()).showInfoIfUserNotConnectedToInternet(listViewPod); } public void mergePodlistWithRessources(DiasporaPodList podlist) { - String sPodlist = Helpers.get().readTextfileFromRawRes(R.raw.podlist, "", ""); + String sPodlist = ContextUtils.get().readTextfileFromRawRes(R.raw.podlist, "", ""); try { JSONObject jPodlist = new JSONObject(sPodlist); podlist.mergeWithNewerEntries(new DiasporaPodList().fromJson(jPodlist)); @@ -164,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(); } } } @@ -176,8 +173,10 @@ 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); - buttonUseCustomPod.setSupportBackgroundTintList(ColorStateList.valueOf(appSettings.isAmoledColorMode() ? Color.DKGRAY : Color.WHITE)); - buttonUseCustomPod.setTextColor(appSettings.isAmoledColorMode() ? Color.WHITE : Color.BLACK); + int bgcolor = appSettings.isAmoledColorMode() ? Color.DKGRAY : appSettings.getAccentColor(); + buttonUseCustomPod.setBackgroundColor(bgcolor); + buttonUseCustomPod.setTextColor(_cu.shouldColorOnTopBeLight(bgcolor) ? Color.WHITE : Color.BLACK); + } @Override @@ -200,7 +199,7 @@ public class PodSelectionFragment extends ThemedFragment implements SearchView.O @Override public View getView(int position, View convertView, ViewGroup parent) { View view = super.getView(position, convertView, parent); - TextView textView = (TextView) view.findViewById(android.R.id.text1); + TextView textView = view.findViewById(android.R.id.text1); textView.setTextColor(appSettings.isAmoledColorMode() ? Color.GRAY : Color.BLACK); return view; } @@ -229,6 +228,7 @@ public class PodSelectionFragment extends ThemedFragment implements SearchView.O @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); inflater.inflate(R.menu.podselection__menu, menu); MenuItem searchItem = menu.findItem(R.id.podselection__action_search); @@ -237,14 +237,15 @@ public class PodSelectionFragment extends ThemedFragment implements SearchView.O searchView.setOnQueryTextListener(this); } - super.onCreateOptionsMenu(menu, inflater); + final boolean darkBg = ContextUtils.get().shouldColorOnTopBeLight(AppSettings.get().getPrimaryColor()); + ContextUtils.get().tintMenuItems(menu, true, ContextCompat.getColor(getActivity(), darkBg ? R.color.white : R.color.black)); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.action_reload: { - if (!Helpers.get().showInfoIfUserNotConnectedToInternet(getActivity(), listViewPod)) { + if (!ActivityUtils.get(getActivity()).showInfoIfUserNotConnectedToInternet(listViewPod)) { Intent i = new Intent(getContext(), FetchPodsService.class); getContext().startService(i); return true; @@ -293,11 +294,6 @@ public class PodSelectionFragment extends ThemedFragment implements SearchView.O /* * Dummy implementations */ - - @Override - public void onCreateBottomOptionsMenu(Menu menu, MenuInflater inflater) { - } - @Override public boolean onQueryTextSubmit(String query) { return false; 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 e5fff475..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 @@ -38,11 +38,6 @@ import butterknife.ButterKnife; import uz.shift.colorpicker.LineColorPicker; import uz.shift.colorpicker.OnColorChangedListener; -/** - * SettingsActivity - * Created by vanitas on 24.10.16. - */ - public class SettingsActivity extends ThemedActivity implements SharedPreferences.OnSharedPreferenceChangeListener { //Toolbar @@ -136,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(); @@ -192,6 +187,12 @@ public class SettingsActivity extends ThemedActivity implements SharedPreference ((SettingsActivity) getActivity()).showFragment(SettingsFragmentDebugging.TAG, true); return true; } + /** Usability */ + else if (settings.isKeyEqual(key, R.string.pref_key__is_overview_statusbar_hidden)) { + AppSettings.get().setRecreateMainActivity(true); + } else if (settings.isKeyEqual(key, R.string.pref_key__language)) { + AppSettings.get().setRecreateMainActivity(true); + } /** Network */ else if (settings.isKeyEqual(key, R.string.pref_key__clear_cache)) { Intent intent = new Intent(getActivity(), MainActivity.class); @@ -225,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() { @@ -270,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))) { @@ -278,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); @@ -304,12 +314,12 @@ public class SettingsActivity extends ThemedActivity implements SharedPreference final ThemedAlertDialogBuilder builder = new ThemedAlertDialogBuilder(context, appSettings); builder.setView(dialogLayout); - final FrameLayout titleBackground = (FrameLayout) dialogLayout.findViewById(R.id.color_picker_dialog__title_background); - final TextView title = (TextView) dialogLayout.findViewById(R.id.color_picker_dialog__title); - final LineColorPicker base = (LineColorPicker) dialogLayout.findViewById(R.id.color_picker_dialog__base_picker); - final LineColorPicker shade = (LineColorPicker) dialogLayout.findViewById(R.id.color_picker_dialog__shade_picker); + final FrameLayout titleBackground = dialogLayout.findViewById(R.id.color_picker_dialog__title_background); + final TextView title = dialogLayout.findViewById(R.id.color_picker_dialog__title); + 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))); @@ -411,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; } } @@ -476,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 3d3ff052..00000000 --- a/app/src/main/java/com/github/dfa/diaspora_android/activity/TagListFragment.java +++ /dev/null @@ -1,215 +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.Menu; -import android.view.MenuInflater; -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.AppLog; -import com.github.dfa.diaspora_android.util.AppSettings; -import com.github.dfa.diaspora_android.util.DiasporaUrlHelper; -import com.github.dfa.diaspora_android.util.Helpers; - -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 - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - AppLog.d(this, "onCreateView()"); - return inflater.inflate(R.layout.recycler_list__fragment, container, false); - } - - @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 void onCreateBottomOptionsMenu(Menu menu, MenuInflater inflater) { - /* Nothing to do */ - } - - @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 : Helpers.get().color(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 2047738b..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 @@ -6,20 +6,22 @@ import org.json.JSONObject; import java.io.Serializable; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.List; /** - * Created by gsantner (https://gsantner.github.io/ 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 * For all Classes a loading and saving to JSON method is available */ +@SuppressWarnings({"WeakerAccess", "unused", "SameParameterValue", "SpellCheckingInspection", "UnusedReturnValue", "JavaDoc", "FieldCanBeLocal"}) public class DiasporaPodList implements Iterable, Serializable { - private static final boolean EXPORT_TOJSON_ACTIVE6 = false; + private static final boolean EXPORT_TOJSON_POST_COUNT_LOCAL = true; private List pods = new ArrayList<>(); private boolean trackMergeChanges = false; private Integer trackAddedIndexStart = -1; @@ -88,8 +90,8 @@ public class DiasporaPodList implements Iterable, S if (updatePodBak.getId() != 0 && updatePod.getId() == 0) { updatePod.setId(updatePodBak.getId()); } - if (updatePodBak.getActive6() != 0 && updatePod.getActive6() == 0) { - updatePod.setActive6(updatePodBak.getActive6()); + if (updatePodBak.getPostCountLocal() != 0 && updatePod.getPostCountLocal() == 0) { + updatePod.setPostCountLocal(updatePodBak.getPostCountLocal()); } if (updatePodBak.getScore() != 0 && updatePod.getScore() == 0) { updatePod.setScore(updatePodBak.getScore()); @@ -179,12 +181,12 @@ public class DiasporaPodList implements Iterable, S * ╚═╝ ╚═════╝ ╚═════╝ */ public static class DiasporaPod implements Iterable, Comparable, Serializable { - private List podUrls = new ArrayList<>(); - private List mainLangs = new ArrayList<>(); - private String name = ""; - private int score = 0; - private int id = 0; - private long active6 = 0; + private List _podUrls = new ArrayList<>(); + private List _mainLangs = new ArrayList<>(); + private String _name = ""; + private int _score = 0; + private int _id = 0; + private long _postCountLocal = 0; public DiasporaPod() { @@ -199,14 +201,14 @@ public class DiasporaPodList implements Iterable, S JSONArray jarr; if (json.has("name")) { - name = json.getString("name"); + _name = json.getString("name"); } if (json.has("mainLangs")) { jarr = json.getJSONArray("mainLangs"); for (int i = 0; i < jarr.length(); i++) { String val = jarr.getString(i); - if (!mainLangs.contains(val)) { - mainLangs.add(val); + if (!_mainLangs.contains(val)) { + _mainLangs.add(val); } } } @@ -214,19 +216,19 @@ public class DiasporaPodList implements Iterable, S jarr = json.getJSONArray("podUrls"); for (int i = 0; i < jarr.length(); i++) { DiasporaPodUrl podUrl = new DiasporaPodUrl().fromJson(jarr.getJSONObject(i)); - if (!podUrls.contains(podUrl)) { - podUrls.add(podUrl); + if (!_podUrls.contains(podUrl)) { + _podUrls.add(podUrl); } } } if (json.has("score")) { - score = json.getInt("score"); + _score = json.getInt("score"); } - if (json.has("active6")) { - active6 = json.getLong("active6"); + if (json.has("postCountLocal")) { + _postCountLocal = json.getLong("postCountLocal"); } if (json.has("id")) { - id = json.getInt("id"); + _id = json.getInt("id"); } return this; } @@ -236,25 +238,28 @@ public class DiasporaPodList implements Iterable, S */ public JSONObject toJson() throws JSONException { JSONObject json = new JSONObject(); - json.put("name", name); - json.put("score", score); - json.put("id", id); + json.put("name", _name); + json.put("id", _id); + + if (_score != 0) { + json.put("score", _score); + } // Only export active6 (frequently changing if told to do) - if (EXPORT_TOJSON_ACTIVE6) { - json.put("active6", active6); + if (EXPORT_TOJSON_POST_COUNT_LOCAL && _postCountLocal > 0) { + json.put("postCountLocal", _postCountLocal); } // Pod urls JSONArray jarr = new JSONArray(); - for (DiasporaPodUrl value : podUrls) { + for (DiasporaPodUrl value : _podUrls) { jarr.put(value.toJson()); } json.put("podUrls", jarr); // main langs jarr = new JSONArray(); - for (String value : mainLangs) { + for (String value : _mainLangs) { jarr.put(value); } json.put("mainLangs", jarr); @@ -268,11 +273,11 @@ public class DiasporaPodList implements Iterable, S DiasporaPod otherPod = (DiasporaPod) o; // Check if id is equal - ret = this.id != 0 && this.id == otherPod.id; + ret = _id != 0 && _id == otherPod._id; - // Check if host is the same (fallback if id is 0) + // Check if _host is the same (fallback if id is 0) if (!ret) { - for (DiasporaPodUrl podUrl : podUrls) { + for (DiasporaPodUrl podUrl : _podUrls) { for (DiasporaPodUrl otherPodUrl : otherPod.getPodUrls()) { if (podUrl.getBaseUrl().equals(otherPodUrl.getBaseUrl())) { ret = true; @@ -292,45 +297,46 @@ public class DiasporaPodList implements Iterable, S if (!myPodUrls.isEmpty() && !otherPodUrls.isEmpty()) { return myPodUrls.get(0).getHost().compareTo(otherPodUrls.get(0).getHost()); } + return _name.compareTo(otherPod.getName()); } - return name.compareTo(otherPod.getName()); + return _name.compareTo(""); } @Override public String toString() { - return name + "(" + id + ")"; + return _name + "(" + _id + ")"; } /** * Iterator for Iterable interface (forEach, ..) */ public Iterator iterator() { - return podUrls.iterator(); + return _podUrls.iterator(); } /* - * Getter & Setter - */ + * Getter & Setter + */ public List getPodUrls() { - return podUrls; + return _podUrls; } public DiasporaPod setPodUrls(List podUrls) { - this.podUrls = podUrls; + _podUrls = podUrls; return this; } public List getMainLangs() { - return mainLangs; + return _mainLangs; } public DiasporaPod setMainLangs(List mainLangs) { - this.mainLangs = mainLangs; + _mainLangs = mainLangs; return this; } public DiasporaPod appendMainLangs(String... values) { - Collections.addAll(this.mainLangs, values); + _mainLangs.addAll(Arrays.asList(values)); return this; } @@ -338,50 +344,50 @@ public class DiasporaPodList implements Iterable, S * Returns the first DiasporaPodUrl in the list */ public DiasporaPodUrl getPodUrl() { - if (podUrls.size() > 0) { - return podUrls.get(0); + if (_podUrls.size() > 0) { + return _podUrls.get(0); } return null; } public DiasporaPod appendPodUrls(DiasporaPodUrl... values) { - Collections.addAll(this.podUrls, values); + _podUrls.addAll(Arrays.asList(values)); return this; } public String getName() { - return name; + return _name; } public DiasporaPod setName(String name) { - this.name = name; + _name = name; return this; } public int getScore() { - return score; + return _score; } public DiasporaPod setScore(int score) { - this.score = score; + _score = score; return this; } - public long getActive6() { - return active6; + public long getPostCountLocal() { + return _postCountLocal; } - public DiasporaPod setActive6(long active6) { - this.active6 = active6; + public DiasporaPod setPostCountLocal(long postCountLocal) { + _postCountLocal = postCountLocal; return this; } public int getId() { - return id; + return _id; } public DiasporaPod setId(int id) { - this.id = id; + _id = id; return this; } @@ -393,9 +399,9 @@ public class DiasporaPodList implements Iterable, S * ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ */ public static class DiasporaPodUrl implements Serializable { - private String host = ""; - private String protocol = "https"; - private Integer port = 443; + private String _host = ""; + private String _protocol = "https"; + private Integer _port = 443; public DiasporaPodUrl() { } @@ -407,10 +413,10 @@ public class DiasporaPodList implements Iterable, S /** * Get the base url * - * @return base url with port + * @return */ public String getBaseUrl() { - return protocol + "://" + host + (isPortNeeded() ? port : ""); + return _protocol + "://" + _host + (isPortNeeded() ? _port : ""); } /** @@ -420,13 +426,13 @@ public class DiasporaPodList implements Iterable, S */ public DiasporaPodUrl fromJson(JSONObject json) throws JSONException { if (json.has("host")) { - host = json.getString("host"); + _host = json.getString("host"); } if (json.has("protocol")) { - protocol = json.getString("protocol"); + _protocol = json.getString("protocol"); } if (json.has("port")) { - port = json.getInt("port"); + _port = json.getInt("port"); } return this; } @@ -436,12 +442,12 @@ public class DiasporaPodList implements Iterable, S */ public JSONObject toJson() throws JSONException { JSONObject json = new JSONObject(); - json.put("host", host); - if (!protocol.equals("https")) { - json.put("protocol", protocol); + json.put("host", _host); + if (!_protocol.equals("https")) { + json.put("protocol", _protocol); } - if (port != 443) { - json.put("port", port); + if (_port != 443) { + json.put("port", _port); } return json; } @@ -467,7 +473,7 @@ public class DiasporaPodList implements Iterable, S * Tells if the ports needs to shown */ public boolean isPortNeeded() { - return !((port == 80 && protocol.equals("http")) || (port == 443 && protocol.equals("https"))); + return !((_port == 80 && _protocol.equals("http")) || (_port == 443 && _protocol.equals("https"))); } @Override @@ -476,6 +482,7 @@ public class DiasporaPodList implements Iterable, S } @Override + @SuppressWarnings("SimplifiableIfStatement") public boolean equals(Object o) { if (o instanceof DiasporaPodUrl) { return getBaseUrl().equals(((DiasporaPodUrl) o).getBaseUrl()); @@ -487,29 +494,29 @@ public class DiasporaPodList implements Iterable, S * GETTER & SETTER */ public String getHost() { - return host; + return _host; } public DiasporaPodUrl setHost(String host) { - this.host = host; + _host = host; return this; } public String getProtocol() { - return protocol; + return _protocol; } public DiasporaPodUrl setProtocol(String protocol) { - this.protocol = protocol; + _protocol = protocol; return this; } public Integer getPort() { - return port; + return _port; } public DiasporaPodUrl setPort(Integer port) { - this.port = port; + _port = port; return this; } } 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 048719c9..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 (https://gsantner.github.io/) 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; @@ -103,7 +103,7 @@ public class DiasporaUserProfile { } // GUID (User id) - if (json.has("guid") && loadGuid(json.getString("guid"))) { + if (json.has("guid") && loadGuid(json.getString("guid")) && !guid.isEmpty()) { appSettings.setProfileId(guid); } 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 2d471060..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 (https://gsantner.github.io/) 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/listener/IntellihideToolbarActivityListener.java b/app/src/main/java/com/github/dfa/diaspora_android/listener/IntellihideToolbarActivityListener.java index dbb51587..58f04704 100644 --- a/app/src/main/java/com/github/dfa/diaspora_android/listener/IntellihideToolbarActivityListener.java +++ b/app/src/main/java/com/github/dfa/diaspora_android/listener/IntellihideToolbarActivityListener.java @@ -10,7 +10,5 @@ import android.support.design.widget.AppBarLayout; public interface IntellihideToolbarActivityListener { int toolbarDefaultScrollFlags = AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP; - void enableToolbarHiding(); - - void disableToolbarHiding(); + void setToolbarIntellihide(boolean enable); } diff --git a/app/src/main/java/com/github/dfa/diaspora_android/service/AvatarImageLoader.java b/app/src/main/java/com/github/dfa/diaspora_android/service/AvatarImageLoader.java index 20f0a4f9..ab975326 100644 --- a/app/src/main/java/com/github/dfa/diaspora_android/service/AvatarImageLoader.java +++ b/app/src/main/java/com/github/dfa/diaspora_android/service/AvatarImageLoader.java @@ -24,6 +24,8 @@ import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.widget.ImageView; +import net.gsantner.opoc.util.DownloadTask; + import java.io.File; public class AvatarImageLoader { @@ -52,7 +54,9 @@ public class AvatarImageLoader { public void startImageDownload(ImageView imageView, String avatarUrl) { if (!avatarUrl.equals("")) { - new ImageDownloadTask(imageView, avatarFile.getAbsolutePath()).execute(avatarUrl); + new DownloadTask(new File(avatarFile.getAbsolutePath()), (ok, file) -> { + loadToImageView(imageView); + }).execute(avatarUrl); } } } 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/service/ImageDownloadTask.java b/app/src/main/java/com/github/dfa/diaspora_android/service/ImageDownloadTask.java deleted file mode 100644 index 0834ee95..00000000 --- a/app/src/main/java/com/github/dfa/diaspora_android/service/ImageDownloadTask.java +++ /dev/null @@ -1,98 +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.service; - -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.os.AsyncTask; -import android.support.annotation.Nullable; -import android.widget.ImageView; - -import com.github.dfa.diaspora_android.util.AppLog; - -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; - -import javax.net.ssl.HttpsURLConnection; - -import info.guardianproject.netcipher.NetCipher; - -/** - * Task that can be used to download images from URLs and store them in storage - * Created by gsantner (https://gsantner.github.io/) on 24.03.16. - */ -public class ImageDownloadTask extends AsyncTask { - private final ImageView imageView; - private String savePath; - - /** - * Download image from URL - * - * @param imageView ImageView to set image to (null = don't set) - * @param savePath Save image to file (null = don't save) - */ - public ImageDownloadTask(@Nullable ImageView imageView, @Nullable String savePath) { - this.imageView = imageView; - this.savePath = savePath; - } - - protected Bitmap doInBackground(String... urls) { - String url = urls[0]; - Bitmap bitmap = null; - FileOutputStream out = null; - InputStream inStream; - HttpsURLConnection connection; - try { - connection = NetCipher.getHttpsURLConnection(url); - inStream = connection.getInputStream(); - bitmap = BitmapFactory.decodeStream(inStream); - - // Save to file if not null - if (savePath != null) { - out = new FileOutputStream(savePath); - bitmap.compress(Bitmap.CompressFormat.PNG, 100, out); - } - - try { - inStream.close(); - } catch (IOException e) {/*Nothing*/} - - connection.disconnect(); - - } catch (Exception e) { - AppLog.e(this, e.getMessage()); - } finally { - try { - if (out != null) { - out.close(); - } - } catch (IOException ignored) { - } - } - return bitmap; - } - - protected void onPostExecute(Bitmap result) { - // Display on imageview if not null - if (imageView != null) { - imageView.setImageBitmap(result); - } - } -} diff --git a/app/src/main/java/com/github/dfa/diaspora_android/ui/BadgeDrawable.java b/app/src/main/java/com/github/dfa/diaspora_android/ui/BadgeDrawable.java index 890295c9..39701d29 100644 --- a/app/src/main/java/com/github/dfa/diaspora_android/ui/BadgeDrawable.java +++ b/app/src/main/java/com/github/dfa/diaspora_android/ui/BadgeDrawable.java @@ -28,95 +28,80 @@ import android.graphics.Rect; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; -import android.support.v4.content.ContextCompat; +import android.support.annotation.NonNull; import com.github.dfa.diaspora_android.R; import com.github.dfa.diaspora_android.util.AppSettings; +import com.github.dfa.diaspora_android.util.ContextUtils; +@SuppressWarnings("WeakerAccess") public class BadgeDrawable extends Drawable { // Source: http://mobikul.com/adding-badge-count-on-menu-items-like-cart-notification-etc/ private static final String BADGE_VALUE_OVERFLOW = "*"; - private Paint badgeBackground; - private Paint badgeStroke; - private Paint badgeText; - private Rect textRect = new Rect(); + private Paint _badgeBackground; + private Paint _badgeText; + private Rect _textRect = new Rect(); - private String badgeValue = ""; - private boolean shouldDraw; + private String _badgeValue = ""; + private boolean _shouldDraw; public BadgeDrawable(Context context) { float textSize = context.getResources().getDimension(R.dimen.textsize_badge_count); AppSettings settings = AppSettings.get(); - badgeBackground = new Paint(); - badgeBackground.setColor(settings.getAccentColor()); - badgeBackground.setAntiAlias(true); - badgeBackground.setStyle(Paint.Style.FILL); - badgeStroke = new Paint(); - badgeStroke.setColor(ContextCompat.getColor(context.getApplicationContext(), R.color.colorPrimaryDark)); - badgeStroke.setAntiAlias(true); - badgeStroke.setStyle(Paint.Style.FILL); + _badgeBackground = new Paint(); + _badgeBackground.setColor(settings.getAccentColor()); + _badgeBackground.setAntiAlias(true); + _badgeBackground.setStyle(Paint.Style.FILL); - badgeText = new Paint(); - badgeText.setColor(Color.WHITE); - badgeText.setTypeface(Typeface.DEFAULT); - badgeText.setTextSize(textSize); - badgeText.setAntiAlias(true); - badgeText.setTextAlign(Paint.Align.CENTER); + _badgeText = new Paint(); + _badgeText.setColor(ContextUtils.get().shouldColorOnTopBeLight(settings.getAccentColor()) ? Color.WHITE : Color.BLACK); + _badgeText.setTypeface(Typeface.DEFAULT); + _badgeText.setTextSize(textSize); + _badgeText.setAntiAlias(true); + _badgeText.setTextAlign(Paint.Align.CENTER); } @Override - public void draw(Canvas canvas) { - if (!shouldDraw) { + public void draw(@NonNull Canvas canvas) { + if (!_shouldDraw) { return; } Rect bounds = getBounds(); float width = bounds.right - bounds.left; float height = bounds.bottom - bounds.top; + float oneDp = ContextUtils.get().convertDpToPx(1); // Position the badge in the top-right quadrant of the icon. float radius = ((Math.max(width, height) / 2)) / 2; - float centerX = (width - radius - 1) + 5; - float centerY = radius - 5; - if (badgeValue.length() <= 2) { - // Draw badge circle. - canvas.drawCircle(centerX, centerY, (int) (radius + 7.5), badgeStroke); - canvas.drawCircle(centerX, centerY, (int) (radius + 5.5), badgeBackground); - } else { - canvas.drawCircle(centerX, centerY, (int) (radius + 8.5), badgeStroke); - canvas.drawCircle(centerX, centerY, (int) (radius + 6.5), badgeBackground); - //canvas.drawRoundRect(radius, radius, radius, radius, 10, 10, badgeBackground); - } + float centerX = (width - radius - 1) + oneDp * 2; + float centerY = radius - 2 * oneDp; + canvas.drawCircle(centerX, centerY, (int) (radius + oneDp * 5), _badgeBackground); + // Draw badge count message inside the circle. - badgeText.getTextBounds(badgeValue, 0, badgeValue.length(), textRect); - float textHeight = textRect.bottom - textRect.top; + _badgeText.getTextBounds(_badgeValue, 0, _badgeValue.length(), _textRect); + float textHeight = _textRect.bottom - _textRect.top; float textY = centerY + (textHeight / 2f); - if (badgeValue.length() > 2) - canvas.drawText(BADGE_VALUE_OVERFLOW, centerX, textY, badgeText); - else - canvas.drawText(badgeValue, centerX, textY, badgeText); + canvas.drawText(_badgeValue.length() > 2 ? BADGE_VALUE_OVERFLOW : _badgeValue, + centerX, textY, _badgeText); } - /* - Sets the count (i.e notifications) to display. - */ - public void setCount(String count) { - badgeValue = count; + // Sets the text to display. Badge displays a '*' if more than 2 characters + private void setBadgeText(String text) { + _badgeValue = text; - // Only draw a badge if there are notifications. - shouldDraw = !count.equalsIgnoreCase("0"); + // Only draw a badge if the value isn't a zero + _shouldDraw = !text.equalsIgnoreCase("0"); invalidateSelf(); } @Override public void setAlpha(int alpha) { - // do nothing } @Override public void setColorFilter(ColorFilter cf) { - // do nothing } @Override @@ -125,11 +110,11 @@ public class BadgeDrawable extends Drawable { } public static void setBadgeCount(Context context, LayerDrawable icon, Integer count) { - setBadgeCount(context, icon, count.toString()); + setBadgeText(context, icon, count.toString()); } - public static void setBadgeCount(Context context, LayerDrawable icon, String count) { - + // Max of 2 characters + public static void setBadgeText(Context context, LayerDrawable icon, String text) { BadgeDrawable badge; // Reuse drawable if possible @@ -140,7 +125,7 @@ public class BadgeDrawable extends Drawable { badge = new BadgeDrawable(context); } - badge.setCount(count); + badge.setBadgeText(text); icon.mutate(); icon.setDrawableByLayerId(R.id.ic_badge, badge); } diff --git a/app/src/main/java/com/github/dfa/diaspora_android/ui/HtmlTextView.java b/app/src/main/java/com/github/dfa/diaspora_android/ui/HtmlTextView.java index b542ab52..26552ec1 100644 --- a/app/src/main/java/com/github/dfa/diaspora_android/ui/HtmlTextView.java +++ b/app/src/main/java/com/github/dfa/diaspora_android/ui/HtmlTextView.java @@ -18,14 +18,13 @@ */ package com.github.dfa.diaspora_android.ui; -import android.annotation.TargetApi; import android.content.Context; +import android.support.v7.widget.AppCompatTextView; import android.text.Html; import android.text.SpannableString; import android.text.util.Linkify; import android.util.AttributeSet; import android.util.Patterns; -import android.widget.TextView; import com.github.dfa.diaspora_android.activity.MainActivity; @@ -38,7 +37,7 @@ import java.util.regex.Pattern; * Hashtags open the MainActivity, load the new-post site of the selected pod and insert the * hashtag into the post editor. See data/HashtagContentProvider. */ -public class HtmlTextView extends TextView { +public class HtmlTextView extends AppCompatTextView { public HtmlTextView(Context context) { super(context); @@ -55,12 +54,6 @@ public class HtmlTextView extends TextView { init(); } - @TargetApi(21) - public HtmlTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - init(); - } - /** * Linkify, format markdown and escape the displayed message. */ 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 77279ab6..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 (https://gsantner.github.io) 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/ui/theme/CustomFragment.java b/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/CustomFragment.java deleted file mode 100644 index e01c5f5b..00000000 --- a/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/CustomFragment.java +++ /dev/null @@ -1,73 +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.ui.theme; - -import android.os.Bundle; -import android.support.v4.app.Fragment; -import android.view.Menu; -import android.view.MenuInflater; - -/** - * Customized abstract Fragment class with some useful methods - * Created by vanitas on 21.09.16. - */ - -public abstract class CustomFragment extends Fragment { - - public static final String TAG = "com.github.dfa.diaspora_android.ui.theme.CustomFragment"; - - /** - * We have an optionsMenu - * - * @param savedInstanceState state - */ - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setHasOptionsMenu(true); - } - - /** - * Return the tag used to identify the Fragment. - * - * @return tag - */ - public abstract String getFragmentTag(); - - /** - * Add fragment-dependent options to the bottom options toolbar - * - * @param menu bottom menu - * @param inflater inflater - */ - public abstract void onCreateBottomOptionsMenu(Menu menu, MenuInflater inflater); - - /** - * Return true if the fragment reacted to a back button press, false else. - * In case the fragment returned false, the parent activity should handle the backPress. - * - * @return did we react to the back press? - */ - public abstract boolean onBackPressed(); - - public boolean isAllowedIntellihide() { - return true; - } -} - diff --git a/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/ThemedActivity.java b/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/ThemedActivity.java index 407a1920..d8651bb5 100644 --- a/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/ThemedActivity.java +++ b/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/ThemedActivity.java @@ -19,19 +19,14 @@ package com.github.dfa.diaspora_android.ui.theme; import android.annotation.TargetApi; -import android.app.ActivityManager; import android.content.pm.ActivityInfo; -import android.content.res.Configuration; -import android.graphics.drawable.BitmapDrawable; import android.os.Build; import android.support.v7.app.AppCompatActivity; import com.github.dfa.diaspora_android.App; import com.github.dfa.diaspora_android.R; import com.github.dfa.diaspora_android.util.AppSettings; -import com.github.dfa.diaspora_android.util.Helpers; - -import java.util.Locale; +import com.github.dfa.diaspora_android.util.ContextUtils; /** * Activity that supports color schemes @@ -72,15 +67,7 @@ public abstract class ThemedActivity extends AppCompatActivity { */ @TargetApi(Build.VERSION_CODES.LOLLIPOP) private void updateRecentAppColor() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - BitmapDrawable drawable = ((BitmapDrawable) getDrawable(R.drawable.ic_launcher)); - if (drawable != null) { - setTaskDescription(new ActivityManager.TaskDescription( - getResources().getString(R.string.app_name), - drawable.getBitmap(), - getAppSettings().getPrimaryColor())); - } - } + } protected void updateScreenRotation() { @@ -99,9 +86,6 @@ public abstract class ThemedActivity extends AppCompatActivity { public void updateLanguage() { AppSettings appSettings = getAppSettings(); - Locale locale = Helpers.get().getLocaleByAndroidCode(appSettings.getLanguage()); - Configuration config = appSettings.getContext().getResources().getConfiguration(); - config.locale = locale != null ? locale : Locale.getDefault(); - appSettings.getContext().getResources().updateConfiguration(config, null); + ContextUtils.get().setAppLanguage(appSettings.getLanguage()); } } diff --git a/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/ThemedCheckBoxPreference.java b/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/ThemedCheckBoxPreference.java index ed4f1400..0c829342 100644 --- a/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/ThemedCheckBoxPreference.java +++ b/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/ThemedCheckBoxPreference.java @@ -41,7 +41,7 @@ public class ThemedCheckBoxPreference extends CheckBoxPreference implements Them @Override public void setColors() { - CheckBox checkBox = (CheckBox) rootLayout.findViewById(android.R.id.checkbox); + CheckBox checkBox = rootLayout.findViewById(android.R.id.checkbox); ThemeHelper.getInstance(AppSettings.get()); ThemeHelper.updateCheckBoxColor(checkBox); } diff --git a/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/ThemedColorPickerPreference.java b/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/ThemedColorPickerPreference.java index 544e8d9c..c335d929 100644 --- a/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/ThemedColorPickerPreference.java +++ b/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/ThemedColorPickerPreference.java @@ -10,7 +10,7 @@ import android.widget.ImageView; import com.github.dfa.diaspora_android.R; import com.github.dfa.diaspora_android.util.AppSettings; -import com.github.dfa.diaspora_android.util.Helpers; +import com.github.dfa.diaspora_android.util.ContextUtils; /** * Preference that shows selected Color in a circle @@ -38,7 +38,7 @@ public class ThemedColorPickerPreference extends Preference implements Themeable @Override protected void onBindView(View view) { super.onBindView(view); - colorPreview = (ImageView) view.findViewById(android.R.id.icon); + colorPreview = view.findViewById(android.R.id.icon); setColors(); } @@ -50,13 +50,13 @@ public class ThemedColorPickerPreference extends Preference implements Themeable AppSettings appSettings = AppSettings.get(); String key = getKey(); - int color = Helpers.get().color(R.color.primary); + int color = ContextUtils.get().rcolor(R.color.primary); if ((appSettings.isKeyEqual(key, R.string.pref_key__primary_color_shade))) { color = appSettings.getPrimaryColor(); } else if ((appSettings.isKeyEqual(key, R.string.pref_key__accent_color_shade))) { color = appSettings.getAccentColor(); } else { - color = appSettings.getColor(getSharedPreferences(), key, color); + color = appSettings.getColor(key, color, getSharedPreferences()); } circle.setColorFilter(color, PorterDuff.Mode.SRC_ATOP); } diff --git a/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/ThemedFragment.java b/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/ThemedFragment.java index b68da449..6158a4a9 100644 --- a/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/ThemedFragment.java +++ b/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/ThemedFragment.java @@ -21,12 +21,14 @@ package com.github.dfa.diaspora_android.ui.theme; import com.github.dfa.diaspora_android.App; import com.github.dfa.diaspora_android.util.AppSettings; +import net.gsantner.opoc.activity.GsFragmentBase; + /** * Fragment that supports color schemes * Created by vanitas on 06.10.16. */ -public abstract class ThemedFragment extends CustomFragment { +public abstract class ThemedFragment extends GsFragmentBase { protected AppSettings getAppSettings() { return ((App) getActivity().getApplication()).getSettings(); } @@ -39,4 +41,9 @@ public abstract class ThemedFragment extends CustomFragment { ThemeHelper.getInstance(getAppSettings()); applyColorToViews(); } + + + public boolean isAllowedIntellihide() { + return true; + } } diff --git a/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/ThemedPreferenceCategory.java b/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/ThemedPreferenceCategory.java index 28b97142..5df13fda 100644 --- a/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/ThemedPreferenceCategory.java +++ b/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/ThemedPreferenceCategory.java @@ -36,7 +36,7 @@ public class ThemedPreferenceCategory extends PreferenceCategory implements Them @Override protected View onCreateView(ViewGroup parent) { View rootLayout = super.onCreateView(parent); - this.titleTextView = (TextView) rootLayout.findViewById(android.R.id.title); + this.titleTextView = rootLayout.findViewById(android.R.id.title); setColors(); return rootLayout; } diff --git a/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/ThemedVisibilityPreference.java b/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/ThemedVisibilityPreference.java index 8e94dac4..6bd8c186 100644 --- a/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/ThemedVisibilityPreference.java +++ b/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/ThemedVisibilityPreference.java @@ -27,7 +27,7 @@ public class ThemedVisibilityPreference extends ThemedCheckBoxPreference { @Override public void setColors() { - CheckBox checkBox = (CheckBox) rootLayout.findViewById(android.R.id.checkbox); + CheckBox checkBox = rootLayout.findViewById(android.R.id.checkbox); checkBox.setButtonDrawable(R.drawable.ic_visibility_selector); ThemeHelper.getInstance(AppSettings.get()); ThemeHelper.updateCheckBoxColor(checkBox); diff --git a/app/src/main/java/com/github/dfa/diaspora_android/util/Helpers.java b/app/src/main/java/com/github/dfa/diaspora_android/util/ActivityUtils.java similarity index 67% rename from app/src/main/java/com/github/dfa/diaspora_android/util/Helpers.java rename to app/src/main/java/com/github/dfa/diaspora_android/util/ActivityUtils.java index e98befde..09ecb767 100644 --- a/app/src/main/java/com/github/dfa/diaspora_android/util/Helpers.java +++ b/app/src/main/java/com/github/dfa/diaspora_android/util/ActivityUtils.java @@ -2,11 +2,13 @@ package com.github.dfa.diaspora_android.util; import android.app.Activity; import android.content.Context; +import android.net.Uri; import android.os.Bundle; import android.os.Environment; +import android.support.v4.content.FileProvider; import android.view.View; -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.web.WebHelper; @@ -17,24 +19,22 @@ import java.util.Arrays; import java.util.Date; import java.util.Locale; -import io.github.gsantner.opoc.util.HelpersA; - @SuppressWarnings({"WeakerAccess", "unused", "SameParameterValue"}) -public class Helpers extends io.github.gsantner.opoc.util.Helpers { - protected Helpers(Context context) { - super(context); +public class ActivityUtils extends net.gsantner.opoc.util.ActivityUtils { + public ActivityUtils(Activity activity) { + super(activity); } - public static Helpers get() { - return new Helpers(App.get()); + public static ActivityUtils get(Activity activity) { + return new ActivityUtils(activity); } public File createImageFile() throws IOException { // Create an image file name String timeStamp = new SimpleDateFormat("dd-MM-yy_HH-mm", Locale.getDefault()).format(new Date()); String imageFileName = "JPEG_" + timeStamp + "_"; - AppLog.d(Helpers.class, Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getAbsolutePath()); + AppLog.d(ActivityUtils.class, Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getAbsolutePath()); File storageDir = Environment.getExternalStoragePublicDirectory( Environment.DIRECTORY_PICTURES); return new File( @@ -47,13 +47,12 @@ public class Helpers extends io.github.gsantner.opoc.util.Helpers { /** * Show Information if user is offline, returns true if is not connected to internet * - * @param activity Activity - * @param anchor A view anchor + * @param anchor A view anchor */ - public boolean showInfoIfUserNotConnectedToInternet(Activity activity, View anchor) { - boolean isOnline = WebHelper.isOnline(context); + public boolean showInfoIfUserNotConnectedToInternet(View anchor) { + boolean isOnline = WebHelper.isOnline(_context); if (!isOnline) { - HelpersA.get(activity).showSnackBar(R.string.no_internet, true); + showSnackBar(R.string.sorry_need_to_be_connected_to_internet, true); } return !isOnline; } @@ -75,4 +74,13 @@ public class Helpers extends io.github.gsantner.opoc.util.Helpers { } } } + + /** + * This method creates file sharing uri by using FileProvider + * + * @return + */ + public static Uri getFileSharingUri(Context context, File file) { + return FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID, file); + } } diff --git a/app/src/main/java/com/github/dfa/diaspora_android/util/AndroidBug5497Workaround.java b/app/src/main/java/com/github/dfa/diaspora_android/util/AndroidBug5497Workaround.java new file mode 100644 index 00000000..98017bde --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/util/AndroidBug5497Workaround.java @@ -0,0 +1,58 @@ +package com.github.dfa.diaspora_android.util; + + +import android.app.Activity; +import android.graphics.Rect; +import android.view.View; +import android.view.ViewTreeObserver; +import android.widget.FrameLayout; + +// From https://stackoverflow.com/a/19494006 +public class AndroidBug5497Workaround { + + // For more information, see https://code.google.com/p/android/issues/detail?id=5497 + // To use this class, simply invoke assistActivity() on an Activity that already has its content view set. + + public static void assistActivity(Activity activity) { + new AndroidBug5497Workaround(activity); + } + + private View mChildOfContent; + private int usableHeightPrevious; + private FrameLayout.LayoutParams frameLayoutParams; + + private AndroidBug5497Workaround(Activity activity) { + FrameLayout content = activity.findViewById(android.R.id.content); + mChildOfContent = content.getChildAt(0); + mChildOfContent.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + public void onGlobalLayout() { + possiblyResizeChildOfContent(); + } + }); + frameLayoutParams = (FrameLayout.LayoutParams) mChildOfContent.getLayoutParams(); + } + + private void possiblyResizeChildOfContent() { + int usableHeightNow = computeUsableHeight(); + if (usableHeightNow != usableHeightPrevious) { + int usableHeightSansKeyboard = mChildOfContent.getRootView().getHeight(); + int heightDifference = usableHeightSansKeyboard - usableHeightNow; + if (heightDifference > (usableHeightSansKeyboard / 4)) { + // keyboard probably just became visible + frameLayoutParams.height = usableHeightSansKeyboard - heightDifference; + } else { + // keyboard probably just became hidden + frameLayoutParams.height = usableHeightSansKeyboard; + } + mChildOfContent.requestLayout(); + usableHeightPrevious = usableHeightNow; + } + } + + private int computeUsableHeight() { + Rect r = new Rect(); + mChildOfContent.getWindowVisibleDisplayFrame(r); + return (r.bottom - r.top); + } + +} \ No newline at end of file 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 74451ea7..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 @@ -18,6 +18,7 @@ import android.annotation.SuppressLint; import android.content.Context; import android.content.SharedPreferences; import android.graphics.Color; +import android.os.Environment; import com.github.dfa.diaspora_android.App; import com.github.dfa.diaspora_android.BuildConfig; @@ -26,70 +27,71 @@ import com.github.dfa.diaspora_android.data.DiasporaAspect; import com.github.dfa.diaspora_android.data.DiasporaPodList.DiasporaPod; import com.github.dfa.diaspora_android.web.ProxyHandler; +import net.gsantner.opoc.preference.SharedPreferencesPropertyBackend; + import org.json.JSONException; import org.json.JSONObject; +import java.io.File; import java.util.List; -import io.github.gsantner.opoc.util.AppSettingsBase; - /** * Settings - * Created by gsantner (https://gsantner.github.io/) 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 AppSettingsBase { - private final SharedPreferences prefPod; +public class AppSettings extends SharedPreferencesPropertyBackend { + private final SharedPreferences _prefPod; private DiasporaPod currentPod0Cached; public static AppSettings get() { return new AppSettings(App.get()); } - private AppSettings(Context context) { + public AppSettings(Context context) { super(context); - prefPod = this.context.getSharedPreferences("pod0", Context.MODE_PRIVATE); + _prefPod = _context.getSharedPreferences("pod0", Context.MODE_PRIVATE); } /** - * Clear all settings in prefPod (Settings related to the configured pod) + * Clear all settings in _prefPod (Settings related to the configured pod) * This uses commit instead of apply, since * SettingsActivity.SettingsFragmentDebugging.showWipeSettingsDialog() * kills the app after the calling this, so we have to block until we are finished. */ @SuppressLint("CommitPrefEdits") public void resetPodSettings() { - super.resetSettings(prefPod); + super.resetSettings(_prefPod); } /** - * Clear all settings in prefApp (related to the App itself) + * Clear all settings in _prefApp (related to the App itself) * This uses commit instead of apply, since * SettingsActivity.SettingsFragmentDebugging.showWipeSettingsDialog() * kills the app after the calling this, so we have to block until we are finished. */ @SuppressLint("CommitPrefEdits") public void resetAppSettings() { - super.resetSettings(prefApp); + super.resetSettings(_prefApp); } //################################# //## Getter & Setter for settings //################################# public String getProfileId() { - return getString(prefPod, R.string.pref_key__podprofile_id, ""); + return getString(R.string.pref_key__podprofile_id, "", _prefPod); } public void setProfileId(String profileId) { - setString(prefPod, R.string.pref_key__podprofile_id, profileId); + setString(R.string.pref_key__podprofile_id, profileId, _prefPod); } public boolean isLoadImages() { - return getBool(prefApp, R.string.pref_key__load_images, true); + return getBool(R.string.pref_key__load_images, true); } public int getMinimumFontSize() { - switch (getString(prefApp, R.string.pref_key__font_size, "")) { + switch (getString(R.string.pref_key__font_size, "")) { case "huge": return 20; case "large": @@ -97,30 +99,30 @@ public class AppSettings extends AppSettingsBase { case "normal": return 8; default: - setString(prefApp, R.string.pref_key__font_size, "normal"); + setString(R.string.pref_key__font_size, "normal"); return 8; } } public String getAvatarUrl() { - return getString(prefPod, R.string.pref_key__podprofile_avatar_url, ""); + return getString(R.string.pref_key__podprofile_avatar_url, "", _prefPod); } public void setAvatarUrl(String avatarUrl) { - setString(prefPod, R.string.pref_key__podprofile_avatar_url, avatarUrl); + setString(R.string.pref_key__podprofile_avatar_url, avatarUrl, _prefPod); } public String getName() { - return getString(prefPod, R.string.pref_key__podprofile_name, ""); + return getString(R.string.pref_key__podprofile_name, "", _prefPod); } public void setName(String name) { - setString(prefPod, R.string.pref_key__podprofile_name, name); + setString(R.string.pref_key__podprofile_name, name, _prefPod); } public DiasporaPod getPod() { if (currentPod0Cached == null) { - String pref = getString(prefPod, R.string.pref_key__current_pod_0, ""); + String pref = getString(R.string.pref_key__current_pod_0, "", _prefPod); try { currentPod0Cached = new DiasporaPod().fromJson(new JSONObject(pref)); @@ -133,23 +135,27 @@ public class AppSettings extends AppSettingsBase { public void setPod(DiasporaPod pod) { try { - setString(prefPod, R.string.pref_key__current_pod_0, - pod == null ? null : pod.toJson().toString()); + setString(R.string.pref_key__current_pod_0, + pod == null ? null : pod.toJson().toString(), _prefPod); currentPod0Cached = pod; } catch (JSONException ignored) { } } public boolean hasPod() { - return !getString(prefPod, R.string.pref_key__current_pod_0, "").equals(""); + return !getString(R.string.pref_key__current_pod_0, "", _prefPod).equals(""); } public void setPodAspects(DiasporaAspect[] aspects) { - setStringArray(prefPod, R.string.pref_key__podprofile_aspects, aspects); + String[] strs = new String[aspects.length]; + for (int i = 0; i < strs.length; i++) { + strs[i] = aspects[i].toShareAbleText(); + } + setStringArray(R.string.pref_key__podprofile_aspects, strs, _prefPod); } public DiasporaAspect[] getAspects() { - String[] s = getStringArray(prefPod, R.string.pref_key__podprofile_aspects); + String[] s = getStringArray(R.string.pref_key__podprofile_aspects, _prefPod); DiasporaAspect[] aspects = new DiasporaAspect[s.length]; for (int i = 0; i < aspects.length; i++) { aspects[i] = new DiasporaAspect(s[i]); @@ -158,53 +164,53 @@ public class AppSettings extends AppSettingsBase { } public String[] getFollowedTags() { - return getStringArray(prefPod, R.string.pref_key__podprofile_followed_tags); + return getStringArray(R.string.pref_key__podprofile_followed_tags, _prefPod); } public void setFollowedTags(String[] values) { - setStringArray(prefPod, R.string.pref_key__podprofile_followed_tags, values); + setStringArray(R.string.pref_key__podprofile_followed_tags, values, _prefPod); } public String[] getFollowedTagsFavs() { - return getStringArray(prefPod, R.string.pref_key__podprofile_followed_tags_favs); + return getStringArray(R.string.pref_key__podprofile_followed_tags_favs, _prefPod); } public void setFollowedTagsFavs(List values) { - setStringList(prefPod, R.string.pref_key__podprofile_followed_tags_favs, values); + setStringList(R.string.pref_key__podprofile_followed_tags_favs, values, _prefPod); } public String[] getAspectFavs() { - return getStringArray(prefPod, R.string.pref_key__podprofile_aspects_favs); + return getStringArray(R.string.pref_key__podprofile_aspects_favs, _prefPod); } public void setAspectFavs(List values) { - setStringList(prefPod, R.string.pref_key__podprofile_aspects_favs, values); + setStringList(R.string.pref_key__podprofile_aspects_favs, values, _prefPod); } public int getUnreadMessageCount() { - return getInt(prefPod, R.string.pref_key__podprofile_unread_message_count, 0); + return getInt(R.string.pref_key__podprofile_unread_message_count, 0, _prefPod); } public void setUnreadMessageCount(int unreadMessageCount) { - setInt(prefPod, R.string.pref_key__podprofile_unread_message_count, unreadMessageCount); + setInt(R.string.pref_key__podprofile_unread_message_count, unreadMessageCount, _prefPod); } public int getNotificationCount() { - return getInt(prefPod, R.string.pref_key__podprofile_notification_count, 0); + return getInt(R.string.pref_key__podprofile_notification_count, 0, _prefPod); } public void setNotificationCount(int notificationCount) { - setInt(prefPod, R.string.pref_key__podprofile_notification_count, notificationCount); + setInt(R.string.pref_key__podprofile_notification_count, notificationCount, _prefPod); } public boolean isAppendSharedViaApp() { - return getBool(prefApp, R.string.pref_key__append_shared_via_app, true); + return getBool(R.string.pref_key__append_shared_via_app, true); } @SuppressLint("CommitPrefEdits") public void setProxyHttpEnabled(boolean enabled) { //commit instead of apply because the app is likely to be killed before apply is called. - prefApp.edit().putBoolean(context.getString(R.string.pref_key__http_proxy_enabled), enabled).commit(); + _prefApp.edit().putBoolean(rstr(R.string.pref_key__http_proxy_enabled), enabled).commit(); } /** @@ -214,7 +220,7 @@ public class AppSettings extends AppSettingsBase { */ public boolean isProxyHttpEnabled() { try { - return getBool(prefApp, R.string.pref_key__http_proxy_enabled, false); + return getBool(R.string.pref_key__http_proxy_enabled, false); } catch (ClassCastException e) { setProxyHttpEnabled(false); return false; @@ -222,7 +228,7 @@ public class AppSettings extends AppSettingsBase { } public boolean wasProxyEnabled() { - return getBool(prefApp, R.string.pref_key__proxy_was_enabled, false); + return getBool(R.string.pref_key__proxy_was_enabled, false); } /** @@ -233,7 +239,7 @@ public class AppSettings extends AppSettingsBase { */ @SuppressLint("CommitPrefEdits") public void setProxyWasEnabled(boolean b) { - prefApp.edit().putBoolean(context.getString(R.string.pref_key__proxy_was_enabled), b).commit(); + _prefApp.edit().putBoolean(rstr(R.string.pref_key__proxy_was_enabled), b).commit(); } /** @@ -242,11 +248,11 @@ public class AppSettings extends AppSettingsBase { * @return proxy host */ public String getProxyHttpHost() { - return getString(prefApp, R.string.pref_key__http_proxy_host, ""); + return getString(R.string.pref_key__http_proxy_host, ""); } public void setProxyHttpHost(String value) { - setString(prefApp, R.string.pref_key__http_proxy_host, value); + setString(R.string.pref_key__http_proxy_host, value); } /** @@ -256,17 +262,17 @@ public class AppSettings extends AppSettingsBase { */ public int getProxyHttpPort() { try { - String str = getString(prefApp, R.string.pref_key__http_proxy_port, "0"); + String str = getString(R.string.pref_key__http_proxy_port, "0"); return Integer.parseInt(str); } catch (ClassCastException e) { - int port = getInt(prefApp, R.string.pref_key__http_proxy_port, 0); + int port = getInt(R.string.pref_key__http_proxy_port, 0); setProxyHttpPort(port); return port; } } public void setProxyHttpPort(int value) { - setString(prefApp, R.string.pref_key__http_proxy_port, Integer.toString(value)); + setString(R.string.pref_key__http_proxy_port, Integer.toString(value)); } public ProxyHandler.ProxySettings getProxySettings() { @@ -274,122 +280,141 @@ public class AppSettings extends AppSettingsBase { } public boolean isIntellihideToolbars() { - return getBool(prefApp, R.string.pref_key__intellihide_toolbars, true); + return getBool(R.string.pref_key__intellihide_toolbars, false); } public boolean isChromeCustomTabsEnabled() { - return getBool(prefApp, R.string.pref_key__chrome_custom_tabs_enabled, true); + return getBool(R.string.pref_key__chrome_custom_tabs_enabled, true); } public boolean isLoggingEnabled() { - return getBool(prefApp, R.string.pref_key__logging_enabled, false); + return getBool(R.string.pref_key__logging_enabled, false); } public boolean isLoggingSpamEnabled() { - return getBool(prefApp, R.string.pref_key__logging_spam_enabled, false); + return getBool(R.string.pref_key__logging_spam_enabled, false); } public boolean isVisibleInNavExit() { - return getBool(prefApp, R.string.pref_key__visibility_nav__exit, false); + return getBool(R.string.pref_key__visibility_nav__exit, true); } public boolean isVisibleInNavHelp_license() { - return getBool(prefApp, R.string.pref_key__visibility_nav__help_license, true); + return getBool(R.string.pref_key__visibility_nav__help_license, true); } public boolean isVisibleInNavPublic_activities() { - return getBool(prefApp, R.string.pref_key__visibility_nav__public_activities, false); + return getBool(R.string.pref_key__visibility_nav__public_activities, false); } public boolean isVisibleInNavMentions() { - return getBool(prefApp, R.string.pref_key__visibility_nav__mentions, false); + return getBool(R.string.pref_key__visibility_nav__mentions, false); } public boolean isVisibleInNavCommented() { - return getBool(prefApp, R.string.pref_key__visibility_nav__commented, true); + return getBool(R.string.pref_key__visibility_nav__commented, true); } public boolean isVisibleInNavLiked() { - return getBool(prefApp, R.string.pref_key__visibility_nav__liked, true); + return getBool(R.string.pref_key__visibility_nav__liked, true); } public boolean isVisibleInNavActivities() { - return getBool(prefApp, R.string.pref_key__visibility_nav__activities, true); + return getBool(R.string.pref_key__visibility_nav__activities, true); } public boolean isVisibleInNavAspects() { - return getBool(prefApp, R.string.pref_key__visibility_nav__aspects, true); + return getBool(R.string.pref_key__visibility_nav__aspects, true); } public boolean isVisibleInNavFollowed_tags() { - return getBool(prefApp, R.string.pref_key__visibility_nav__followed_tags, true); + return getBool(R.string.pref_key__visibility_nav__followed_tags, true); } public boolean isVisibleInNavProfile() { - return getBool(prefApp, R.string.pref_key__visibility_nav__profile, true); + return getBool(R.string.pref_key__visibility_nav__profile, true); } public boolean isVisibleInNavContacts() { - return getBool(prefApp, R.string.pref_key__visibility_nav__contacts, false); + return getBool(R.string.pref_key__visibility_nav__contacts, false); } public boolean isVisibleInNavStatistics() { - return getBool(prefApp, R.string.pref_key__visibility_nav__statistics, false); + return getBool(R.string.pref_key__visibility_nav__statistics, false); } public boolean isVisibleInNavReports() { - return getBool(prefApp, R.string.pref_key__visibility_nav__reports, false); + return getBool(R.string.pref_key__visibility_nav__reports, false); } - public boolean isVisibleToggleMobileDesktop() { - return getBool(prefApp, R.string.pref_key__visibility_nav__toggle_mobile_desktop, false); + public boolean isVisibleInNavGsantnerAccount() { + return getBool(R.string.pref_key__visibility_nav__gsantner_account, false); + } + + public boolean isVisibleInNavToggleMobileDesktop() { + return getBool(R.string.pref_key__visibility_nav__toggle_mobile_desktop, false); } public boolean isTopbarStreamShortcutEnabled() { - return getBool(prefApp, R.string.pref_key__topbar_stream_shortcut, false); + 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(prefApp, R.string.pref_key__screen_rotation, R.string.rotation_val_system); + return getString(R.string.pref_key__screen_rotation, R.string.rotation_val_system); } public boolean isAppFirstStart() { - boolean value = getBool(prefApp, R.string.pref_key__app_first_start, true); - setBool(prefApp, R.string.pref_key__app_first_start, false); + boolean value = getBool(R.string.pref_key__app_first_start, true); + setBool(R.string.pref_key__app_first_start, false); return value; } - public boolean isAppCurrentVersionFirstStart() { - int value = getInt(prefApp, R.string.pref_key__app_first_start_current_version, -1); - setInt(prefApp, R.string.pref_key__app_first_start_current_version, BuildConfig.VERSION_CODE); + public boolean isAppCurrentVersionFirstStart(boolean doSet) { + int value = getInt(R.string.pref_key__app_first_start_current_version, -1); + if (doSet) { + setInt(R.string.pref_key__app_first_start_current_version, BuildConfig.VERSION_CODE); + } return value != BuildConfig.VERSION_CODE && !BuildConfig.IS_TEST_BUILD; } + public File getAppSaveDirectory() { + return new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) + "/dandelion"); + + } + public long getLastVisitedPositionInStream() { - return getLong(prefPod, R.string.pref_key__podprofile_last_stream_position, -1); + return getLong(R.string.pref_key__podprofile_last_stream_position, -1, _prefPod); } public void setLastVisitedPositionInStream(long timestamp) { - setLong(prefPod, R.string.pref_key__podprofile_last_stream_position, timestamp); + setLong(R.string.pref_key__podprofile_last_stream_position, timestamp, _prefPod); } public void setLanguage(String value) { - setString(prefApp, R.string.pref_key__language, value); + setString(R.string.pref_key__language, value); } public String getLanguage() { - return getString(prefApp, R.string.pref_key__language, ""); + return getString(R.string.pref_key__language, ""); } public void setPrimaryColorSettings(int base, int shade) { - setInt(prefApp, R.string.pref_key__primary_color_base, base); - setInt(prefApp, R.string.pref_key__primary_color_shade, shade); + setInt(R.string.pref_key__primary_color_base, base); + setInt(R.string.pref_key__primary_color_shade, shade); } public int[] getPrimaryColorSettings() { return new int[]{ - getInt(prefApp, R.string.pref_key__primary_color_base, rcolor(R.color.md_blue_650)), - getInt(prefApp, R.string.pref_key__primary_color_shade, rcolor(R.color.primary)) + getInt(R.string.pref_key__primary_color_base, rcolor(R.color.md_blue_650)), + getInt(R.string.pref_key__primary_color_shade, rcolor(R.color.primary)) }; } @@ -398,36 +423,58 @@ public class AppSettings extends AppSettingsBase { if (isAmoledColorMode()) { return Color.BLACK; } else { - return getInt(prefApp, R.string.pref_key__primary_color_shade, rcolor( + return getInt(R.string.pref_key__primary_color_shade, rcolor( BuildConfig.IS_TEST_BUILD ? R.color.md_brown_800 : R.color.primary)); } } public void setAccentColorSettings(int base, int shade) { - setInt(prefApp, R.string.pref_key__accent_color_base, base); - setInt(prefApp, R.string.pref_key__accent_color_shade, shade); + setInt(R.string.pref_key__accent_color_base, base); + setInt(R.string.pref_key__accent_color_shade, shade); } public int[] getAccentColorSettings() { return new int[]{ - getInt(prefApp, R.string.pref_key__accent_color_base, rcolor(R.color.md_green_400)), - getInt(prefApp, R.string.pref_key__accent_color_shade, rcolor(R.color.accent)) + getInt(R.string.pref_key__accent_color_base, rcolor(R.color.md_green_400)), + getInt(R.string.pref_key__accent_color_shade, rcolor(R.color.accent)) }; } public int getAccentColor() { - return getInt(prefApp, R.string.pref_key__accent_color_shade, rcolor(R.color.accent)); + return getInt(R.string.pref_key__accent_color_shade, rcolor(R.color.accent)); } public boolean isExtendedNotificationsActivated() { - return getBool(prefApp, R.string.pref_key__extended_notifications, false); + return getBool(R.string.pref_key__extended_notifications, false); } public boolean isAmoledColorMode() { - return getBool(prefApp, R.string.pref_key__primary_color__amoled_mode, false); + 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(prefApp, R.string.pref_key__adblock_enable, true); + return getBool(R.string.pref_key__adblock_enable, true); } -} \ No newline at end of file + + public boolean isEditorStatusBarHidden() { + return getBool(R.string.pref_key__is_overview_statusbar_hidden, false); + } + + public void setRecreateMainActivity(boolean value) { + setBool(R.string.pref_key__recreate_main_activity, value); + } + + public boolean isRecreateMainActivity() { + boolean value = getBool(R.string.pref_key__recreate_main_activity, false); + setRecreateMainActivity(false); + return value; + } + + public boolean isShowTitleInMainView() { + return getBool(R.string.pref_key__show_title, false); + } +} diff --git a/app/src/main/java/com/github/dfa/diaspora_android/util/ContextUtils.java b/app/src/main/java/com/github/dfa/diaspora_android/util/ContextUtils.java new file mode 100644 index 00000000..d2132cc4 --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/util/ContextUtils.java @@ -0,0 +1,58 @@ +package com.github.dfa.diaspora_android.util; + +import android.content.Context; +import android.os.Bundle; +import android.os.Environment; + +import com.github.dfa.diaspora_android.App; + +import java.io.File; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Date; +import java.util.Locale; + +@SuppressWarnings({"WeakerAccess", "unused", "SameParameterValue"}) +public class ContextUtils extends net.gsantner.opoc.util.ContextUtils { + protected ContextUtils(Context context) { + super(context); + } + + + public static ContextUtils get() { + return new ContextUtils(App.get()); + } + + public File createImageFile() throws IOException { + // Create an image file name + String timeStamp = new SimpleDateFormat("dd-MM-yy_HH-mm", Locale.getDefault()).format(new Date()); + String imageFileName = "JPEG_" + timeStamp + "_"; + AppLog.d(ContextUtils.class, Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getAbsolutePath()); + File storageDir = Environment.getExternalStoragePublicDirectory( + Environment.DIRECTORY_PICTURES); + return new File( + imageFileName + /* prefix */ + ".jpg", /* suffix */ + storageDir.getAbsolutePath() /* directory */ + ); + } + + public void logBundle(Bundle savedInstanceState, String k) { + if (savedInstanceState != null) { + for (String key : savedInstanceState.keySet()) { + AppLog.d("Bundle", key + " is a key in the bundle " + k); + Object bun = savedInstanceState.get(key); + if (bun != null) { + if (bun instanceof Bundle) { + logBundle((Bundle) bun, k + "." + key); + } else if (bun instanceof byte[]) { + AppLog.d("Bundle", "Key: " + k + "." + key + ": " + Arrays.toString((byte[]) bun)); + } else { + AppLog.d("Bundle", "Key: " + k + "." + key + ": " + bun.toString()); + } + } + } + } + } +} 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 eb7876c9..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 @@ -27,7 +27,7 @@ import com.github.dfa.diaspora_android.data.DiasporaPodList.DiasporaPod; * Helper class that provides easy access to specific urls related to diaspora * Created by vanitasvitae on 10.08.16. */ -@SuppressWarnings("unused") +@SuppressWarnings({"unused", "SpellCheckingInspection", "SameParameterValue", "WeakerAccess"}) public class DiasporaUrlHelper { private final AppSettings settings; @@ -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; @@ -151,7 +152,7 @@ public class DiasporaUrlHelper { * @param profileId Id of the profile to be shown * @return https://(pod-domain.tld)/people/(profileId) */ - public String getProfileUrl(long profileId) { + public String getProfileUrl(String profileId) { return getPodUrl() + SUBURL_PEOPLE + profileId; } @@ -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 b0369aad..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 @@ -18,21 +18,10 @@ */ package com.github.dfa.diaspora_android.web; -import android.Manifest; import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; import android.content.MutableContextWrapper; -import android.content.pm.PackageManager; -import android.graphics.Bitmap; -import android.net.Uri; import android.os.Bundle; -import android.os.Environment; -import android.support.design.widget.Snackbar; -import android.support.v7.app.AlertDialog; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; +import android.support.v4.widget.SwipeRefreshLayout; import android.view.View; import android.view.ViewGroup; import android.webkit.WebSettings; @@ -41,21 +30,11 @@ import android.widget.ProgressBar; import com.github.dfa.diaspora_android.App; import com.github.dfa.diaspora_android.R; -import com.github.dfa.diaspora_android.activity.MainActivity; import com.github.dfa.diaspora_android.ui.theme.ThemeHelper; import com.github.dfa.diaspora_android.ui.theme.ThemedFragment; import com.github.dfa.diaspora_android.util.AppLog; import com.github.dfa.diaspora_android.util.AppSettings; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; - /** * Fragment with a webView and a ProgressBar. * This Fragment retains its instance. @@ -65,7 +44,6 @@ import java.util.Locale; public class BrowserFragment extends ThemedFragment { public static final String TAG = "com.github.dfa.diaspora_android.BrowserFragment"; - protected View rootLayout; protected ContextMenuWebView webView; protected ProgressBar progressBar; protected AppSettings appSettings; @@ -73,20 +51,14 @@ public class BrowserFragment extends ThemedFragment { protected WebSettings webSettings; protected String pendingUrl; + protected SwipeRefreshLayout swipe;//pull to refresh @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - AppLog.d(this, "onCreateView()"); - if (rootLayout == null) { - LayoutInflater inflater1 = inflater.cloneInContext(new MutableContextWrapper(getContext())); - rootLayout = inflater1.inflate(R.layout.browser__fragment, container, false); - } else { - MutableContextWrapper context = (MutableContextWrapper) rootLayout.getContext(); - context.setBaseContext(getContext()); - } - return rootLayout; + protected int getLayoutResId() { + return R.layout.browser__fragment; } + @Override public void onViewCreated(View view, Bundle savedInstanceState) { AppLog.d(this, "onViewCreated()"); @@ -97,7 +69,7 @@ public class BrowserFragment extends ThemedFragment { } if (this.webView == null) { - this.webView = (ContextMenuWebView) view.findViewById(R.id.webView); + this.webView = view.findViewById(R.id.webView); getActivity().runOnUiThread(new Runnable() { @Override public void run() { @@ -109,7 +81,7 @@ public class BrowserFragment extends ThemedFragment { } if (this.progressBar == null) { - this.progressBar = (ProgressBar) view.findViewById(R.id.progressBar); + this.progressBar = view.findViewById(R.id.progressBar); } if (pendingUrl != null) { @@ -120,14 +92,26 @@ 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 public void onDestroyView() { super.onDestroyView(); - if (getRetainInstance() && rootLayout.getParent() instanceof ViewGroup) { - ((ViewGroup) rootLayout.getParent()).removeView(rootLayout); + if (getRetainInstance() && getView() != null && getView().getParent() instanceof ViewGroup) { + ((ViewGroup) getView().getParent()).removeView(getView()); } } @@ -165,98 +149,11 @@ public class BrowserFragment extends ThemedFragment { } } - @SuppressWarnings("ResultOfMethodCallIgnored") - protected boolean makeScreenshotOfWebView(boolean hasToShareScreenshot) { - AppLog.i(this, "StreamFragment.makeScreenshotOfWebView()"); - if (android.os.Build.VERSION.SDK_INT >= 23) { - int hasWRITE_EXTERNAL_STORAGE = getActivity().checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE); - if (hasWRITE_EXTERNAL_STORAGE != PackageManager.PERMISSION_GRANTED) { - if (!shouldShowRequestPermissionRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { - new AlertDialog.Builder(getContext()) - .setMessage(R.string.permissions_screenshot) - .setNegativeButton(android.R.string.no, null) - .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - if (android.os.Build.VERSION.SDK_INT >= 23) - requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, - MainActivity.REQUEST_CODE_ASK_PERMISSIONS); - } - }) - .show(); - return false; - } - requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, - MainActivity.REQUEST_CODE_ASK_PERMISSIONS); - return false; - } - } - - Date dateNow = new Date(); - DateFormat dateFormat = new SimpleDateFormat("yy_MM_dd--HH_mm_ss", Locale.getDefault()); - File fileSaveDirectory = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) + "/Diaspora"); - - String fileSaveName = hasToShareScreenshot ? ".DfA_share.jpg" : String.format("DfA_%s.jpg", dateFormat.format(dateNow)); - if (!fileSaveDirectory.exists()) { - if (!fileSaveDirectory.mkdirs()) { - AppLog.w(this, "Could not mkdir " + fileSaveDirectory.getAbsolutePath()); - } - } - - if (!hasToShareScreenshot) { - Snackbar.make(webView, getString(R.string.share__toast_screenshot) + " " + fileSaveName, Snackbar.LENGTH_LONG).show(); - } - - Bitmap bitmap; - webView.setDrawingCacheEnabled(true); - bitmap = Bitmap.createBitmap(webView.getDrawingCache()); - webView.setDrawingCacheEnabled(false); - - OutputStream bitmapWriter = null; - try { - bitmapWriter = new FileOutputStream(new File(fileSaveDirectory, fileSaveName)); - bitmap.compress(Bitmap.CompressFormat.JPEG, 85, bitmapWriter); - bitmapWriter.flush(); - bitmap.recycle(); - } catch (Exception e) { - return false; - } finally { - if (bitmapWriter != null) { - try { - bitmapWriter.close(); - } catch (IOException _ignSaveored) {/* Nothing */} - } - } - - // Only show share intent when Action Share Screenshot was selected - if (hasToShareScreenshot) { - Intent sharingIntent = new Intent(Intent.ACTION_SEND); - sharingIntent.setType("image/jpeg"); - sharingIntent.putExtra(Intent.EXTRA_SUBJECT, webView.getTitle()); - sharingIntent.putExtra(Intent.EXTRA_TEXT, webView.getUrl()); - Uri bmpUri = Uri.fromFile(new File(fileSaveDirectory, fileSaveName)); - sharingIntent.putExtra(Intent.EXTRA_STREAM, bmpUri); - startActivity(Intent.createChooser(sharingIntent, getString(R.string.action_share_dotdotdot))); - } else { - // Broadcast that this file is indexable - File file = new File(fileSaveDirectory, fileSaveName); - Uri uri = Uri.fromFile(file); - Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri); - getActivity().sendBroadcast(intent); - } - return true; - } - @Override public String getFragmentTag() { return TAG; } - @Override - public void onCreateBottomOptionsMenu(Menu menu, MenuInflater inflater) { - /* Nothing to do here */ - } - @Override public void onAttach(Context context) { super.onAttach(context); @@ -309,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 cb5ed280..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 @@ -18,29 +18,27 @@ */ package com.github.dfa.diaspora_android.web; -import android.Manifest; import android.app.Activity; -import android.app.AlertDialog; -import android.app.DownloadManager; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; -import android.content.pm.PackageManager; -import android.graphics.Bitmap; import android.net.Uri; -import android.os.Environment; import android.util.AttributeSet; import android.view.ContextMenu; import android.view.MenuItem; import android.widget.Toast; +import com.github.dfa.diaspora_android.BuildConfig; import com.github.dfa.diaspora_android.R; -import com.github.dfa.diaspora_android.activity.MainActivity; -import com.github.dfa.diaspora_android.service.ImageDownloadTask; +import com.github.dfa.diaspora_android.util.AppSettings; + +import net.gsantner.opoc.util.DownloadTask; +import net.gsantner.opoc.util.PermissionChecker; +import net.gsantner.opoc.util.ShareUtil; import java.io.File; +import java.util.Date; /** * Subclass of WebView which adds a context menu for long clicks on images or links to share, save @@ -80,106 +78,46 @@ public class ContextMenuWebView extends NestedWebView { public boolean onMenuItemClick(MenuItem item) { HitTestResult result = getHitTestResult(); String url = result.getExtra(); + 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: { - boolean writeToStoragePermitted = true; - if (android.os.Build.VERSION.SDK_INT >= 23) { - int hasWRITE_EXTERNAL_STORAGE = parentActivity.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE); - if (hasWRITE_EXTERNAL_STORAGE != PackageManager.PERMISSION_GRANTED) { - writeToStoragePermitted = false; - if (!parentActivity.shouldShowRequestPermissionRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { - new AlertDialog.Builder(parentActivity) - .setMessage(R.string.permissions_image) - .setPositiveButton(context.getText(android.R.string.yes), new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - if (android.os.Build.VERSION.SDK_INT >= 23) - parentActivity.requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, - MainActivity.REQUEST_CODE__ACCESS_EXTERNAL_STORAGE); - } - }) - .setNegativeButton(context.getText(android.R.string.no), null) - .show(); - } - parentActivity.requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, - MainActivity.REQUEST_CODE__ACCESS_EXTERNAL_STORAGE); - } - } - if (writeToStoragePermitted) { - //Make sure, Diaspora Folder exists - File destinationFolder = new File(Environment.getExternalStorageDirectory() + "/Pictures/Diaspora"); - if (!destinationFolder.exists()) { - destinationFolder.mkdirs(); - } - - if (url != null) { - Uri source = Uri.parse(url); + 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(".")); + /*Uri source = Uri.parse(url); DownloadManager.Request request = new DownloadManager.Request(source); - File destinationFile = new File(Environment.getExternalStorageDirectory() + "/Pictures/Diaspora/" + System.currentTimeMillis() + ".png"); - - request.setDestinationUri(Uri.fromFile(destinationFile)); - ((DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE)).enqueue(request); - - Toast.makeText(context, context.getText(R.string.share__toast_saved_image_to_location) + " " + - destinationFile.getAbsolutePath(), Toast.LENGTH_LONG).show(); + request.setDestinationUri(Uri.fromFile(new File(fileSaveDirectory, filename))); + ((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.saving_image_to) + " " + dlfile.getName(), Toast.LENGTH_LONG).show(); + } + }).execute(url); } } - } - break; - - case ID_SHARE_IMAGE: - if (url != null) { - boolean writeToStoragePermitted = true; - if (android.os.Build.VERSION.SDK_INT >= 23) { - int hasWRITE_EXTERNAL_STORAGE = parentActivity.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE); - if (hasWRITE_EXTERNAL_STORAGE != PackageManager.PERMISSION_GRANTED) { - writeToStoragePermitted = false; - if (!parentActivity.shouldShowRequestPermissionRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { - new AlertDialog.Builder(parentActivity) - .setMessage(R.string.permissions_image) - .setPositiveButton(context.getText(android.R.string.yes), new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - if (android.os.Build.VERSION.SDK_INT >= 23) - parentActivity.requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, - MainActivity.REQUEST_CODE__ACCESS_EXTERNAL_STORAGE); - } - }) - .setNegativeButton(context.getText(android.R.string.no), null) - .show(); - } else { - parentActivity.requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, - MainActivity.REQUEST_CODE__ACCESS_EXTERNAL_STORAGE); - } - } - } - if (writeToStoragePermitted) { - //Make sure, Diaspora Folder exists - File destinationFolder = new File(Environment.getExternalStorageDirectory() + "/Pictures/Diaspora"); - if (!destinationFolder.exists()) { - destinationFolder.mkdirs(); - } - - final Uri local = Uri.parse(Environment.getExternalStorageDirectory() + "/Pictures/Diaspora/" + System.currentTimeMillis() + ".png"); - new ImageDownloadTask(null, local.getPath()) { - @Override - protected void onPostExecute(Bitmap result) { - Uri myUri = Uri.fromFile(new File(local.getPath())); - Intent sharingIntent = new Intent(); - sharingIntent.setAction(Intent.ACTION_SEND); - sharingIntent.putExtra(Intent.EXTRA_STREAM, myUri); - sharingIntent.setType("image/png"); - sharingIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - context.startActivity(Intent.createChooser(sharingIntent, getResources().getString(R.string.action_share_dotdotdot))); - } - }.execute(url); - } - } else { - Toast.makeText(context, "Cannot share image: url is null", Toast.LENGTH_SHORT).show(); - } - break; + } + + case ID_SHARE_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.saving_image_to) + " " + dlfile.getName(), Toast.LENGTH_LONG).show(); + shu.shareStream(dlfile, "image/" + dlfile.getAbsolutePath().lastIndexOf(".") + 1); + } + }).execute(url); + } + } + break; + } case ID_IMAGE_EXTERNAL_BROWSER: if (url != null) { @@ -194,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; @@ -206,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; } @@ -219,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.context_menu_copy_link)).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 6358b836..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; @@ -32,12 +33,13 @@ import com.github.dfa.diaspora_android.activity.MainActivity; import com.github.dfa.diaspora_android.data.DiasporaPodList; import com.github.dfa.diaspora_android.util.AppSettings; -import io.github.gsantner.opoc.util.AdBlock; +import net.gsantner.opoc.util.AdBlock; 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/DiasporaStreamWebChromeClient.java b/app/src/main/java/com/github/dfa/diaspora_android/web/DiasporaStreamWebChromeClient.java index 60a8f8cc..1dc21836 100644 --- a/app/src/main/java/com/github/dfa/diaspora_android/web/DiasporaStreamWebChromeClient.java +++ b/app/src/main/java/com/github/dfa/diaspora_android/web/DiasporaStreamWebChromeClient.java @@ -18,7 +18,6 @@ */ package com.github.dfa.diaspora_android.web; -import android.content.DialogInterface; import android.webkit.JsResult; import android.webkit.WebView; import android.widget.ProgressBar; @@ -65,20 +64,13 @@ public class DiasporaStreamWebChromeClient extends FileUploadWebChromeClient { ThemedAlertDialogBuilder builder = new ThemedAlertDialogBuilder(view.getContext(), AppSettings.get()); builder.setTitle(view.getContext().getString(R.string.confirmation)) .setMessage(message) - .setPositiveButton(android.R.string.ok, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - result.confirm(); - } - }) - .setNegativeButton(android.R.string.cancel, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - result.cancel(); - } - }) - .create() - .show(); + .setPositiveButton(android.R.string.ok, (dialog, which) -> result.confirm()) + .setNegativeButton(android.R.string.cancel, (dialog, which) -> result.cancel()) + .setOnCancelListener(dialog -> { + result.cancel(); + dialog.dismiss(); + }) + .create().show(); return true; } 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 7ec5c99d..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. - * https://gsantner.github.io */ public class WebHelper { diff --git a/app/src/main/java/com/github/dfa/diaspora_android/web/custom_tab/CustomTabsHelper.java b/app/src/main/java/com/github/dfa/diaspora_android/web/custom_tab/CustomTabsHelper.java index 50481fd3..aaa89f06 100644 --- a/app/src/main/java/com/github/dfa/diaspora_android/web/custom_tab/CustomTabsHelper.java +++ b/app/src/main/java/com/github/dfa/diaspora_android/web/custom_tab/CustomTabsHelper.java @@ -41,6 +41,9 @@ public class CustomTabsHelper { static final String BETA_PACKAGE = "com.chrome.beta"; static final String DEV_PACKAGE = "com.chrome.dev"; static final String LOCAL_PACKAGE = "com.google.android.apps.chrome"; + static final String CHROMIUM = "org.chromium.chrome"; + static final String FENNEC = "org.mozilla.fennec_fdroid"; + static final String KLAR = "org.mozilla.klar"; private static final String EXTRA_CUSTOM_TABS_KEEP_ALIVE = "android.support.customtabs.extra.KEEP_ALIVE"; @@ -101,6 +104,12 @@ public class CustomTabsHelper { sPackageNameToUse = DEV_PACKAGE; } else if (packagesSupportingCustomTabs.contains(LOCAL_PACKAGE)) { sPackageNameToUse = LOCAL_PACKAGE; + } else if (packagesSupportingCustomTabs.contains(CHROMIUM)) { + sPackageNameToUse = CHROMIUM; + } else if (packagesSupportingCustomTabs.contains(FENNEC)) { + sPackageNameToUse = FENNEC; + } else if (packagesSupportingCustomTabs.contains(KLAR)) { + sPackageNameToUse = KLAR; } return sPackageNameToUse; } @@ -137,6 +146,6 @@ public class CustomTabsHelper { * @return All possible chrome package names that provide custom tabs feature. */ public static String[] getPackages() { - return new String[]{"", STABLE_PACKAGE, BETA_PACKAGE, DEV_PACKAGE, LOCAL_PACKAGE}; + return new String[]{"", STABLE_PACKAGE, BETA_PACKAGE, DEV_PACKAGE, LOCAL_PACKAGE, CHROMIUM, FENNEC, KLAR}; } } diff --git a/app/src/main/java/io/github/gsantner/opoc/util/AppSettingsBase.java b/app/src/main/java/io/github/gsantner/opoc/util/AppSettingsBase.java deleted file mode 100644 index 76435609..00000000 --- a/app/src/main/java/io/github/gsantner/opoc/util/AppSettingsBase.java +++ /dev/null @@ -1,325 +0,0 @@ -/* - * ---------------------------------------------------------------------------- * - * Gregor Santner wrote this file. You can do whatever - * you want with this stuff. If we meet some day, and you think this stuff is - * worth it, you can buy me a coke in return. Provided as is without any kind - * of warranty. No attribution required. - Gregor Santner - * - * License: Creative Commons Zero (CC0 1.0) - * http://creativecommons.org/publicdomain/zero/1.0/ - * ---------------------------------------------------------------------------- - */ - -/* - * Get updates: - * https://github.com/gsantner/onePieceOfCode/blob/master/java/AppSettingsBase.java - * This is a wrapper for settings based on SharedPreferences - * with keys in resources. Extend from this class and add - * getters/setters for the app's settings. - * Example: - public boolean isAppFirstStart() { - return getBool(prefApp, R.string.pref_key__app_first_start, true); - } - - public void setAppFirstStart(boolean value) { - setBool(prefApp, R.string.pref_key__app_first_start, value); - } - - public boolean isAppFirstStartCurrentVersion() { - int value = getInt(prefApp, R.string.pref_key__app_first_start_current_version, -1); - setInt(prefApp, R.string.pref_key__app_first_start_current_version, BuildConfig.VERSION_CODE); - return value != BuildConfig.VERSION_CODE && !BuildConfig.IS_TEST_BUILD; - } - - * Maybe add a singleton for this: - * Whereas App.get() is returning ApplicationContext - private AppSettings(Context context) { - super(context); - } - - public static AppSettings get() { - return new AppSettings(App.get()); - } - */ - -package io.github.gsantner.opoc.util; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.content.SharedPreferences; -import android.support.annotation.ColorRes; -import android.support.annotation.NonNull; -import android.support.annotation.StringRes; -import android.support.v4.content.ContextCompat; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - - -/** - * Wrapper for settings based on SharedPreferences - * with keys in resources - */ -@SuppressWarnings({"WeakerAccess", "unused"}) -public class AppSettingsBase { - protected static final String ARRAY_SEPARATOR = "%%%"; - protected static final String ARRAY_SEPARATOR_SUBSTITUTE = "§§§"; - public static final String SHARED_PREF_APP = "app"; - - //##################### - //## Members - //##################### - protected final SharedPreferences prefApp; - protected final Context context; - - //##################### - //## Methods - //##################### - public AppSettingsBase(Context context) { - this(context, SHARED_PREF_APP); - } - - public AppSettingsBase(Context context, String prefAppName) { - this.context = context.getApplicationContext(); - prefApp = this.context.getSharedPreferences(prefAppName, Context.MODE_PRIVATE); - } - - public Context getContext() { - return context; - } - - public boolean isKeyEqual(String key, int stringKeyResourceId) { - return key.equals(rstr(stringKeyResourceId)); - } - - public void resetSettings() { - resetSettings(prefApp); - } - - @SuppressLint("ApplySharedPref") - public void resetSettings(SharedPreferences pref) { - pref.edit().clear().commit(); - } - - public boolean isPrefSet(@StringRes int stringKeyResourceId) { - return isPrefSet(prefApp, stringKeyResourceId); - } - - public boolean isPrefSet(SharedPreferences pref, @StringRes int stringKeyResourceId) { - return pref.contains(rstr(stringKeyResourceId)); - } - - public void registerPreferenceChangedListener(SharedPreferences.OnSharedPreferenceChangeListener value) { - registerPreferenceChangedListener(prefApp, value); - } - - public void registerPreferenceChangedListener(SharedPreferences pref, SharedPreferences.OnSharedPreferenceChangeListener value) { - pref.registerOnSharedPreferenceChangeListener(value); - } - - public void unregisterPreferenceChangedListener(SharedPreferences.OnSharedPreferenceChangeListener value) { - unregisterPreferenceChangedListener(prefApp, value); - } - - public void unregisterPreferenceChangedListener(SharedPreferences pref, SharedPreferences.OnSharedPreferenceChangeListener value) { - pref.unregisterOnSharedPreferenceChangeListener(value); - } - - //################################# - //## Getter for resources - //################################# - public String rstr(@StringRes int stringKeyResourceId) { - return context.getString(stringKeyResourceId); - } - - public int rcolor(@ColorRes int resColorId) { - return ContextCompat.getColor(context, resColorId); - } - - //################################# - //## Getter & Setter for settings - //################################# - public void setString(@StringRes int keyResourceId, String value) { - setString(prefApp, keyResourceId, value); - } - - public void setString(SharedPreferences pref, @StringRes int keyResourceId, String value) { - pref.edit().putString(rstr(keyResourceId), value).apply(); - } - - public String getString(@StringRes int keyResourceId, String defaultValue) { - return getString(prefApp, keyResourceId, defaultValue); - } - - public String getString(SharedPreferences pref, @StringRes int keyResourceId, String defaultValue) { - return pref.getString(rstr(keyResourceId), defaultValue); - } - - public String getString(@StringRes int keyResourceId, @StringRes int keyResourceIdDefaultValue) { - return getString(prefApp, keyResourceId, keyResourceIdDefaultValue); - } - - public String getString(SharedPreferences pref, @StringRes int keyResourceId, @StringRes int keyResourceIdDefaultValue) { - return pref.getString(rstr(keyResourceId), rstr(keyResourceIdDefaultValue)); - } - - public void setStringArray(@StringRes int keyResourceId, Object[] values) { - setStringArray(prefApp, keyResourceId, values); - } - - public void setStringArray(SharedPreferences pref, @StringRes int keyResourceId, Object[] values) { - StringBuilder sb = new StringBuilder(); - for (Object value : values) { - sb.append(ARRAY_SEPARATOR); - sb.append(value.toString().replace(ARRAY_SEPARATOR, ARRAY_SEPARATOR_SUBSTITUTE)); - } - setString(pref, keyResourceId, sb.toString().replaceFirst(ARRAY_SEPARATOR, "")); - } - - @NonNull - public String[] getStringArray(@StringRes int keyResourceId) { - return getStringArray(prefApp, keyResourceId); - } - - @NonNull - public String[] getStringArray(SharedPreferences pref, @StringRes int keyResourceId) { - String value = pref.getString(rstr(keyResourceId), ARRAY_SEPARATOR); - if (value.equals(ARRAY_SEPARATOR)) { - return new String[0]; - } - return value.split(ARRAY_SEPARATOR); - } - - public void setStringList(@StringRes int keyResourceId, List values) { - setStringList(prefApp, keyResourceId, values); - } - - public void setStringList(SharedPreferences pref, @StringRes int keyResourceId, List values) { - setStringArray(pref, keyResourceId, values.toArray(new String[values.size()])); - } - - public ArrayList getStringList(@StringRes int keyResourceId) { - return getStringList(prefApp, keyResourceId); - } - - public ArrayList getStringList(SharedPreferences pref, @StringRes int keyResourceId) { - return new ArrayList<>(Arrays.asList(getStringArray(pref, keyResourceId))); - } - - public void setLong(@StringRes int keyResourceId, long value) { - setLong(prefApp, keyResourceId, value); - } - - public void setLong(SharedPreferences pref, @StringRes int keyResourceId, long value) { - pref.edit().putLong(rstr(keyResourceId), value).apply(); - } - - public long getLong(@StringRes int keyResourceId, long defaultValue) { - return getLong(prefApp, keyResourceId, defaultValue); - } - - public long getLong(SharedPreferences pref, @StringRes int keyResourceId, long defaultValue) { - return pref.getLong(rstr(keyResourceId), defaultValue); - } - - public void setBool(@StringRes int keyResourceId, boolean value) { - setBool(prefApp, keyResourceId, value); - } - - public void setBool(SharedPreferences pref, @StringRes int keyResourceId, boolean value) { - pref.edit().putBoolean(rstr(keyResourceId), value).apply(); - } - - public boolean getBool(@StringRes int keyResourceId, boolean defaultValue) { - return getBool(prefApp, keyResourceId, defaultValue); - } - - public boolean getBool(SharedPreferences pref, @StringRes int keyResourceId, boolean defaultValue) { - return pref.getBoolean(rstr(keyResourceId), defaultValue); - } - - public int getColor(String key, int defaultColor) { - return getColor(prefApp, key, defaultColor); - } - - public int getColor(SharedPreferences pref, String key, int defaultColor) { - return pref.getInt(key, defaultColor); - } - - public int getColor(@StringRes int keyResourceId, int defaultColor) { - return getColor(prefApp, keyResourceId, defaultColor); - } - - public int getColor(SharedPreferences pref, @StringRes int keyResourceId, int defaultColor) { - return pref.getInt(rstr(keyResourceId), defaultColor); - } - - public void setDouble(@StringRes int keyResId, double value) { - setDouble(prefApp, keyResId, value); - } - - public void setDouble(SharedPreferences pref, @StringRes int keyResId, double value) { - prefApp.edit().putLong(rstr(keyResId), Double.doubleToRawLongBits(value)).apply(); - } - - public double getDouble(@StringRes int keyResId, double defaultValue) { - return getDouble(prefApp, keyResId, defaultValue); - } - - public double getDouble(SharedPreferences pref, @StringRes int keyResId, double defaultValue) { - return Double.longBitsToDouble(prefApp.getLong(rstr(keyResId), Double.doubleToLongBits(defaultValue))); - } - - public int getIntOfStringPref(@StringRes int keyResId, int defaultValue) { - String strNum = prefApp.getString(context.getString(keyResId), Integer.toString(defaultValue)); - return Integer.valueOf(strNum); - } - - public void setInt(@StringRes int keyResourceId, int value) { - setInt(prefApp, keyResourceId, value); - } - - public void setInt(SharedPreferences pref, @StringRes int keyResourceId, int value) { - pref.edit().putInt(rstr(keyResourceId), value).apply(); - } - - public int getInt(@StringRes int keyResourceId, int defaultValue) { - return getInt(prefApp, keyResourceId, defaultValue); - } - - public int getInt(SharedPreferences pref, @StringRes int keyResourceId, int defaultValue) { - return pref.getInt(rstr(keyResourceId), defaultValue); - } - - public void setIntList(@StringRes int keyResId, List values) { - setIntList(prefApp, keyResId, values); - } - - public void setIntList(SharedPreferences pref, @StringRes int keyResId, List values) { - StringBuilder sb = new StringBuilder(); - for (int value : values) { - sb.append(ARRAY_SEPARATOR); - sb.append(Integer.toString(value)); - } - setString(prefApp, keyResId, sb.toString().replaceFirst(ARRAY_SEPARATOR, "")); - } - - @NonNull - public ArrayList getIntList(@StringRes int keyResId) { - return getIntList(prefApp, keyResId); - } - - @NonNull - public ArrayList getIntList(SharedPreferences pref, @StringRes int keyResId) { - ArrayList ret = new ArrayList<>(); - String value = getString(prefApp, keyResId, ARRAY_SEPARATOR); - if (value.equals(ARRAY_SEPARATOR)) { - return ret; - } - for (String s : value.split(ARRAY_SEPARATOR)) { - ret.add(Integer.parseInt(s)); - } - return ret; - } -} diff --git a/app/src/main/java/io/github/gsantner/opoc/util/Helpers.java b/app/src/main/java/io/github/gsantner/opoc/util/Helpers.java deleted file mode 100644 index 21c12492..00000000 --- a/app/src/main/java/io/github/gsantner/opoc/util/Helpers.java +++ /dev/null @@ -1,221 +0,0 @@ -/* - * ---------------------------------------------------------------------------- * - * Gregor Santner wrote this file. You can do whatever - * you want with this stuff. If we meet some day, and you think this stuff is - * worth it, you can buy me a coke in return. Provided as is without any kind - * of warranty. No attribution required. - Gregor Santner - * - * License: Creative Commons Zero (CC0 1.0) - * http://creativecommons.org/publicdomain/zero/1.0/ - * ---------------------------------------------------------------------------- - */ -package io.github.gsantner.opoc.util; - -import android.annotation.SuppressLint; -import android.app.AlarmManager; -import android.app.PendingIntent; -import android.content.ActivityNotFoundException; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.res.ColorStateList; -import android.content.res.Configuration; -import android.graphics.drawable.Drawable; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.net.Uri; -import android.support.annotation.ColorRes; -import android.support.annotation.DrawableRes; -import android.support.annotation.RawRes; -import android.support.annotation.StringRes; -import android.support.v4.content.ContextCompat; -import android.support.v7.app.AlertDialog; -import android.support.v7.widget.AppCompatButton; -import android.text.TextUtils; -import android.util.DisplayMetrics; -import android.webkit.WebView; - -import com.github.dfa.diaspora_android.App; -import com.github.dfa.diaspora_android.BuildConfig; -import com.github.dfa.diaspora_android.R; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.util.Locale; - -@SuppressWarnings({"WeakerAccess", "unused", "SameParameterValue"}) -public class Helpers { - protected Context context; - - protected Helpers(Context context) { - this.context = context; - } - - public static Helpers get() { - return new Helpers(App.get()); - } - - public String str(@StringRes int strResId) { - return context.getString(strResId); - } - - public Drawable drawable(@DrawableRes int resId) { - return ContextCompat.getDrawable(context, resId); - } - - public int color(@ColorRes int resId) { - return ContextCompat.getColor(context, resId); - } - - public Context context() { - return context; - } - - public String colorToHexString(int intColor) { - return String.format("#%06X", 0xFFFFFF & intColor); - } - - public String getAppVersionName() { - try { - PackageManager manager = context.getPackageManager(); - PackageInfo info = manager.getPackageInfo(context.getPackageName(), 0); - return info.versionName; - } catch (PackageManager.NameNotFoundException e) { - e.printStackTrace(); - return "unknown"; - } - } - - - public void openWebpageInExternalBrowser(String url) { - Uri uri = Uri.parse(url); - Intent intent = new Intent(Intent.ACTION_VIEW, uri); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(intent); - } - - public void showDonateBitcoinRequest() { - if (!BuildConfig.IS_GPLAY_BUILD) { - String btcUri = String.format("bitcoin:%s?amount=%s&label=%s&message=%s", - str(R.string.donate__bitcoin_id), str(R.string.donate__bitcoin_amount), - str(R.string.donate__bitcoin_message), str(R.string.donate__bitcoin_message)); - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setData(Uri.parse(btcUri)); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - try { - context.startActivity(intent); - } catch (ActivityNotFoundException e) { - openWebpageInExternalBrowser(str(R.string.donate__bitcoin_url)); - } - } - } - - public String readTextfileFromRawRes(@RawRes int rawResId, String linePrefix, String linePostfix) { - StringBuilder sb = new StringBuilder(); - BufferedReader br = null; - String line; - - linePrefix = linePrefix == null ? "" : linePrefix; - linePostfix = linePostfix == null ? "" : linePostfix; - - try { - br = new BufferedReader(new InputStreamReader(context.getResources().openRawResource(rawResId))); - while ((line = br.readLine()) != null) { - sb.append(linePrefix); - sb.append(line); - sb.append(linePostfix); - sb.append("\n"); - } - } catch (Exception ignored) { - } finally { - if (br != null) { - try { - br.close(); - } catch (IOException ignored) { - } - } - } - return sb.toString(); - } - - public void showDialogWithRawFileInWebView(String fileInRaw, @StringRes int resTitleId) { - WebView wv = new WebView(context); - wv.loadUrl("file:///android_res/raw/" + fileInRaw); - AlertDialog.Builder dialog = new AlertDialog.Builder(context) - .setPositiveButton(android.R.string.ok, null) - .setTitle(resTitleId) - .setView(wv); - dialog.show(); - } - - @SuppressLint("RestrictedApi") - public void setTintColorOfButton(AppCompatButton button, @ColorRes int resColor) { - button.setSupportBackgroundTintList(ColorStateList.valueOf( - color(resColor) - )); - } - - public boolean isConnectedToInternet() { - ConnectivityManager connectivityManager = (ConnectivityManager) - context.getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo activeNetInfo = connectivityManager.getActiveNetworkInfo(); - return activeNetInfo != null && activeNetInfo.isConnectedOrConnecting(); - } - - public void restartApp(Class classToStartupWith) { - Intent restartIntent = new Intent(context, classToStartupWith); - PendingIntent restartIntentP = PendingIntent.getActivity(context, 555, - restartIntent, PendingIntent.FLAG_CANCEL_CURRENT); - AlarmManager mgr = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 100, restartIntentP); - System.exit(0); - } - - public String loadMarkdownForTextViewFromRaw(@RawRes int rawMdFile, String prepend) { - try { - return new SimpleMarkdownParser() - .parse(context.getResources().openRawResource(rawMdFile), - SimpleMarkdownParser.FILTER_ANDROID_TEXTVIEW, prepend) - .replaceColor("#000001", color(R.color.accent)) - .removeMultiNewlines().replaceBulletCharacter("*").getHtml(); - } catch (IOException e) { - e.printStackTrace(); - return ""; - } - } - - public double getEstimatedScreenSizeInches() { - DisplayMetrics dm = context.getResources().getDisplayMetrics(); - - double density = dm.density * 160; - double x = Math.pow(dm.widthPixels / density, 2); - double y = Math.pow(dm.heightPixels / density, 2); - double screenInches = Math.sqrt(x + y) * 1.16; // 1.16 = est. Nav/Statusbar - screenInches = screenInches < 4.0 ? 4.0 : screenInches; - screenInches = screenInches > 12.0 ? 12.0 : screenInches; - return screenInches; - } - - public boolean isInPortraitMode() { - return context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT; - } - - public Locale getLocaleByAndroidCode(String code) { - if (!TextUtils.isEmpty(code)) { - return code.contains("-r") - ? new Locale(code.substring(0, 2), code.substring(4, 6)) // de-rAt - : new Locale(code); // de - } - return Locale.getDefault(); - } - - // "en"/"de"/"de-rAt"; Empty string = default locale - public void setAppLanguage(String androidLocaleString) { - Locale locale = getLocaleByAndroidCode(androidLocaleString); - Configuration config = context.getResources().getConfiguration(); - config.locale = locale != null ? locale : Locale.getDefault(); - context.getResources().updateConfiguration(config, null); - } -} diff --git a/app/src/main/java/io/github/gsantner/opoc/util/HelpersA.java b/app/src/main/java/io/github/gsantner/opoc/util/HelpersA.java deleted file mode 100644 index 5989138f..00000000 --- a/app/src/main/java/io/github/gsantner/opoc/util/HelpersA.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * ---------------------------------------------------------------------------- * - * Gregor Santner wrote this file. You can do whatever - * you want with this stuff. If we meet some day, and you think this stuff is - * worth it, you can buy me a coke in return. Provided as is without any kind - * of warranty. No attribution required. - Gregor Santner - * - * License: Creative Commons Zero (CC0 1.0) - * http://creativecommons.org/publicdomain/zero/1.0/ - * ---------------------------------------------------------------------------- - */ -package io.github.gsantner.opoc.util; - -import android.app.Activity; -import android.content.DialogInterface; -import android.content.Intent; -import android.support.annotation.StringRes; -import android.support.design.widget.Snackbar; -import android.support.v7.app.AlertDialog; -import android.support.v7.widget.AppCompatTextView; -import android.text.Html; -import android.text.SpannableString; -import android.text.method.ScrollingMovementMethod; -import android.util.TypedValue; -import android.view.inputmethod.InputMethodManager; - -import com.github.dfa.diaspora_android.R; - - -@SuppressWarnings({"WeakerAccess", "unused", "SameParameterValue"}) -public class HelpersA extends Helpers { - protected Activity activity; - - protected HelpersA(Activity activity) { - super(activity); - this.activity = activity; - } - - public static HelpersA get(Activity activity) { - return new HelpersA(activity); - } - - - //######################## - //## Methods - //######################## - - /** - * Animate to specified Activity - * - * @param to The class of the activity - * @param finishFromActivity true: Finish the current activity - * @param requestCode Request code for stating the activity, not waiting for result if null - */ - public void animateToActivity(Class to, Boolean finishFromActivity, Integer requestCode) { - animateToActivity(new Intent(activity, to), finishFromActivity, requestCode); - } - - /** - * Animate to activity specified in intent - * - * @param intent Intent to open start an activity - * @param finishFromActivity true: Finish the current activity - * @param requestCode Request code for stating the activity, not waiting for result if null - */ - public void animateToActivity(Intent intent, Boolean finishFromActivity, Integer requestCode) { - intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); - if (requestCode != null) { - activity.startActivityForResult(intent, requestCode); - } else { - activity.startActivity(intent); - - } - activity.overridePendingTransition(R.anim.fadein, R.anim.fadeout); - if (finishFromActivity != null && finishFromActivity) { - activity.finish(); - } - } - - - public void showSnackBar(@StringRes int stringId, boolean showLong) { - Snackbar.make(activity.findViewById(android.R.id.content), stringId, - showLong ? Snackbar.LENGTH_LONG : Snackbar.LENGTH_SHORT).show(); - } - - public void hideSoftKeyboard() { - InputMethodManager inputMethodManager = (InputMethodManager) activity.getSystemService(Activity.INPUT_METHOD_SERVICE); - if (activity.getCurrentFocus() != null && activity.getCurrentFocus().getWindowToken() != null) { - inputMethodManager.hideSoftInputFromWindow(activity.getCurrentFocus().getWindowToken(), 0); - } - } - - public void showDialogWithHtmlTextView(@StringRes int resTitleId, String html) { - showDialogWithHtmlTextView(resTitleId, html, null); - } - - public void showDialogWithHtmlTextView(@StringRes int resTitleId, String html, DialogInterface.OnDismissListener dismissedListener) { - AppCompatTextView textView = new AppCompatTextView(context); - int padding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16, - context.getResources().getDisplayMetrics()); - textView.setMovementMethod(new ScrollingMovementMethod()); - textView.setPadding(padding, 0, padding, 0); - - textView.setText(new SpannableString(Html.fromHtml(html))); - AlertDialog.Builder dialog = new AlertDialog.Builder(context) - .setPositiveButton(android.R.string.ok, null) - .setOnDismissListener(dismissedListener) - .setTitle(resTitleId) - .setView(textView); - dialog.show(); - } -} diff --git a/app/src/main/java/io/github/gsantner/opoc/util/SimpleMarkdownParser.java b/app/src/main/java/io/github/gsantner/opoc/util/SimpleMarkdownParser.java deleted file mode 100644 index 0cdebcbb..00000000 --- a/app/src/main/java/io/github/gsantner/opoc/util/SimpleMarkdownParser.java +++ /dev/null @@ -1,160 +0,0 @@ -/* - * ---------------------------------------------------------------------------- * - * Gregor Santner wrote this file. You can do whatever - * you want with this stuff. If we meet some day, and you think this stuff is - * worth it, you can buy me a coke in return. Provided as is without any kind - * of warranty. No attribution required. - Gregor Santner - * - * License: Creative Commons Zero (CC0 1.0) - * http://creativecommons.org/publicdomain/zero/1.0/ - * ---------------------------------------------------------------------------- - */ - - /* - * Get updates: - * https://github.com/gsantner/onePieceOfCode/blob/master/java/SimpleMarkdownParser.java - * Apply to TextView: - * See https://github.com/gsantner/onePieceOfCode/blob/master/android/Helpers.get().java - * Parses most common markdown tags. Only inline tags are supported, multiline/block syntax - * is not supported (citation, multiline code, ..). This is intended to stay as easy as possible. - * - * You can e.g. apply a accent color by replacing #000001 with your accentColor string. - * - * FILTER_ANDROID_TEXTVIEW output is intended to be used at simple Android TextViews, - * were a limited set of html tags is supported. This allow to still display e.g. a simple - * CHANGELOG.md file without inlcuding a WebView for showing HTML, or other additional UI-libraries. - * - * FILTER_HTMLPART is intended to be used at engines understanding most common HTML tags. - */ - -package io.github.gsantner.opoc.util; - -import java.io.BufferedReader; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; - -/** - * Simple Markdown Parser - */ -@SuppressWarnings({"WeakerAccess", "CaughtExceptionImmediatelyRethrown"}) -public class SimpleMarkdownParser { - public interface SimpleLineFilter { - String filterLine(String line); - } - - public static final SimpleLineFilter FILTER_ANDROID_TEXTVIEW = new SimpleLineFilter() { - @Override - public String filterLine(String line) { - // TextView supports a limited set of html tags, most notably - // a href, b, big, font size&color, i, li, small, u - line = line - .replace("~°", "  ") // double space/half tab - .replaceAll("^### ([^<]*)", "
$1 ") // h3 - .replaceAll("^## ([^<]*)", "
$1
") // h2 (DEP: h3) - .replaceAll("^# ([^<]*)", "
$1
") // h1 (DEP: h2,h3) - .replaceAll("!\\[(.*?)\\]\\((.*?)\\)", "$1") // img - .replaceAll("\\[(.*?)\\]\\((.*?)\\)", "$1") // a href (DEP: img) - .replaceAll("<(http|https):\\/\\/(.*)>", "$1://$2") // a href (DEP: img) - .replaceAll("^(-|\\*) ([^<]*)", " $2 ") // unordered list + end line - .replaceAll("^ (-|\\*) ([^<]*)", "   $2 ") // unordered list2 + end line - .replaceAll("`([^<]*)`", "$1") // code - .replace("\\*", "●") // temporary replace escaped star symbol - .replaceAll("\\*\\*([^<]*)\\*\\*", "$1") // bold (DEP: temp star) - .replaceAll("\\*([^<]*)\\*", "$1") // italic (DEP: temp star code) - .replace("●", "*") // restore escaped star symbol (DEP: b,i) - .replaceAll(" $", "
") // new line (DEP: ul) - ; - return line.isEmpty() ? line + "
" : line; - } - }; - - public static final SimpleLineFilter FILTER_HTMLPART = new SimpleLineFilter() { - @Override - public String filterLine(String line) { - line = line - .replaceAll("~°", "  ") // double space/half tab - .replaceAll("^### ([^<]*)", "

$1

") // h3 - .replaceAll("^## ([^<]*)", "

$1

") /// h2 (DEP: h3) - .replaceAll("^# ([^<]*)", "

$1

") // h1 (DEP: h2,h3) - .replaceAll("!\\[(.*?)\\]\\((.*?)\\)", "$1") // img - .replaceAll("<(http|https):\\/\\/(.*)>", "$1://$2") // a href (DEP: img) - .replaceAll("\\[(.*?)\\]\\((.*?)\\)", "$1") // a href (DEP: img) - .replaceAll("^(-|\\*) ([^<]*)", " $2 ") // unordered list + end line - .replaceAll("^ (-|\\*) ([^<]*)", "   $2 ") // unordered list2 + end line - .replaceAll("`([^<]*)`", "$1") // code - .replace("\\*", "●") // temporary replace escaped star symbol - .replaceAll("\\*\\*([^<]*)\\*\\*", "$1") // bold (DEP: temp star) - .replaceAll("\\*([^<]*)\\*", "$1") // italic (DEP: temp star) - .replace("●", "*") // restore escaped star symbol (DEP: b,i) - .replaceAll(" $", "
") // new line (DEP: ul) - ; - return line.isEmpty() ? line + "
" : line; - } - }; - - //######################## - //## Members - //######################## - private String html; - - public SimpleMarkdownParser parse(String filepath, SimpleLineFilter simpleLineFilter) throws IOException { - return parse(new FileInputStream(filepath), simpleLineFilter, ""); - } - - public SimpleMarkdownParser parse(InputStream inputStream, SimpleLineFilter simpleLineFilter, String lineMdPrefix) throws IOException { - StringBuilder sb = new StringBuilder(); - BufferedReader br = null; - String line; - - try { - br = new BufferedReader(new InputStreamReader(inputStream)); - while ((line = br.readLine()) != null) { - sb.append(simpleLineFilter.filterLine(lineMdPrefix + line)); - sb.append("\n"); - } - } catch (IOException rethrow) { - html = ""; - throw rethrow; - } finally { - if (br != null) { - try { - br.close(); - } catch (IOException ignored) { - } - } - } - html = sb.toString().trim(); - return this; - } - - public String getHtml() { - return html; - } - - public SimpleMarkdownParser setHtml(String html) { - this.html = html; - return this; - } - - public SimpleMarkdownParser removeMultiNewlines() { - html = html.replace("\n", "").replaceAll("(
){3,}", "

"); - return this; - } - - public SimpleMarkdownParser replaceBulletCharacter(String replacment) { - html = html.replace("•", replacment); - return this; - } - - public SimpleMarkdownParser replaceColor(String hexColor, int newIntColor) { - html = html.replace(hexColor, String.format("#%06X", 0xFFFFFF & newIntColor)); - return this; - } - - @Override - public String toString() { - return html != null ? html : ""; - } -} diff --git a/app/src/main/java/net/gsantner/opoc/activity/GsFragmentBase.java b/app/src/main/java/net/gsantner/opoc/activity/GsFragmentBase.java new file mode 100644 index 00000000..479551d9 --- /dev/null +++ b/app/src/main/java/net/gsantner/opoc/activity/GsFragmentBase.java @@ -0,0 +1,160 @@ +/*####################################################### + * + * 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.activity; + +import android.content.Context; +import android.os.Bundle; +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; + +/** + * A common base fragment to extend from + */ +public abstract class GsFragmentBase extends Fragment { + private boolean _fragmentFirstTimeVisible = true; + private final Object _fragmentFirstTimeVisibleSync = new Object(); + + protected ContextUtils _cu; + protected Bundle _savedInstanceState = null; + protected Menu _fragmentMenu = new MenuItemDummy.Menu(); + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + /** + * Inflate the fragments layout. Don't override this method, just supply the needed + * {@link LayoutRes} via abstract method {@link #getLayoutResId()}, super does the rest + */ + @Deprecated + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + _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; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + view.postDelayed(() -> { + synchronized (_fragmentFirstTimeVisibleSync) { + if (getUserVisibleHint() && isVisible() && _fragmentFirstTimeVisible) { + _fragmentFirstTimeVisible = false; + onFragmentFirstTimeVisible(); + } + } + }, 1); + } + + /** + * Get a tag from the fragment, allows faster distinction + * + * @return This fragments tag + */ + public abstract String getFragmentTag(); + + + /** + * Get the layout to be inflated in the fragment + * + * @return Layout resource id + */ + @LayoutRes + protected abstract int getLayoutResId(); + + /** + * Event to be called when the back button was pressed + * True should be returned when this was handled by the fragment + * and no further handling in the view hierarchy is needed + * + * @return True if back handled by fragment + */ + public boolean onBackPressed() { + return false; + } + + /** + * Set the language to be used in this fragment + * Defaults to resolve the language from sharedpreferences: pref_key__language + * + * @return Empty string for system language, or an android locale code + */ + public String getAppLanguage() { + if (getContext() != null) { + return getContext().getSharedPreferences("app", Context.MODE_PRIVATE) + .getString("pref_key__language", ""); + } + return ""; + } + + /** + * This will be called when this fragment gets the first time visible + */ + public void onFragmentFirstTimeVisible() { + } + + @Override + public void setUserVisibleHint(boolean isVisibleToUser) { + super.setUserVisibleHint(isVisibleToUser); + synchronized (_fragmentFirstTimeVisibleSync) { + if (isVisibleToUser && _fragmentFirstTimeVisible) { + _fragmentFirstTimeVisible = false; + onFragmentFirstTimeVisible(); + } + } + } + + @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 new file mode 100644 index 00000000..ea117a8a --- /dev/null +++ b/app/src/main/java/net/gsantner/opoc/format/markdown/SimpleMarkdownParser.java @@ -0,0 +1,248 @@ +/*####################################################### + * + * Maintained 2018-2023 by Gregor Santner + * + * License: Apache 2.0 + * https://github.com/gsantner/opoc/#licensing + * https://www.apache.org/licenses/LICENSE-2.0 + * +#########################################################*/ + +/* + * Parses most common markdown tags. Only inline tags are supported, multiline/block syntax + * is not supported (citation, multiline code, ..). This is intended to stay as easy as possible. + * + * You can e.g. apply a accent color by replacing #000001 with your accentColor string. + * + * FILTER_ANDROID_TEXTVIEW output is intended to be used at simple Android TextViews, + * were a limited set of _html tags is supported. This allow to still display e.g. a simple + * CHANGELOG.md file without including a WebView for showing HTML, or other additional UI-libraries. + * + * FILTER_WEB is intended to be used at engines understanding most common HTML tags. + */ + +package net.gsantner.opoc.format.markdown; + +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; + +/** + * Simple Markdown Parser + */ +@SuppressWarnings({"WeakerAccess", "CaughtExceptionImmediatelyRethrown", "SameParameterValue", "unused", "SpellCheckingInspection", "RepeatedSpace", "SingleCharAlternation", "Convert2Lambda"}) +public class SimpleMarkdownParser { + //######################## + //## Statics + //######################## + public interface SmpFilter { + String filter(String text); + } + + public final static SmpFilter FILTER_ANDROID_TEXTVIEW = new SmpFilter() { + @Override + public String filter(String text) { + // TextView supports a limited set of html tags, most notably + // a href, b, big, font size&color, i, li, small, u + + // Don't start new line if 2 empty lines and heading + while (text.contains("\n\n#")) { + text = text.replace("\n\n#", "\n#"); + } + + return text + .replaceAll("(?s)", "") // HTML comments + .replace("\n\n", "\n
\n") // Start new line if 2 empty lines + .replace("~°", "  ") // double space/half tab + .replaceAll("(?m)^### (.*)$", "
$1
") // h3 + .replaceAll("(?m)^## (.*)$", "
$1

") // h2 (DEP: h3) + .replaceAll("(?m)^# (.*)$", "
$1

") // h1 (DEP: h2,h3) + .replaceAll("!\\[(.*?)\\]\\((.*?)\\)", "$1") // img + .replaceAll("\\[(.*?)\\]\\((.*?)\\)", "$1") // a href (DEP: img) + .replaceAll("<(http|https):\\/\\/(.*)>", "$1://$2") // a href (DEP: img) + .replaceAll("(?m)^([-*] )(.*)$", " $2
") // unordered list + end line + .replaceAll("(?m)^ (-|\\*) ([^<]*)$", "   $2
") // unordered list2 + end line + .replaceAll("`([^<]*)`", "$1") // code + .replace("\\*", "●") // temporary replace escaped star symbol + .replaceAll("(?m)\\*\\*(.*)\\*\\*", "$1") // bold (DEP: temp star) + .replaceAll("(?m)\\*(.*)\\*", "$1") // italic (DEP: temp star code) + .replace("●", "*") // restore escaped star symbol (DEP: b,i) + .replaceAll("(?m) $", "
") // new line (DEP: ul) + ; + } + }; + + public final static SmpFilter FILTER_WEB = new SmpFilter() { + @Override + public String filter(String text) { + // Don't start new line if 2 empty lines and heading + while (text.contains("\n\n#")) { + text = text.replace("\n\n#", "\n#"); + } + + text = text + .replaceAll("(?s)", "") // HTML comments + .replace("\n\n", "\n
\n") // Start new line if 2 empty lines + .replaceAll("~°", "  ") // double space/half tab + .replaceAll("(?m)^### (.*)$", "

$1

") // h3 + .replaceAll("(?m)^## (.*)$", "

$1

") /// h2 (DEP: h3) + .replaceAll("(?m)^# (.*)$", "

$1

") // h1 (DEP: h2,h3) + .replaceAll("!\\[(.*?)\\]\\((.*?)\\)", "$1") // img + .replaceAll("<(http|https):\\/\\/(.*)>", "$1://$2") // a href (DEP: img) + .replaceAll("\\[(.*?)\\]\\((.*?)\\)", "$1") // a href (DEP: img) + .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) + .replaceAll("(?m)\\*(.*)\\*", "$1") // italic (DEP: temp star code) + .replace("●", "*") // restore escaped star symbol (DEP: b,i) + .replaceAll("(?m) $", "
") // new line (DEP: ul) + ; + return text; + } + }; + + public final static SmpFilter FILTER_CHANGELOG = new SmpFilter() { + @Override + public String filter(String text) { + text = text + .replace("New:", "New:") + .replace("New features:", "New:") + .replace("Added:", "Added:") + .replace("Add:", "Add:") + .replace("Fixed:", "Fixed:") + .replace("Fix:", "Fix:") + .replace("Removed:", "Removed:") + .replace("Updated:", "Updated:") + .replace("Improved:", "Improved:") + .replace("Modified:", "Modified:") + .replace("Mod:", "Mod:") + ; + 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 + //######################## + private static SimpleMarkdownParser __instance; + + public static SimpleMarkdownParser get() { + if (__instance == null) { + __instance = new SimpleMarkdownParser(); + } + return __instance; + } + + //######################## + //## Members, Constructors + //######################## + private SmpFilter _defaultSmpFilter; + private String _html; + + public SimpleMarkdownParser() { + setDefaultSmpFilter(FILTER_WEB); + } + + //######################## + //## Methods + //######################## + public SimpleMarkdownParser setDefaultSmpFilter(SmpFilter defaultSmpFilter) { + _defaultSmpFilter = defaultSmpFilter; + return this; + } + + public SimpleMarkdownParser parse(String filepath, SmpFilter... smpFilters) throws IOException { + return parse(new FileInputStream(filepath), "", smpFilters); + } + + public SimpleMarkdownParser parse(InputStream inputStream, String lineMdPrefix, SmpFilter... smpFilters) throws IOException { + StringBuilder sb = new StringBuilder(); + BufferedReader br = null; + String line; + + try { + br = new BufferedReader(new InputStreamReader(inputStream)); + while ((line = br.readLine()) != null) { + sb.append(lineMdPrefix); + sb.append(line); + sb.append("\n"); + } + } catch (IOException rethrow) { + _html = ""; + throw rethrow; + } finally { + if (br != null) { + try { + br.close(); + } catch (IOException ignored) { + } + } + } + _html = parse(sb.toString(), "", smpFilters).getHtml(); + return this; + } + + public SimpleMarkdownParser parse(String markdown, String lineMdPrefix, SmpFilter... smpFilters) throws IOException { + _html = markdown; + if (smpFilters.length == 0) { + smpFilters = new SmpFilter[]{_defaultSmpFilter}; + } + for (SmpFilter smpFilter : smpFilters) { + _html = smpFilter.filter(_html).trim(); + } + return this; + } + + public String getHtml() { + return _html; + } + + public SimpleMarkdownParser setHtml(String html) { + _html = html; + return this; + } + + public SimpleMarkdownParser removeMultiNewlines() { + _html = _html.replace("\n", "").replaceAll("(
){3,}", "

"); + return this; + } + + public SimpleMarkdownParser replaceBulletCharacter(String replacment) { + _html = _html.replace("•", replacment); + return this; + } + + public SimpleMarkdownParser replaceColor(String hexColor, int newIntColor) { + _html = _html.replace(hexColor, String.format("#%06X", 0xFFFFFF & newIntColor)); + return this; + } + + @Override + public String toString() { + return _html != null ? _html : ""; + } +} diff --git a/app/src/main/java/net/gsantner/opoc/preference/PropertyBackend.java b/app/src/main/java/net/gsantner/opoc/preference/PropertyBackend.java new file mode 100644 index 00000000..f96af499 --- /dev/null +++ b/app/src/main/java/net/gsantner/opoc/preference/PropertyBackend.java @@ -0,0 +1,47 @@ +/*####################################################### + * + * Maintained 2018-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.preference; + +import java.util.List; + +@SuppressWarnings({"UnusedReturnValue", "SpellCheckingInspection", "unused", "SameParameterValue"}) +public interface PropertyBackend { + String getString(TKEY key, String defaultValue); + + int getInt(TKEY key, int defaultValue); + + long getLong(TKEY key, long defaultValue); + + boolean getBool(TKEY key, boolean defaultValue); + + float getFloat(TKEY key, float defaultValue); + + double getDouble(TKEY key, double defaultValue); + + List getIntList(TKEY key); + + List getStringList(TKEY key); + + TTHIS setString(TKEY key, String value); + + TTHIS setInt(TKEY key, int value); + + TTHIS setLong(TKEY key, long value); + + TTHIS setBool(TKEY key, boolean value); + + TTHIS setFloat(TKEY key, float value); + + TTHIS setDouble(TKEY key, double value); + + 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 new file mode 100644 index 00000000..9f5c948c --- /dev/null +++ b/app/src/main/java/net/gsantner/opoc/preference/SharedPreferencesPropertyBackend.java @@ -0,0 +1,617 @@ +/*####################################################### + * + * Maintained 2016-2023 by Gregor Santner + * + * License: Apache 2.0 + * https://github.com/gsantner/opoc/#licensing + * https://www.apache.org/licenses/LICENSE-2.0 + * +#########################################################*/ + +/* + * This is a wrapper for settings based on SharedPreferences + * with keys in resources. Extend from this class and add + * getters/setters for the app's settings. + * Example: + public boolean isAppFirstStart(boolean doSet) { + int value = getInt(R.string.pref_key__app_first_start, -1); + if (doSet) { + setBool(true); + } + return value; + } + + public boolean isAppCurrentVersionFirstStart(boolean doSet) { + int value = getInt(R.string.pref_key__app_first_start_current_version, -1); + if (doSet) { + setInt(R.string.pref_key__app_first_start_current_version, BuildConfig.VERSION_CODE); + } + return value != BuildConfig.VERSION_CODE; + } + */ + +package net.gsantner.opoc.preference; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.SharedPreferences; +import android.support.annotation.ColorRes; +import android.support.annotation.NonNull; +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; + + +/** + * Wrapper for settings based on SharedPreferences, optionally with keys in resources + * Default SharedPreference (_prefApp) will be taken if no SP is specified, else the first one + */ +@SuppressWarnings({"WeakerAccess", "unused", "SpellCheckingInspection", "SameParameterValue"}) +public class SharedPreferencesPropertyBackend implements PropertyBackend { + protected static final String ARRAY_SEPARATOR = "%%%"; + protected static final String ARRAY_SEPARATOR_SUBSTITUTE = "§§§"; + public static final String SHARED_PREF_APP = "app"; + private static String _debugLog = ""; + + + // + // Members, Constructors + // + protected final SharedPreferences _prefApp; + protected final String _prefAppName; + protected final Context _context; + + public SharedPreferencesPropertyBackend(final Context context) { + this(context, SHARED_PREF_APP); + } + + public SharedPreferencesPropertyBackend(final Context context, final String prefAppName) { + _context = context.getApplicationContext(); + _prefAppName = TextUtils.isEmpty(prefAppName) ? + _context.getPackageName() + "_preferences" : prefAppName; + _prefApp = _context.getSharedPreferences(_prefAppName, Context.MODE_PRIVATE); + } + + // + // Methods + // + public Context getContext() { + return _context; + } + + public boolean isKeyEqual(String key, int stringKeyResourceId) { + return key.equals(rstr(stringKeyResourceId)); + } + + public void resetSettings() { + resetSettings(_prefApp); + } + + @SuppressLint("ApplySharedPref") + public void resetSettings(final SharedPreferences pref) { + pref.edit().clear().commit(); + } + + public boolean isPrefSet(@StringRes int stringKeyResourceId) { + return isPrefSet(_prefApp, stringKeyResourceId); + } + + public boolean isPrefSet(final SharedPreferences pref, @StringRes int stringKeyResourceId) { + return pref.contains(rstr(stringKeyResourceId)); + } + + public void registerPreferenceChangedListener(SharedPreferences.OnSharedPreferenceChangeListener value) { + registerPreferenceChangedListener(_prefApp, value); + } + + public void registerPreferenceChangedListener(final SharedPreferences pref, SharedPreferences.OnSharedPreferenceChangeListener value) { + pref.registerOnSharedPreferenceChangeListener(value); + } + + public void unregisterPreferenceChangedListener(SharedPreferences.OnSharedPreferenceChangeListener value) { + unregisterPreferenceChangedListener(_prefApp, value); + } + + public void unregisterPreferenceChangedListener(final SharedPreferences pref, SharedPreferences.OnSharedPreferenceChangeListener value) { + pref.unregisterOnSharedPreferenceChangeListener(value); + } + + public SharedPreferences getDefaultPreferences() { + return _prefApp; + } + + public SharedPreferences.Editor getDefaultPreferencesEditor() { + return _prefApp.edit(); + } + + public String getDefaultPreferencesName() { + return _prefAppName; + } + + + private SharedPreferences gp(final SharedPreferences... pref) { + return (pref != null && pref.length > 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 + // + public String rstr(@StringRes int stringKeyResourceId) { + return _context.getString(stringKeyResourceId); + } + + public int rcolor(@ColorRes int resColorId) { + return ContextCompat.getColor(_context, resColorId); + } + + public String[] rstrs(int... keyResourceIds) { + String[] ret = new String[keyResourceIds.length]; + for (int i = 0; i < keyResourceIds.length; i++) { + ret[i] = rstr(keyResourceIds[i]); + } + return ret; + } + + + // + // Getter & Setter for String + // + public void setString(@StringRes int keyResourceId, String value, final SharedPreferences... pref) { + gp(pref).edit().putString(rstr(keyResourceId), value).apply(); + } + + public void setString(String key, String value, final SharedPreferences... pref) { + gp(pref).edit().putString(key, value).apply(); + } + + public void setString(@StringRes int keyResourceId, @StringRes int defaultValueResourceId, final SharedPreferences... pref) { + gp(pref).edit().putString(rstr(keyResourceId), rstr(defaultValueResourceId)).apply(); + } + + public String getString(@StringRes int keyResourceId, String defaultValue, final SharedPreferences... pref) { + return gp(pref).getString(rstr(keyResourceId), defaultValue); + } + + public String getString(@StringRes int keyResourceId, @StringRes int defaultValueResourceId, final SharedPreferences... pref) { + return gp(pref).getString(rstr(keyResourceId), rstr(defaultValueResourceId)); + } + + public String getString(String key, String defaultValue, final SharedPreferences... pref) { + try { + return gp(pref).getString(key, defaultValue); + } catch (ClassCastException e) { + return defaultValue; + } + } + + public String getString(@StringRes int keyResourceId, String defaultValue, @StringRes int keyResourceIdDefaultValue, final SharedPreferences... pref) { + return getString(rstr(keyResourceId), rstr(keyResourceIdDefaultValue), pref); + } + + private void setStringListOne(String key, List values, final SharedPreferences pref) { + StringBuilder sb = new StringBuilder(); + for (String value : values) { + sb.append(ARRAY_SEPARATOR); + sb.append(value.replace(ARRAY_SEPARATOR, ARRAY_SEPARATOR_SUBSTITUTE)); + } + setString(key, sb.toString().replaceFirst(ARRAY_SEPARATOR, ""), pref); + } + + private ArrayList getStringListOne(String key, final SharedPreferences pref) { + ArrayList ret = new ArrayList<>(); + 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))); + return ret; + } + + public void setStringArray(@StringRes int keyResourceId, String[] values, final SharedPreferences... pref) { + setStringArray(rstr(keyResourceId), values, pref); + } + + public void setStringArray(String key, String[] values, final SharedPreferences... pref) { + setStringListOne(key, Arrays.asList(values), gp(pref)); + } + + public void setStringList(@StringRes int keyResourceId, List values, final SharedPreferences... pref) { + setStringArray(rstr(keyResourceId), values.toArray(new String[values.size()]), pref); + } + + public void setStringList(String key, List values, final SharedPreferences... pref) { + setStringArray(key, values.toArray(new String[values.size()]), pref); + } + + @NonNull + public String[] getStringArray(@StringRes int keyResourceId, final SharedPreferences... pref) { + return getStringArray(rstr(keyResourceId), pref); + } + + @NonNull + public String[] getStringArray(String key, final SharedPreferences... pref) { + List list = getStringListOne(key, gp(pref)); + return list.toArray(new String[list.size()]); + } + + + public ArrayList getStringList(@StringRes int keyResourceId, final SharedPreferences... pref) { + return getStringListOne(rstr(keyResourceId), gp(pref)); + } + + public ArrayList getStringList(String key, final SharedPreferences... pref) { + return getStringListOne(key, gp(pref)); + } + + // + // Getter & Setter for integer + // + public void setInt(@StringRes int keyResourceId, int value, final SharedPreferences... pref) { + gp(pref).edit().putInt(rstr(keyResourceId), value).apply(); + } + + public void setInt(String key, int value, final SharedPreferences... pref) { + gp(pref).edit().putInt(key, value).apply(); + } + + public int getInt(@StringRes int keyResourceId, int defaultValue, final SharedPreferences... pref) { + return getInt(rstr(keyResourceId), defaultValue, pref); + } + + public int getInt(String key, int defaultValue, final SharedPreferences... pref) { + try { + return gp(pref).getInt(key, defaultValue); + } catch (ClassCastException e) { + return defaultValue; + } + } + + public int getIntOfStringPref(@StringRes int keyResId, int defaultValue, final SharedPreferences... pref) { + return getIntOfStringPref(rstr(keyResId), defaultValue, gp(pref)); + } + + public int getIntOfStringPref(String key, int defaultValue, final SharedPreferences... pref) { + String strNum = getString(key, Integer.toString(defaultValue), gp(pref)); + return Integer.valueOf(strNum); + } + + private void setIntListOne(String key, List values, final SharedPreferences pref) { + StringBuilder sb = new StringBuilder(); + for (Integer value : values) { + sb.append(ARRAY_SEPARATOR); + sb.append(value.toString()); + } + setString(key, sb.toString().replaceFirst(ARRAY_SEPARATOR, ""), pref); + } + + private ArrayList getIntListOne(String key, final SharedPreferences pref) { + ArrayList ret = new ArrayList<>(); + String value = getString(key, ARRAY_SEPARATOR); + if (value.equals(ARRAY_SEPARATOR)) { + return ret; + } + for (String s : value.split(ARRAY_SEPARATOR)) { + ret.add(Integer.parseInt(s)); + } + return ret; + } + + public void setIntArray(@StringRes int keyResourceId, Integer[] values, final SharedPreferences... pref) { + setIntArray(rstr(keyResourceId), values, gp(pref)); + } + + public void setIntArray(String key, Integer[] values, final SharedPreferences... pref) { + setIntListOne(key, Arrays.asList(values), gp(pref)); + } + + public Integer[] getIntArray(@StringRes int keyResourceId, final SharedPreferences... pref) { + return getIntArray(rstr(keyResourceId), gp(pref)); + } + + public Integer[] getIntArray(String key, final SharedPreferences... pref) { + List data = getIntListOne(key, gp(pref)); + return data.toArray(new Integer[data.size()]); + } + + + public void setIntList(@StringRes int keyResourceId, List values, final SharedPreferences... pref) { + setIntListOne(rstr(keyResourceId), values, gp(pref)); + } + + public void setIntList(String key, List values, final SharedPreferences... pref) { + setIntListOne(key, values, gp(pref)); + } + + public ArrayList getIntList(@StringRes int keyResourceId, final SharedPreferences... pref) { + return getIntListOne(rstr(keyResourceId), gp(pref)); + } + + public ArrayList getIntList(String key, final SharedPreferences... pref) { + return getIntListOne(key, gp(pref)); + } + + + // + // Getter & Setter for Long + // + public void setLong(@StringRes int keyResourceId, long value, final SharedPreferences... pref) { + gp(pref).edit().putLong(rstr(keyResourceId), value).apply(); + } + + public void setLong(String key, long value, final SharedPreferences... pref) { + gp(pref).edit().putLong(key, value).apply(); + } + + public long getLong(@StringRes int keyResourceId, long defaultValue, final SharedPreferences... pref) { + return getLong(rstr(keyResourceId), defaultValue, pref); + } + + public long getLong(String key, long defaultValue, final SharedPreferences... pref) { + try { + return gp(pref).getLong(key, defaultValue); + } catch (ClassCastException e) { + return defaultValue; + } + } + + // + // Getter & Setter for Float + // + public void setFloat(@StringRes int keyResourceId, float value, final SharedPreferences... pref) { + gp(pref).edit().putFloat(rstr(keyResourceId), value).apply(); + } + + public void setFloat(String key, float value, final SharedPreferences... pref) { + gp(pref).edit().putFloat(key, value).apply(); + } + + public float getFloat(@StringRes int keyResourceId, float defaultValue, final SharedPreferences... pref) { + return getFloat(rstr(keyResourceId), defaultValue); + } + + public float getFloat(String key, float defaultValue, final SharedPreferences... pref) { + try { + return gp(pref).getFloat(key, defaultValue); + } catch (ClassCastException e) { + return defaultValue; + } + } + + // + // Getter & Setter for Double + // + public void setDouble(@StringRes int keyResourceId, double value, final SharedPreferences... pref) { + setLong(rstr(keyResourceId), Double.doubleToRawLongBits(value)); + } + + public void setDouble(String key, double value, final SharedPreferences... pref) { + setLong(key, Double.doubleToRawLongBits(value)); + } + + public double getDouble(@StringRes int keyResourceId, double defaultValue, final SharedPreferences... pref) { + return getDouble(rstr(keyResourceId), defaultValue, gp(pref)); + } + + public double getDouble(String key, double defaultValue, final SharedPreferences... pref) { + return Double.longBitsToDouble(getLong(key, Double.doubleToRawLongBits(defaultValue), gp(pref))); + } + + // + // Getter & Setter for boolean + // + public void setBool(@StringRes int keyResourceId, boolean value, final SharedPreferences... pref) { + gp(pref).edit().putBoolean(rstr(keyResourceId), value).apply(); + } + + public void setBool(String key, boolean value, final SharedPreferences... pref) { + gp(pref).edit().putBoolean(key, value).apply(); + } + + public boolean getBool(@StringRes int keyResourceId, boolean defaultValue, final SharedPreferences... pref) { + return getBool(rstr(keyResourceId), defaultValue); + } + + public boolean getBool(String key, boolean defaultValue, final SharedPreferences... pref) { + try { + return gp(pref).getBoolean(key, defaultValue); + } catch (ClassCastException e) { + return defaultValue; + } + } + + // + // Getter & Setter for Color + // + public int getColor(String key, @ColorRes int defaultColor, final SharedPreferences... pref) { + return getInt(key, rcolor(defaultColor)); + } + + public int getColor(@StringRes int keyResourceId, @ColorRes int defaultColor, final SharedPreferences... pref) { + return getColor(rstr(keyResourceId), defaultColor); + } + + // + // PropertyBackend implementations + // + @Override + public String getString(String key, String defaultValue) { + return getString(key, defaultValue, _prefApp); + } + + @Override + public int getInt(String key, int defaultValue) { + return getInt(key, defaultValue, _prefApp); + } + + @Override + public long getLong(String key, long defaultValue) { + return getLong(key, defaultValue, _prefApp); + } + + @Override + public boolean getBool(String key, boolean defaultValue) { + return getBool(key, defaultValue, _prefApp); + } + + @Override + public float getFloat(String key, float defaultValue) { + return getFloat(key, defaultValue, _prefApp); + } + + @Override + public double getDouble(String key, double defaultValue) { + return getDouble(key, defaultValue, _prefApp); + } + + @Override + public ArrayList getIntList(String key) { + return getIntList(key, _prefApp); + } + + @Override + public ArrayList getStringList(String key) { + return getStringList(key, _prefApp); + } + + @Override + public SharedPreferencesPropertyBackend setString(String key, String value) { + setString(key, value, _prefApp); + return this; + } + + @Override + public SharedPreferencesPropertyBackend setInt(String key, int value) { + setInt(key, value, _prefApp); + return this; + } + + @Override + public SharedPreferencesPropertyBackend setLong(String key, long value) { + setLong(key, value, _prefApp); + return this; + } + + @Override + public SharedPreferencesPropertyBackend setBool(String key, boolean value) { + setBool(key, value, _prefApp); + return this; + } + + @Override + public SharedPreferencesPropertyBackend setFloat(String key, float value) { + setFloat(key, value, _prefApp); + return this; + } + + @Override + public SharedPreferencesPropertyBackend setDouble(String key, double value) { + setDouble(key, value, _prefApp); + return this; + } + + @Override + public SharedPreferencesPropertyBackend setIntList(String key, List value) { + setIntListOne(key, value, _prefApp); + return this; + } + + @Override + public SharedPreferencesPropertyBackend setStringList(String key, List value) { + setStringListOne(key, value, _prefApp); + return this; + } + + public boolean contains(String key, final SharedPreferences... pref) { + return gp(pref).contains(key); + } + + /** + * A method to determine if current hour is between begin and end. + * This is especially useful for time-based light/dark mode + */ + public static boolean isCurrentHourOfDayBetween(int begin, int end) { + begin = (begin >= 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 new file mode 100644 index 00000000..04c3be4f --- /dev/null +++ b/app/src/main/java/net/gsantner/opoc/preference/nonsupport/LanguagePreference.java @@ -0,0 +1,187 @@ +/*####################################################### + * + * Maintained 2017-2023 by Gregor Santner + * + * License: Apache 2.0 + * https://github.com/gsantner/opoc/#licensing + * https://www.apache.org/licenses/LICENSE-2.0 + * +#########################################################*/ + +/* + * Requires: + The BuildConfig field "APPLICATION_LANGUAGES" which is a array of all available languages + opoc/ContextUtils + * BuildConfig field can be defined by using the method below + +buildConfigField "String[]", "APPLICATION_LANGUAGES", "${getUsedAndroidLanguages()}" + +@SuppressWarnings(["UnnecessaryQualifiedReference", "SpellCheckingInspection", "GroovyUnusedDeclaration"]) +// Returns used android languages as a buildConfig array: {'de', 'it', ..}" +static String getUsedAndroidLanguages() { + Set langs = new HashSet<>() + new File('.').eachFileRecurse(groovy.io.FileType.DIRECTORIES) { + final foldername = it.name + if (foldername.startsWith('values-') && !it.canonicalPath.contains("build" + File.separator + "intermediates")) { + new File(it.toString()).eachFileRecurse(groovy.io.FileType.FILES) { + if (it.name.toLowerCase().endsWith(".xml") && it.getCanonicalFile().getText('UTF-8').contains(" + */ +package net.gsantner.opoc.preference.nonsupport; + +import android.annotation.TargetApi; +import android.content.Context; +import android.os.Build; +import android.preference.ListPreference; +import android.support.annotation.Nullable; +import android.text.TextUtils; +import android.util.AttributeSet; + +import net.gsantner.opoc.util.ContextUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +/** + * A {@link android.preference.ListPreference} that displays a list of languages to select from + */ +@SuppressWarnings({"unused", "SpellCheckingInspection", "WeakerAccess"}) +public class LanguagePreference extends ListPreference { + private static final String SYSTEM_LANGUAGE_CODE = ""; + + // The language of res/values/ -> (usually English) + public String _systemLanguageName = "System"; + public String _defaultLanguageCode = "en"; + + public LanguagePreference(Context context) { + super(context); + loadLangs(context, null); + } + + public LanguagePreference(Context context, AttributeSet attrs) { + super(context, attrs); + loadLangs(context, attrs); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public LanguagePreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + loadLangs(context, attrs); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public LanguagePreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + loadLangs(context, attrs); + } + + @Override + public boolean callChangeListener(Object newValue) { + if (newValue instanceof String) { + // Does not apply to existing UI, use recreate() + new ContextUtils(getContext()).setAppLanguage((String) newValue); + } + return super.callChangeListener(newValue); + } + + + private void loadLangs(Context context) { + loadLangs(context, null); + } + + private void loadLangs(Context context, @Nullable AttributeSet attrs) { + setDefaultValue(SYSTEM_LANGUAGE_CODE); + + // Fetch readable details + ContextUtils contextUtils = new ContextUtils(context); + List languages = new ArrayList<>(); + Object bcof = contextUtils.getBuildConfigValue("DETECTED_ANDROID_LOCALES"); + if (bcof instanceof String[]) { + for (String langId : (String[]) bcof) { + Locale locale = contextUtils.getLocaleByAndroidCode(langId); + languages.add(summarizeLocale(locale, langId) + ";" + langId); + } + } + + // Sort languages naturally + Collections.sort(languages); + + // Show in UI + String[] entries = new String[languages.size() + 2]; + String[] entryval = new String[languages.size() + 2]; + for (int i = 0; i < languages.size(); i++) { + entries[i + 2] = languages.get(i).split(";")[0]; + entryval[i + 2] = languages.get(i).split(";")[1]; + } + entryval[0] = SYSTEM_LANGUAGE_CODE; + entries[0] = _systemLanguageName + " » " + summarizeLocale(context.getResources().getConfiguration().locale, ""); + entryval[1] = _defaultLanguageCode; + entries[1] = summarizeLocale(contextUtils.getLocaleByAndroidCode(_defaultLanguageCode), _defaultLanguageCode); + + setEntries(entries); + setEntryValues(entryval); + } + + // Concat english and localized language name + // Append country if country specific (e.g. Portuguese Brazil) + private String summarizeLocale(final Locale locale, final String localeAndroidCode) { + String country = locale.getDisplayCountry(locale); + String language = locale.getDisplayLanguage(locale); + String ret = locale.getDisplayLanguage(Locale.ENGLISH) + + " (" + language.substring(0, 1).toUpperCase(Locale.getDefault()) + language.substring(1) + + ((!country.isEmpty() && !country.toLowerCase(Locale.getDefault()).equals(language.toLowerCase(Locale.getDefault()))) ? (", " + country) : "") + + ")"; + + if (localeAndroidCode.equals("zh-rCN")) { + ret = ret.substring(0, ret.indexOf(" ") + 1) + "Simplified" + ret.substring(ret.indexOf(" ")); + } else if (localeAndroidCode.equals("zh-rTW")) { + ret = ret.substring(0, ret.indexOf(" ") + 1) + "Traditional" + ret.substring(ret.indexOf(" ")); + } + + return ret; + } + + // Add current language to summary + @Override + public CharSequence getSummary() { + Locale locale = new ContextUtils(getContext()).getLocaleByAndroidCode(getValue()); + String prefix = TextUtils.isEmpty(super.getSummary()) + ? "" : super.getSummary() + "\n\n"; + return prefix + summarizeLocale(locale, getValue()); + } + + public String getSystemLanguageName() { + return _systemLanguageName; + } + + public void setSystemLanguageName(String systemLanguageName) { + _systemLanguageName = systemLanguageName; + loadLangs(getContext()); + } + + public String getDefaultLanguageCode() { + return _defaultLanguageCode; + } + + public void setDefaultLanguageCode(String defaultLanguageCode) { + _defaultLanguageCode = defaultLanguageCode; + loadLangs(getContext()); + } +} 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 new file mode 100644 index 00000000..741b3b76 --- /dev/null +++ b/app/src/main/java/net/gsantner/opoc/util/ActivityUtils.java @@ -0,0 +1,346 @@ +/*####################################################### + * + * Maintained 2016-2023 by Gregor Santner + * + * 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; +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", "rawtypes", "UnusedReturnValue"}) +public class ActivityUtils extends net.gsantner.opoc.util.ContextUtils { + //######################## + //## Members, Constructors + //######################## + protected Activity _activity; + + public ActivityUtils(final Activity activity) { + super(activity); + _activity = activity; + } + + @Override + public void freeContextRef() { + super.freeContextRef(); + _activity = null; + } + + //######################## + //## Methods + //######################## + + /** + * Animate to specified Activity + * + * @param to The class of the activity + * @param finishFromActivity true: Finish the current activity + * @param requestCode Request code for stating the activity, not waiting for result if null + */ + public void animateToActivity(Class to, Boolean finishFromActivity, Integer requestCode) { + animateToActivity(new Intent(_activity, to), finishFromActivity, requestCode); + } + + /** + * Animate to Activity specified in intent + * Requires animation resources + * + * @param intent Intent to open start an activity + * @param finishFromActivity true: Finish the current activity + * @param requestCode Request code for stating the activity, not waiting for result if null + */ + public void animateToActivity(Intent intent, Boolean finishFromActivity, Integer requestCode) { + intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); + if (requestCode != null) { + _activity.startActivityForResult(intent, requestCode); + } else { + _activity.startActivity(intent); + + } + _activity.overridePendingTransition(getResId(ResType.DIMEN, "fadein"), getResId(ResType.DIMEN, "fadeout")); + if (finishFromActivity != null && finishFromActivity) { + _activity.finish(); + } + } + + + 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) { + Snackbar.make(_activity.findViewById(android.R.id.content), stringResId, + showLong ? Snackbar.LENGTH_LONG : Snackbar.LENGTH_SHORT) + .setAction(actionResId, listener) + .show(); + } + + 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 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) { + showDialogWithHtmlTextView(resTitleId, html, true, null); + } + + 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()); + + 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) + .setView(scroll); + if (resTitleId != 0) { + dialog.setTitle(resTitleId); + } + dialogFullWidth(dialog.show(), true, false); + } + + public void showDialogWithRawFileInWebView(String fileInRaw, @StringRes int resTitleId) { + WebView wv = new WebView(_context); + wv.loadUrl("file:///android_res/raw/" + fileInRaw); + AlertDialog.Builder dialog = new AlertDialog.Builder(_context) + .setPositiveButton(android.R.string.ok, null) + .setTitle(resTitleId) + .setView(wv); + dialogFullWidth(dialog.show(), true, false); + } + + // Toggle with no param, else set visibility according to first bool + public ActivityUtils toggleStatusbarVisibility(boolean... optionalForceVisible) { + WindowManager.LayoutParams attrs = _activity.getWindow().getAttributes(); + int flag = WindowManager.LayoutParams.FLAG_FULLSCREEN; + if (optionalForceVisible.length == 0) { + attrs.flags ^= flag; + } else if (optionalForceVisible.length == 1 && optionalForceVisible[0]) { + attrs.flags &= ~flag; + } else { + attrs.flags |= flag; + } + _activity.getWindow().setAttributes(attrs); + return this; + } + + 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 | + (Build.VERSION.SDK_INT >= 21 ? Intent.FLAG_ACTIVITY_NEW_DOCUMENT : Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET) | + Intent.FLAG_ACTIVITY_MULTIPLE_TASK); + try { + _activity.startActivity(goToMarket); + } catch (ActivityNotFoundException e) { + _activity.startActivity(new Intent(Intent.ACTION_VIEW, + 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/io/github/gsantner/opoc/util/AdBlock.java b/app/src/main/java/net/gsantner/opoc/util/AdBlock.java similarity index 55% rename from app/src/main/java/io/github/gsantner/opoc/util/AdBlock.java rename to app/src/main/java/net/gsantner/opoc/util/AdBlock.java index a8a6cc27..c5c81678 100644 --- a/app/src/main/java/io/github/gsantner/opoc/util/AdBlock.java +++ b/app/src/main/java/net/gsantner/opoc/util/AdBlock.java @@ -1,14 +1,12 @@ -/* - * ---------------------------------------------------------------------------- * - * Gregor Santner wrote this file. You can do whatever - * you want with this stuff. If we meet some day, and you think this stuff is - * worth it, you can buy me a coke in return. Provided as is without any kind - * of warranty. No attribution required. - Gregor Santner +/*####################################################### * - * License: Creative Commons Zero (CC0 1.0) - * http://creativecommons.org/publicdomain/zero/1.0/ - * ---------------------------------------------------------------------------- - */ + * Maintained 2017-2023 by Gregor Santner + * + * License: Apache 2.0 + * https://github.com/gsantner/opoc/#licensing + * https://www.apache.org/licenses/LICENSE-2.0 + * +#########################################################*/ /* * Place adblock hosts file in raw: src/main/res/raw/adblock_domains__xyz.txt @@ -24,7 +22,7 @@ public WebResourceResponse shouldInterceptRequest(WebView view, String url) { : super.shouldInterceptRequest(view, url); } */ -package io.github.gsantner.opoc.util; +package net.gsantner.opoc.util; import android.content.Context; import android.util.Log; @@ -47,7 +45,9 @@ import java.util.Set; /** * Simple Host-Based AdBlocker */ +@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() { @@ -59,9 +59,11 @@ public class AdBlock { //## Members //## //######################## - private final Set adblockHostsFromRaw = new HashSet<>(); - private final Set adblockHosts = new HashSet<>(); - private boolean isLoaded; + private final Set _adblockHostsFromRaw = new HashSet<>(); + private final Set _adblockHosts = new HashSet<>(); + private final List> _customBlockCallbacks = new ArrayList<>(); + private boolean _isLoaded = false; + private boolean _isAdblockLogging = false; //######################## //## @@ -72,33 +74,59 @@ 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; } + public boolean isLoaded() { + return _isLoaded; + } + public static WebResourceResponse createEmptyResponse() { 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(); @@ -106,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(); + _isLoaded = true; } + } catch (IOException e) { + e.printStackTrace(); } }).start(); } @@ -131,7 +165,7 @@ public class AdBlock { BufferedReader br = null; String host; - adblockHosts.clear(); + _adblockHosts.clear(); for (int rawId : getAdblockIdsInRaw()) { try { br = new BufferedReader(new InputStreamReader(context.getResources().openRawResource(rawId))); @@ -149,8 +183,8 @@ public class AdBlock { } } } - adblockHostsFromRaw.clear(); - adblockHostsFromRaw.addAll(adblockHosts); + _adblockHostsFromRaw.clear(); + _adblockHostsFromRaw.addAll(_adblockHosts); } private List getAdblockIdsInRaw() { @@ -163,9 +197,22 @@ public class AdBlock { if (resFilename.startsWith("adblock_domains__")) { adblockResIds.add(resId); } - } catch (IllegalAccessException ignored) { + } catch (IllegalAccessException | IllegalArgumentException ignored) { } } 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 new file mode 100644 index 00000000..012cb634 --- /dev/null +++ b/app/src/main/java/net/gsantner/opoc/util/Callback.java @@ -0,0 +1,86 @@ +/*####################################################### + * + * Maintained 2018-2023 by Gregor Santner + * + * 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); + } + + public interface a2 { + void callback(A arg1, B arg2); + } + + public interface a3 { + void callback(A arg1, B arg2, C arg3); + } + + public interface a4 { + void callback(A arg1, B arg2, C arg3, D arg4); + } + + 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 new file mode 100644 index 00000000..483461d8 --- /dev/null +++ b/app/src/main/java/net/gsantner/opoc/util/ContextUtils.java @@ -0,0 +1,1017 @@ +/*####################################################### + * + * Maintained 2016-2023 by Gregor Santner + * + * 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; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.drawable.AdaptiveIconDrawable; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.VectorDrawable; +import android.media.MediaScannerConnection; +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; +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; +import android.text.method.LinkMovementMethod; +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; + +import net.gsantner.opoc.format.markdown.SimpleMarkdownParser; + +import java.io.BufferedReader; +import java.io.File; +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", "TryFinallyCanBeTryWithResources", "UnusedAssignment", "UnusedReturnValue"}) +public class ContextUtils { + // + // Members, Constructors + // + protected Context _context; + + public ContextUtils(Context context) { + _context = context; + } + + public Context context() { + return _context; + } + + public void freeContextRef() { + _context = null; + } + + // + // Class Methods + // + public enum ResType { + ID, BOOL, INTEGER, COLOR, STRING, ARRAY, DRAWABLE, PLURALS, + ANIM, ATTR, DIMEN, LAYOUT, MENU, RAW, STYLE, XML, + } + + /** + * Find out the nuermical ressource id by given {@link ResType} + * + * @return A valid id if the id could be found, else 0 + */ + 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 final int strResId) { + try { + return _context.getString(strResId); + } catch (Exception e) { + return null; + } + } + + /** + * Get String by given string ressource identifier (textual) + */ + public String rstr(final String strResKey, Object... a0getResKeyAsFallback) { + try { + return rstr(getResId(ResType.STRING, strResKey)); + } catch (Resources.NotFoundException e) { + return a0getResKeyAsFallback != null && a0getResKeyAsFallback.length > 0 ? strResKey : null; + } + } + + /** + * Get drawable from given ressource identifier + */ + 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 final int resId) { + if (resId == 0) { + Log.e(getClass().getName(), "ContextUtils::rcolor: resId is 0!"); + return Color.BLACK; + } + return ContextCompat.getColor(_context, resId); + } + + /** + * Checks if all given (textual) ressource ids are available + * + * @param resType A {@link ResType} + * @param resIdsTextual A (textual) identifier to be awaited at R.restype.resIdsTextual + * @return True if all given ids are available + */ + public boolean areRessourcesAvailable(final ResType resType, final String... resIdsTextual) { + for (String name : resIdsTextual) { + if (getResId(resType, name) == 0) { + return false; + } + } + return true; + } + + /** + * Convert an int color to a hex string. Optionally including alpha value. + * + * @param intColor The color coded in int + * @param withAlpha Optional; Set first bool parameter to true to also include alpha value + */ + 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 { + PackageInfo info = manager.getPackageInfo(getPackageIdManifest(), 0); + return info.versionName; + } catch (PackageManager.NameNotFoundException e) { + 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 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 (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(); + } + + /** + * Get field from ${applicationId}.BuildConfig + * May be helpful in libraries, where a access to + * BuildConfig would only get values of the library + * rather than the app ones. It awaits a string resource + * of the package set in manifest (root element). + * Falls back to applicationId of the app which may differ from manifest. + */ + 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; + } + + 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(final String fieldName, final Boolean defaultValue) { + Object field = getBuildConfigValue(fieldName); + if (field instanceof Boolean) { + return (Boolean) field; + } + return defaultValue; + } + + /** + * Get a BuildConfig string value + */ + public String bcstr(final String fieldName, final String defaultValue) { + Object field = getBuildConfigValue(fieldName); + 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) + */ + public boolean isGooglePlayBuild() { + return bcbool("IS_GPLAY_BUILD", true); + } + + /** + * Check if this is a foss build (requires BuildConfig field) + */ + public boolean isFossBuild() { + return bcbool("IS_FOSS_BUILD", false); + } + + public String readTextfileFromRawRes(@RawRes int rawResId, String linePrefix, String linePostfix) { + StringBuilder sb = new StringBuilder(); + BufferedReader br = null; + String line; + + linePrefix = linePrefix == null ? "" : linePrefix; + linePostfix = linePostfix == null ? "" : linePostfix; + + try { + br = new BufferedReader(new InputStreamReader(_context.getResources().openRawResource(rawResId))); + while ((line = br.readLine()) != null) { + sb.append(linePrefix); + sb.append(line); + sb.append(linePostfix); + sb.append("\n"); + } + } catch (Exception ignored) { + } finally { + if (br != null) { + try { + br.close(); + } catch (IOException ignored) { + } + } + } + return sb.toString(); + } + + /** + * Get internet connection state - the permission ACCESS_NETWORK_STATE is required + * + * @return True if internet connection available + */ + public boolean isConnectedToInternet() { + try { + ConnectivityManager con = (ConnectivityManager) _context.getSystemService(Context.CONNECTIVITY_SERVICE); + @SuppressLint("MissingPermission") NetworkInfo activeNetInfo = + con == null ? null : con.getActiveNetworkInfo(); + return activeNetInfo != null && activeNetInfo.isConnectedOrConnecting(); + } catch (Exception ignored) { + throw new RuntimeException("Error: Developer forgot to declare a permission"); + } + } + + /** + * Check if app with given {@code packageName} is installed + */ + public boolean isAppInstalled(String packageName) { + try { + PackageManager pm = _context.getApplicationContext().getPackageManager(); + pm.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES); + return true; + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } + + /** + * Restart the current app. Supply the class to start on startup + */ + public void restartApp(Class classToStart) { + 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, pendi); + } else { + intent.addFlags(FLAG_ACTIVITY_NEW_TASK); + _context.startActivity(intent); + } + Runtime.getRuntime().exit(0); + } + + /** + * Load a markdown file from a {@link RawRes}, prepend each line with {@code prepend} text + * and convert markdown to html using {@link SimpleMarkdownParser} + */ + public String loadMarkdownForTextViewFromRaw(@RawRes int rawMdFile, String prepend) { + try { + return new SimpleMarkdownParser() + .parse(_context.getResources().openRawResource(rawMdFile), + prepend, SimpleMarkdownParser.FILTER_ANDROID_TEXTVIEW) + .replaceColor("#000001", rcolor(getResId(ResType.COLOR, "accent"))) + .removeMultiNewlines().replaceBulletCharacter("*").getHtml(); + } catch (IOException e) { + e.printStackTrace(); + return ""; + } + } + + /** + * Load html into a {@link Spanned} object and set the + * {@link TextView}'s text using {@link TextView#setText(CharSequence)} + */ + public void setHtmlToTextView(TextView textView, String html) { + textView.setMovementMethod(LinkMovementMethod.getInstance()); + textView.setText(new SpannableString(htmlToSpanned(html))); + } + + /** + * Estimate this device's screen diagonal size in inches + */ + public double getEstimatedScreenSizeInches() { + DisplayMetrics dm = _context.getResources().getDisplayMetrics(); + + double calc = dm.density * 160d; + double x = Math.pow(dm.widthPixels / calc, 2); + double y = Math.pow(dm.heightPixels / calc, 2); + calc = Math.sqrt(x + y) * 1.16; // 1.16 = est. Nav/Statusbar + return Math.min(12, Math.max(4, calc)); + } + + /** + * Check if the device is currently in portrait orientation + */ + public boolean isInPortraitMode() { + return _context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT; + } + + /** + * Get an {@link Locale} out of a android language code + * The {@code androidLC} may be in any of the forms: de, en, de-rAt + */ + public Locale getLocaleByAndroidCode(String androidLC) { + if (!TextUtils.isEmpty(androidLC)) { + return androidLC.contains("-r") + ? new Locale(androidLC.substring(0, 2), androidLC.substring(4, 6)) // de-rAt + : new Locale(androidLC); // de + } + return Resources.getSystem().getConfiguration().locale; + } + + /** + * Set the apps language + * {@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(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 ? 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 final int colorOnBottomInt) { + return 186 > (((0.299 * Color.red(colorOnBottomInt)) + + ((0.587 * Color.green(colorOnBottomInt)) + + (0.114 * Color.blue(colorOnBottomInt))))); + } + + /** + * Convert a html string to an android {@link Spanned} object + */ + 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); + } else { + result = Html.fromHtml(html); + } + return result; + } + + /** + * Convert pixel unit do android dp unit + */ + public float convertPxToDp(final float px) { + return px / _context.getResources().getDisplayMetrics().density; + } + + /** + * Convert android dp unit to pixel unit + */ + public float convertDpToPx(final float dp) { + 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(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++) { + paths[i] = files[i].getAbsolutePath(); + } + MediaScannerConnection.scanFile(_context, paths, null, null); + } else { + for (File file : files) { + _context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(file))); + } + } + } + + /** + * Load an image into a {@link ImageView} and apply a color filter + */ + public static void setDrawableWithColorToImageView(ImageView imageView, @DrawableRes int drawableResId, @ColorRes int colorResId) { + imageView.setImageResource(drawableResId); + imageView.setColorFilter(ContextCompat.getColor(imageView.getContext(), colorResId)); + } + + /** + * Get a {@link Bitmap} out of a {@link Drawable} + */ + public Bitmap drawableToBitmap(Drawable drawable) { + Bitmap bitmap = null; + if (drawable instanceof VectorDrawableCompat + || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && drawable instanceof VectorDrawable) + || ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && drawable instanceof AdaptiveIconDrawable))) { + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + drawable = (DrawableCompat.wrap(drawable)).mutate(); + } + + bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), + drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + drawable.draw(canvas); + } else if (drawable instanceof BitmapDrawable) { + bitmap = ((BitmapDrawable) drawable).getBitmap(); + } + return bitmap; + } + + /** + * Get a {@link Bitmap} out of a {@link DrawableRes} + */ + public Bitmap drawableToBitmap(@DrawableRes final int drawableId) { + try { + return drawableToBitmap(ContextCompat.getDrawable(_context, drawableId)); + } catch (Exception e) { + return null; + } + } + + /** + * Get a {@link Bitmap} from a given {@code imagePath} on the filesystem + * Specifying a {@code maxDimen} is also possible and a value below 2000 + * is recommended, otherwise a {@link OutOfMemoryError} may occur + */ + public Bitmap loadImageFromFilesystem(final File imagePath, final int maxDimen) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(imagePath.getAbsolutePath(), options); + options.inSampleSize = calculateInSampleSize(options, maxDimen); + options.inJustDecodeBounds = false; + return BitmapFactory.decodeFile(imagePath.getAbsolutePath(), options); + } + + /** + * Calculates the scaling factor so the bitmap is maximal as big as the maxDimen + * + * @param options Bitmap-options that contain the current dimensions of the bitmap + * @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(final BitmapFactory.Options options, final int maxDimen) { + // Raw height and width of image + int height = options.outHeight; + int width = options.outWidth; + int inSampleSize = 1; + + if (Math.max(height, width) > maxDimen) { + inSampleSize = Math.round(1f * Math.max(height, width) / maxDimen); + } + return inSampleSize; + } + + /** + * Scale the bitmap so both dimensions are lower or equal to {@code maxDimen} + * This keeps the aspect ratio + */ + 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(); + matrix.postScale(scale, scale); + return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); + } + + /** + * Write the given {@link Bitmap} to filesystem + * + * @param targetFile The file to be written in + * @param image Android {@link Bitmap} + * @return True if writing was successful + */ + 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 (folder.exists() || folder.mkdirs()) { + FileOutputStream stream = null; + try { + stream = new FileOutputStream(targetFile); + image.compress(format, quality, stream); + ok = true; + } catch (Exception ignored) { + } finally { + try { + if (stream != null) { + stream.close(); + } + } catch (IOException ignored) { + } + } + } + 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 final int drawableRes, final String text, final int textSize) { + Resources resources = _context.getResources(); + float scale = resources.getDisplayMetrics().density; + Bitmap bitmap = drawableToBitmap(drawableRes); + + bitmap = bitmap.copy(bitmap.getConfig(), true); + Canvas canvas = new Canvas(bitmap); + Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + paint.setColor(Color.rgb(61, 61, 61)); + paint.setTextSize((int) (textSize * scale)); + paint.setShadowLayer(1f, 0f, 1f, Color.WHITE); + + Rect bounds = new Rect(); + paint.getTextBounds(text, 0, text.length(), bounds); + int x = (bitmap.getWidth() - bounds.width()) / 2; + int y = (bitmap.getHeight() + bounds.height()) / 2; + canvas.drawText(text, x, y, paint); + + return bitmap; + } + + /** + * Try to tint all {@link Menu}s {@link MenuItem}s with given color + */ + @SuppressWarnings("ConstantConditions") + 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); + 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 + } + } + } + + /** + * Loads {@link Drawable} by given {@link DrawableRes} and applies a 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 final int color) { + if (drawable != null) { + drawable = DrawableCompat.wrap(drawable); + DrawableCompat.setTint(drawable.mutate(), color); + } + return drawable; + } + + /** + * 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(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); + m.setAccessible(true); + m.invoke(menu, visible); + } catch (Exception ignored) { + Log.d(getClass().getName(), "Error: 'setSubMenuIconsVisiblity' not supported on this device"); + } + } + } + + + 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/DownloadTask.java b/app/src/main/java/net/gsantner/opoc/util/DownloadTask.java new file mode 100644 index 00000000..aec5b868 --- /dev/null +++ b/app/src/main/java/net/gsantner/opoc/util/DownloadTask.java @@ -0,0 +1,57 @@ +/* + 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 net.gsantner.opoc.util; + +import android.os.AsyncTask; +import android.support.annotation.Nullable; + +import java.io.File; +import java.io.IOException; + +import javax.net.ssl.HttpsURLConnection; + +import info.guardianproject.netcipher.NetCipher; + +public class DownloadTask extends AsyncTask { + private File _targetFile; + private Callback.a2 _callback; + + public DownloadTask(File targetFile, @Nullable Callback.a2 callback) { + _targetFile = targetFile; + _callback = callback; + } + + protected Boolean doInBackground(String... urls) { + if (urls != null && urls.length > 0 && urls[0] != null) { + try { + HttpsURLConnection connection = NetCipher.getHttpsURLConnection(urls[0]); + return NetworkUtils.downloadFile(null, _targetFile, connection, null); + } catch (IOException e) { + e.printStackTrace(); + } + } + return false; + } + + protected void onPostExecute(Boolean result) { + if (_callback != null) { + _callback.callback(result, _targetFile); + } + } +} diff --git a/app/src/main/java/net/gsantner/opoc/util/FileUtils.java b/app/src/main/java/net/gsantner/opoc/util/FileUtils.java new file mode 100644 index 00000000..7dbd1658 --- /dev/null +++ b/app/src/main/java/net/gsantner/opoc/util/FileUtils.java @@ -0,0 +1,527 @@ +/*####################################################### + * + * Maintained 2017-2023 by Gregor Santner + * + * 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", "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)); + } catch (FileNotFoundException e) { + System.err.println("readTextFile: File " + file + " not found."); + } + + return ""; + } + + public static String readCloseTextStream(final InputStream stream) { + return readCloseTextStream(stream, true).get(0); + } + + public static List readCloseTextStream(final InputStream stream, boolean concatToOneString) { + final ArrayList lines = new ArrayList<>(); + BufferedReader reader = null; + String line = ""; + try { + StringBuilder sb = new StringBuilder(); + reader = new BufferedReader(new InputStreamReader(stream)); + + while ((line = reader.readLine()) != null) { + if (concatToOneString) { + sb.append(line).append('\n'); + } else { + lines.add(line); + } + } + line = sb.toString(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + if (concatToOneString) { + lines.clear(); + lines.add(line); + } + return lines; + } + + public static byte[] readBinaryFile(final File file) { + try { + return readCloseBinaryStream(new FileInputStream(file), (int) file.length()); + } catch (FileNotFoundException e) { + System.err.println("readBinaryFile: File " + file + " not found."); + } + + return new byte[0]; + } + + public static byte[] readCloseBinaryStream(final InputStream stream, int byteCount) { + final ArrayList lines = new ArrayList<>(); + BufferedInputStream reader = null; + byte[] buf = new byte[byteCount]; + int totalBytesRead = 0; + try { + reader = new BufferedInputStream(stream); + while (totalBytesRead < byteCount) { + int bytesRead = reader.read(buf, totalBytesRead, byteCount - totalBytesRead); + if (bytesRead > 0) { + totalBytesRead = totalBytesRead + bytesRead; + } + } + } catch (IOException e) { + e.printStackTrace(); + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + return buf; + } + + // Read binary stream (of unknown conf size) + public static byte[] readCloseBinaryStream(final InputStream stream) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + byte[] buffer = new byte[BUFFER_SIZE]; + int read; + while ((read = stream.read(buffer)) != -1) { + baos.write(buffer, 0, read); + } + } catch (IOException e) { + e.printStackTrace(); + } finally { + if (stream != null) { + try { + stream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + return baos.toByteArray(); + } + + public static boolean writeFile(final File file, byte[] data) { + try { + OutputStream output = null; + try { + output = new BufferedOutputStream(new FileOutputStream(file)); + output.write(data); + output.flush(); + return true; + } finally { + if (output != null) { + output.close(); + } + } + } catch (Exception ex) { + return false; + } + } + + public static boolean writeFile(final File file, final String content) { + BufferedWriter writer = null; + try { + if (!file.getParentFile().isDirectory() && !file.getParentFile().mkdirs()) + return false; + + writer = new BufferedWriter(new FileWriter(file)); + writer.write(content); + writer.flush(); + return true; + } catch (IOException e) { + e.printStackTrace(); + return false; + } finally { + if (writer != null) { + try { + writer.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + + public static boolean copyFile(final File src, final File dst) { + // Just touch file if src is empty + if (src.length() == 0) { + return touch(dst); + } + + InputStream is = null; + FileOutputStream os = null; + try { + try { + is = new FileInputStream(src); + os = new FileOutputStream(dst); + 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; + } + } + + 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. + // + // Needless MUST be in lower-case. + public static int fileContains(File file, String... needles) { + try { + FileInputStream in = new FileInputStream(file); + + int i; + String line; + BufferedReader reader = new BufferedReader(new InputStreamReader(in)); + while ((line = reader.readLine()) != null) { + for (i = 0; i != needles.length; ++i) + if (line.toLowerCase(Locale.ROOT).contains(needles[i])) { + return i; + } + } + + in.close(); + } catch (IOException e) { + e.printStackTrace(); + } + + return -1; + } + + public static boolean deleteRecursive(final File file) { + boolean ok = true; + if (file.exists()) { + if (file.isDirectory()) { + for (File child : file.listFiles()) + ok &= deleteRecursive(child); + } + ok &= file.delete(); + } + return ok; + } + + // Example: Check if this is maybe a conf: (str, "jpg", "png", "jpeg") + public static boolean hasExtension(String str, String... extensions) { + String lc = str.toLowerCase(Locale.ROOT); + for (String extension : extensions) { + if (lc.endsWith("." + extension.toLowerCase(Locale.ROOT))) { + return true; + } + } + return false; + } + + public static boolean renameFile(File srcFile, File destFile) { + if (srcFile.getAbsolutePath().equals(destFile.getAbsolutePath())) { + return false; + } + + // renameTo will fail in case of case-changed filename in same dir.Even on case-sensitive FS!!! + if (srcFile.getParent().equals(destFile.getParent()) && srcFile.getName().toLowerCase(Locale.getDefault()).equals(destFile.getName().toLowerCase(Locale.getDefault()))) { + File tmpFile = new File(destFile.getParent(), UUID.randomUUID().getLeastSignificantBits() + ".tmp"); + if (!tmpFile.exists()) { + renameFile(srcFile, tmpFile); + srcFile = tmpFile; + } + } + + if (!srcFile.renameTo(destFile)) { + if (copyFile(srcFile, destFile) && !srcFile.delete()) { + if (!destFile.delete()) { + return false; + } + return false; + } + } + return true; + } + + @SuppressWarnings("ResultOfMethodCallIgnored") + public static boolean renameFileInSameFolder(File srcFile, String destFilename) { + return renameFile(srcFile, new File(srcFile.getParent(), destFilename)); + } + + public static boolean touch(File file) { + try { + if (!file.exists()) { + new FileOutputStream(file).close(); + } + return file.setLastModified(System.currentTimeMillis()); + } catch (IOException e) { + return false; + } + } + + // Get relative path to specified destination + public static String relativePath(File src, File dest) { + try { + String[] srcSplit = (src.isDirectory() ? src : src.getParentFile()).getCanonicalPath().split(Pattern.quote(File.separator)); + String[] destSplit = dest.getCanonicalPath().split(Pattern.quote(File.separator)); + StringBuilder sb = new StringBuilder(); + int i = 0; + + for (; i < destSplit.length && i < srcSplit.length; ++i) { + if (!destSplit[i].equals(srcSplit[i])) + break; + } + if (i != srcSplit.length) { + for (int iUpperDir = i; iUpperDir < srcSplit.length; ++iUpperDir) { + sb.append(".."); + sb.append(File.separator); + } + } + for (; i < destSplit.length; ++i) { + sb.append(destSplit[i]); + sb.append(File.separator); + } + if (!dest.getPath().endsWith("/") && !dest.getPath().endsWith("\\")) { + sb.delete(sb.length() - File.separator.length(), sb.length()); + } + return sb.toString(); + } catch (IOException | NullPointerException exception) { + 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 new file mode 100644 index 00000000..dc06674e --- /dev/null +++ b/app/src/main/java/net/gsantner/opoc/util/NetworkUtils.java @@ -0,0 +1,233 @@ +/*####################################################### + * + * Maintained 2017-2023 by Gregor Santner + * + * 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 org.json.JSONObject; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Map; + +@SuppressWarnings({"WeakerAccess", "unused", "SameParameterValue", "SpellCheckingInspection", "deprecation"}) +public class NetworkUtils { + private static final String UTF8 = "UTF-8"; + public static final String GET = "GET"; + public static final String POST = "POST"; + public static final String PATCH = "PATCH"; + + private final static int BUFFER_SIZE = 4096; + + // Downloads a file from the give url to the output file + // Creates the file's parent directory if it doesn't exist + public static boolean downloadFile(final String url, final File out) { + return downloadFile(url, out, null); + } + + public static boolean downloadFile(final String url, final File out, final Callback.a1 progressCallback) { + try { + return downloadFile(new URL(url), out, progressCallback); + } catch (MalformedURLException e) { + // Won't happen + e.printStackTrace(); + return false; + } + } + + public static boolean downloadFile(final URL url, final File outFile, final Callback.a1 progressCallback) { + return downloadFile(url, outFile, null, progressCallback); + } + + public static boolean downloadFile(final URL url, final File outFile, HttpURLConnection connection, final Callback.a1 progressCallback) { + InputStream input = null; + OutputStream output = null; + try { + if (connection == null) { + connection = (HttpURLConnection) url.openConnection(); + } + connection.connect(); + input = connection.getResponseCode() < HttpURLConnection.HTTP_BAD_REQUEST + ? connection.getInputStream() : connection.getErrorStream(); + + + if (!outFile.getParentFile().isDirectory()) + if (!outFile.getParentFile().mkdirs()) + return false; + output = new FileOutputStream(outFile); + + int count; + int written = 0; + final float invLength = 1f / connection.getContentLength(); + + byte[] data = new byte[BUFFER_SIZE]; + while ((count = input.read(data)) != -1) { + output.write(data, 0, count); + if (invLength != -1f && progressCallback != null) { + written += count; + progressCallback.callback(written * invLength); + } + } + + return true; + } catch (IOException e) { + e.printStackTrace(); + return false; + } finally { + try { + if (output != null) + output.close(); + if (input != null) + input.close(); + } catch (IOException ignored) { + } + if (connection != null) + connection.disconnect(); + } + } + + // No parameters, method can be GET, POST, etc. + public static String performCall(final String url, final String method) { + try { + return performCall(new URL(url), method, ""); + } catch (MalformedURLException e) { + e.printStackTrace(); + } + return ""; + } + + public static String performCall(final String url, final String method, final String data) { + try { + return performCall(new URL(url), method, data); + } catch (MalformedURLException e) { + e.printStackTrace(); + } + return ""; + } + + // URL encoded parameters + public static String performCall(final String url, final String method, final HashMap params) { + try { + return performCall(new URL(url), method, encodeQuery(params)); + } catch (UnsupportedEncodingException | MalformedURLException e) { + e.printStackTrace(); + } + return ""; + } + + // Defaults to POST + public static String performCall(final String url, final JSONObject json) { + return performCall(url, POST, json); + } + + public static String performCall(final String url, final String method, final JSONObject json) { + try { + return performCall(new URL(url), method, json.toString()); + } catch (MalformedURLException e) { + e.printStackTrace(); + } + return ""; + } + + private static String performCall(final URL url, final String method, final String data) { + 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 + ? existingConnection : (HttpURLConnection) url.openConnection(); + connection.setRequestMethod(method); + connection.setDoInput(true); + + if (data != null && !data.isEmpty()) { + connection.setDoOutput(true); + final OutputStream output = connection.getOutputStream(); + output.write(data.getBytes(Charset.forName("UTF-8"))); + output.flush(); + output.close(); + } + + InputStream input = connection.getResponseCode() < HttpURLConnection.HTTP_BAD_REQUEST + ? connection.getInputStream() : connection.getErrorStream(); + + return FileUtils.readCloseTextStream(connection.getInputStream()); + } catch (Exception e) { + e.printStackTrace(); + } + return ""; + } + + private static String encodeQuery(final HashMap params) throws UnsupportedEncodingException { + final StringBuilder result = new StringBuilder(); + boolean first = true; + for (Map.Entry entry : params.entrySet()) { + if (first) first = false; + else result.append("&"); + + result.append(URLEncoder.encode(entry.getKey(), UTF8)); + result.append("="); + result.append(URLEncoder.encode(entry.getValue(), UTF8)); + } + + return result.toString(); + } + + public static HashMap getDataMap(final String query) { + final HashMap result = new HashMap<>(); + final StringBuilder sb = new StringBuilder(); + String name = ""; + + try { + for (int i = 0; i < query.length(); i++) { + char c = query.charAt(i); + switch (c) { + case '=': + name = URLDecoder.decode(sb.toString(), UTF8); + sb.setLength(0); + break; + case '&': + result.put(name, URLDecoder.decode(sb.toString(), UTF8)); + sb.setLength(0); + break; + default: + sb.append(c); + break; + } + } + if (!name.isEmpty()) + result.put(name, URLDecoder.decode(sb.toString(), UTF8)); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + + 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 new file mode 100644 index 00000000..be156745 --- /dev/null +++ b/app/src/main/java/net/gsantner/opoc/util/PermissionChecker.java @@ -0,0 +1,69 @@ +/*####################################################### + * + * Maintained 2017-2023 by Gregor Santner + * + * 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.app.Activity; +import android.content.pm.PackageManager; +import android.support.v4.app.ActivityCompat; +import android.support.v4.content.ContextCompat; +import android.support.v7.app.AlertDialog; + +import java.io.File; + +@SuppressWarnings({"unused", "WeakerAccess"}) +public class PermissionChecker { + protected static final int CODE_PERMISSION_EXTERNAL_STORAGE = 4000; + + protected Activity _activity; + + public PermissionChecker(Activity activity) { + _activity = activity; + } + + public boolean doIfExtStoragePermissionGranted(String... optionalToastMessageForKnowingWhyNeeded) { + if (ContextCompat.checkSelfPermission(_activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + + if (optionalToastMessageForKnowingWhyNeeded != null && optionalToastMessageForKnowingWhyNeeded.length > 0 && optionalToastMessageForKnowingWhyNeeded[0] != null) { + new AlertDialog.Builder(_activity) + .setMessage(optionalToastMessageForKnowingWhyNeeded[0]) + .setCancelable(false) + .setNegativeButton(android.R.string.no, null) + .setPositiveButton(android.R.string.yes, (dialog, which) -> { + if (android.os.Build.VERSION.SDK_INT >= 23) { + ActivityCompat.requestPermissions(_activity, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, CODE_PERMISSION_EXTERNAL_STORAGE); + } + }) + .show(); + return false; + } + ActivityCompat.requestPermissions(_activity, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, CODE_PERMISSION_EXTERNAL_STORAGE); + return false; + } + return true; + } + + public boolean checkPermissionResult(int requestCode, String[] permissions, int[] grantResults) { + if (grantResults.length > 0) { + switch (requestCode) { + case CODE_PERMISSION_EXTERNAL_STORAGE: { + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + return true; + } + } + } + } + return false; + } + + public boolean mkdirIfStoragePermissionGranted(File dir) { + return doIfExtStoragePermissionGranted() && (dir.exists() || dir.mkdirs()); + } +} diff --git a/app/src/main/java/net/gsantner/opoc/util/ShareUtil.java b/app/src/main/java/net/gsantner/opoc/util/ShareUtil.java new file mode 100644 index 00000000..dc467500 --- /dev/null +++ b/app/src/main/java/net/gsantner/opoc/util/ShareUtil.java @@ -0,0 +1,1215 @@ +/*####################################################### + * + * Maintained 2017-2023 by Gregor Santner + * + * 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; +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. + * (M)Permissions are not checked, wrap ShareUtils methods if neccessary + */ +@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-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 _chooserTitle; + + 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"); + } + return _fileProviderAuthority; + } + + public static void setFileProviderAuthority(final String fileProviderAuthority) { + _fileProviderAuthority = fileProviderAuthority; + } + + + public ShareUtil setChooserTitle(final String title) { + _chooserTitle = title; + return this; + } + + /** + * Convert a {@link File} to an {@link Uri} + * + * @param file the file + * @return Uri for this file + */ + public Uri getUriByFileProviderAuthority(final File file) { + return FileProvider.getUriForFile(_context, getFileProviderAuthority(), file); + } + + /** + * Allow to choose a handling app for given intent + * + * @param intent Thing to be shared + * @param chooserText The title text for the chooser, or null for default + */ + public void showChooser(final Intent intent, final String chooserText) { + try { + _context.startActivity(Intent.createChooser(intent, chooserText != null ? chooserText : _chooserTitle)); + } catch (Exception ignored) { + } + } + + /** + * Try to create a new desktop shortcut on the launcher. Add permissions: + * + * + * + * @param intent The intent to be invoked on tap + * @param iconRes Icon resource for the item + * @param title Title of the item + */ + 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) { + intent.setAction(Intent.ACTION_VIEW); + } + + ShortcutInfoCompat shortcut = new ShortcutInfoCompat.Builder(_context, Long.toString(new Random().nextLong())) + .setIntent(intent) + .setIcon(IconCompat.createWithResource(_context, iconRes)) + .setShortLabel(title) + .setLongLabel(title) + .build(); + ShortcutManagerCompat.requestPinShortcut(_context, shortcut, null); + } + + /** + * Try to create a new desktop shortcut on the launcher. This will not work on Api > 25. Add permissions: + * + * + * + * @param intent The intent to be invoked on tap + * @param iconRes Icon resource for the item + * @param title Title of the item + */ + 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) { + intent.setAction(Intent.ACTION_VIEW); + } + + Intent creationIntent = new Intent("com.android.launcher.action.INSTALL_SHORTCUT"); + creationIntent.putExtra("duplicate", true); + creationIntent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, intent); + creationIntent.putExtra(Intent.EXTRA_SHORTCUT_NAME, title); + creationIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, Intent.ShortcutIconResource.fromContext(_context, iconRes)); + _context.sendBroadcast(creationIntent); + } + + /** + * Share text with given mime-type + * + * @param text The text to share + * @param mimeType MimeType or null (uses text/plain) + */ + 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 : MIME_TEXT_PLAIN); + showChooser(intent, null); + } + + /** + * Share the given file as stream with given mime-type + * + * @param file The file to share + * @param mimeType The files mime type + */ + public boolean shareStream(final File file, final String mimeType) { + Intent intent = new Intent(Intent.ACTION_SEND); + intent.putExtra(EXTRA_FILEPATH, file.getAbsolutePath()); + intent.setType(mimeType); + + 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 files as stream with given mime-type + * + * @param files The files to share + * @param mimeType The files mime type. Usally * / * is the best option + */ + 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; + } + + /** + * Share the given bitmap with given format + * + * @param bitmap Image + * @param format A {@link Bitmap.CompressFormat}, supporting JPEG,PNG,WEBP + * @param imageName Filename without extension + * @param quality Quality of the exported image [0-100] + * @return if success, true + */ + public boolean shareImage(final Bitmap bitmap, final Integer... quality) { + try { + 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 (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 + * + * @param webview WebView + * @param jobName Name of the job (affects PDF name too) + * @return {{@link PrintJob}} or null + */ + @RequiresApi(api = Build.VERSION_CODES.KITKAT) + public PrintJob print(final WebView webview, final String jobName, final boolean... landscape) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + 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) { + 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"); + } + return null; + } + + + /** + * See {@link #print(WebView, String) print method} + */ + @RequiresApi(api = Build.VERSION_CODES.KITKAT) + public PrintJob createPdf(final WebView webview, final String jobName) { + return print(webview, jobName); + } + + + /** + * Create a picture out of {@link WebView}'s whole content + * + * @param webView The WebView to get contents from + * @return A {@link Bitmap} or null + */ + @Nullable + public static Bitmap getBitmapFromWebView(final WebView webView, final boolean... a0fullpage) { + try { + //Measure WebView's content + 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(); + + //Creates the bitmap and draw WebView's content on in + 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()); + + webView.draw(canvas); + webView.destroyDrawingCache(); + + return bitmap; + } catch (Exception | OutOfMemoryError e) { + e.printStackTrace(); + return null; + } + } + + + /*** + * Replace (primary) clipboard contents with given {@code text} + * @param text Text to be set + */ + 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) { + cm.setText(text); + return true; + } + } else { + android.content.ClipboardManager cm = ((android.content.ClipboardManager) _context.getSystemService(Context.CLIPBOARD_SERVICE)); + if (cm != null) { + ClipData clip = ClipData.newPlainText(_context.getPackageName(), text); + try { + cm.setPrimaryClip(clip); + } catch (Exception ignored) { + } + return true; + } + } + return false; + } + + /** + * Get clipboard contents, very failsafe and compat to older android versions + */ + public List getClipboard() { + List clipper = new ArrayList<>(); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { + android.text.ClipboardManager cm = ((android.text.ClipboardManager) _context.getSystemService(Context.CLIPBOARD_SERVICE)); + if (cm != null && !TextUtils.isEmpty(cm.getText())) { + clipper.add(cm.getText().toString()); + } + } else { + android.content.ClipboardManager cm = ((android.content.ClipboardManager) _context.getSystemService(Context.CLIPBOARD_SERVICE)); + if (cm != null && cm.hasPrimaryClip()) { + ClipData data = cm.getPrimaryClip(); + for (int i = 0; data != null && i < data.getItemCount() && i < data.getItemCount(); i++) { + ClipData.Item item = data.getItemAt(i); + if (item != null && !TextUtils.isEmpty(item.getText())) { + clipper.add(data.getItemAt(i).getText().toString()); + } + } + } + } + return clipper; + } + + /** + * Share given text on a hastebin compatible server + * (https://github.com/seejohnrun/haste-server) + * Permission needed: Internet + * Pastes will be deleted after 30 days without access + * + * @param text The text to paste + * @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, final String... serverOrNothing) { + final Handler handler = new Handler(); + final String server = (serverOrNothing != null && serverOrNothing.length > 0 && serverOrNothing[0] != null) + ? serverOrNothing[0] : "https://hastebin.com"; + new Thread() { + public void run() { + // Returns a simple result, handleable without json parser {"key":"feediyujiq"} + String ret = NetworkUtils.performCall(server + "/documents", NetworkUtils.POST, text); + final String key = (ret.length() > 15) ? ret.split("\"")[3] : ""; + handler.post(() -> callback.callback(!key.isEmpty(), server + "/" + key)); + } + }.start(); + } + + /** + * Draft an email with given data. Unknown data can be supplied as null. + * This will open a chooser with installed mail clients where the mail can be sent from + * + * @param subject Subject (top/title) text to be prefilled in the mail + * @param body Body (content) text to be prefilled in the mail + * @param to recipients to be prefilled in the mail + */ + 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) { + intent.putExtra(Intent.EXTRA_SUBJECT, subject); + } + if (body != null) { + intent.putExtra(Intent.EXTRA_TEXT, body); + } + if (to != null && to.length > 0 && to[0] != null) { + intent.putExtra(Intent.EXTRA_EMAIL, to); + } + showChooser(intent, null); + } + + /** + * Try to force extract a absolute filepath from an intent + * + * @param receivingIntent The intent from {@link Activity#getIntent()} + * @return A file or null if extraction did not succeed + */ + 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)) || Intent.ACTION_SEND.equals(action)) { + // Markor, S.M.T FileManager + if (receivingIntent.hasExtra((tmps = EXTRA_FILEPATH))) { + return new File(receivingIntent.getStringExtra(tmps)); + } + + // Analyze data/Uri + Uri fileUri = receivingIntent.getData(); + if (fileUri != null && (fileStr = fileUri.toString()) != null) { + // Uri contains file + if (fileStr.startsWith("file://")) { + return new File(fileUri.getPath()); + } + if (fileStr.startsWith((tmps = "content://"))) { + fileStr = fileStr.substring(tmps.length()); + String fileProvider = fileStr.substring(0, fileStr.indexOf("/")); + fileStr = fileStr.substring(fileProvider.length() + 1); + + // Some file managers dont add leading slash + if (fileStr.startsWith("storage/")) { + fileStr = "/" + fileStr; + } + // Some do add some custom prefix + 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/")) { + return new File(Uri.decode("/storage/" + fileStr.substring(tmps.length()))); + } + } + // AOSP File Manager/Documents + if (fileProvider.equals("com.android.externalstorage.documents") && fileStr.startsWith(tmps = "/primary%3A")) { + return new File(Uri.decode(Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + fileStr.substring(tmps.length()))); + } + // Mi File Explorer + 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/anim/fadein.xml b/app/src/main/res/anim/fadein.xml deleted file mode 100644 index fd0e7dd8..00000000 --- a/app/src/main/res/anim/fadein.xml +++ /dev/null @@ -1,4 +0,0 @@ - - \ No newline at end of file diff --git a/app/src/main/res/anim/fadeout.xml b/app/src/main/res/anim/fadeout.xml deleted file mode 100644 index 7134515f..00000000 --- a/app/src/main/res/anim/fadeout.xml +++ /dev/null @@ -1,4 +0,0 @@ - - \ No newline at end of file diff --git a/app/src/main/res/drawable-anydpi-v26/ic_launcher.xml b/app/src/main/res/drawable-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..bbd3e021 --- /dev/null +++ b/app/src/main/res/drawable-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/drawable-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 00000000..bbd3e021 --- /dev/null +++ b/app/src/main/res/drawable-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi/ic_launcher.png b/app/src/main/res/drawable-hdpi/ic_launcher.png index 4e38e5c2..6f647c49 100644 Binary files a/app/src/main/res/drawable-hdpi/ic_launcher.png and b/app/src/main/res/drawable-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_launcher_round.png b/app/src/main/res/drawable-hdpi/ic_launcher_round.png new file mode 100644 index 00000000..6f647c49 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_launcher_test.png b/app/src/main/res/drawable-hdpi/ic_launcher_test.png deleted file mode 100644 index 2e1ab3f5..00000000 Binary files a/app/src/main/res/drawable-hdpi/ic_launcher_test.png and /dev/null differ 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-mdpi/ic_launcher.png b/app/src/main/res/drawable-mdpi/ic_launcher.png index 375b47d7..a9db7c16 100644 Binary files a/app/src/main/res/drawable-mdpi/ic_launcher.png and b/app/src/main/res/drawable-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_launcher_round.png b/app/src/main/res/drawable-mdpi/ic_launcher_round.png new file mode 100644 index 00000000..a9db7c16 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_launcher_test.png b/app/src/main/res/drawable-mdpi/ic_launcher_test.png deleted file mode 100644 index 7f0dec56..00000000 Binary files a/app/src/main/res/drawable-mdpi/ic_launcher_test.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_launcher.png b/app/src/main/res/drawable-xhdpi/ic_launcher.png index 16dc7a29..c97e5b53 100644 Binary files a/app/src/main/res/drawable-xhdpi/ic_launcher.png and b/app/src/main/res/drawable-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_launcher_round.png b/app/src/main/res/drawable-xhdpi/ic_launcher_round.png new file mode 100644 index 00000000..c97e5b53 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_launcher_test.png b/app/src/main/res/drawable-xhdpi/ic_launcher_test.png deleted file mode 100644 index 066d7f73..00000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_launcher_test.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_launcher.png b/app/src/main/res/drawable-xxhdpi/ic_launcher.png index 80bb0633..11c9bd14 100644 Binary files a/app/src/main/res/drawable-xxhdpi/ic_launcher.png and b/app/src/main/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_launcher_round.png b/app/src/main/res/drawable-xxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..11c9bd14 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_launcher_test.png b/app/src/main/res/drawable-xxhdpi/ic_launcher_test.png deleted file mode 100644 index cb862934..00000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_launcher_test.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_launcher.png b/app/src/main/res/drawable-xxxhdpi/ic_launcher.png index bdde0721..48f09459 100644 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_launcher.png and b/app/src/main/res/drawable-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_launcher_round.png b/app/src/main/res/drawable-xxxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..48f09459 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_launcher_test.png b/app/src/main/res/drawable-xxxhdpi/ic_launcher_test.png deleted file mode 100644 index dbb8dee8..00000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_launcher_test.png and /dev/null differ diff --git a/app/src/main/res/drawable/ic_bug_report_black_24px.xml b/app/src/main/res/drawable/ic_bug_report_black_24px.xml index 58308de9..4d83902b 100644 --- a/app/src/main/res/drawable/ic_bug_report_black_24px.xml +++ b/app/src/main/res/drawable/ic_bug_report_black_24px.xml @@ -1,17 +1,9 @@ - - + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> - - \ No newline at end of file + android:fillColor="#FF000000" + android:pathData="M20,8h-2.81c-0.45,-0.78 -1.07,-1.45 -1.82,-1.96L17,4.41 15.59,3l-2.17,2.17C12.96,5.06 12.49,5 12,5c-0.49,0 -0.96,0.06 -1.41,0.17L8.41,3 7,4.41l1.62,1.63C7.88,6.55 7.26,7.22 6.81,8L4,8v2h2.09c-0.05,0.33 -0.09,0.66 -0.09,1v1L4,12v2h2v1c0,0.34 0.04,0.67 0.09,1L4,16v2h2.81c1.04,1.79 2.97,3 5.19,3s4.15,-1.21 5.19,-3L20,18v-2h-2.09c0.05,-0.33 0.09,-0.66 0.09,-1v-1h2v-2h-2v-1c0,-0.34 -0.04,-0.67 -0.09,-1L20,10L20,8zM14,16h-4v-2h4v2zM14,12h-4v-2h4v2z"/> + diff --git a/app/src/main/res/drawable/ic_color_lens_black_24px.xml b/app/src/main/res/drawable/ic_color_lens_black_24px.xml index 9294749e..f75e2fbe 100644 --- a/app/src/main/res/drawable/ic_color_lens_black_24px.xml +++ b/app/src/main/res/drawable/ic_color_lens_black_24px.xml @@ -1,19 +1,9 @@ - - + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> - - \ No newline at end of file + android:fillColor="#FF000000" + android:pathData="M12,3c-4.97,0 -9,4.03 -9,9s4.03,9 9,9c0.83,0 1.5,-0.67 1.5,-1.5 0,-0.39 -0.15,-0.74 -0.39,-1.01 -0.23,-0.26 -0.38,-0.61 -0.38,-0.99 0,-0.83 0.67,-1.5 1.5,-1.5L16,16c2.76,0 5,-2.24 5,-5 0,-4.42 -4.03,-8 -9,-8zM6.5,12c-0.83,0 -1.5,-0.67 -1.5,-1.5S5.67,9 6.5,9 8,9.67 8,10.5 7.33,12 6.5,12zM9.5,8C8.67,8 8,7.33 8,6.5S8.67,5 9.5,5s1.5,0.67 1.5,1.5S10.33,8 9.5,8zM14.5,8c-0.83,0 -1.5,-0.67 -1.5,-1.5S13.67,5 14.5,5s1.5,0.67 1.5,1.5S15.33,8 14.5,8zM17.5,12c-0.83,0 -1.5,-0.67 -1.5,-1.5S16.67,9 17.5,9s1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5z"/> + diff --git a/app/src/main/res/drawable/ic_comment_black_48px.xml b/app/src/main/res/drawable/ic_comment_black_48px.xml index f6a725cd..040e3245 100644 --- a/app/src/main/res/drawable/ic_comment_black_48px.xml +++ b/app/src/main/res/drawable/ic_comment_black_48px.xml @@ -7,8 +7,8 @@ + android:pathData="M21.99 4c0-1.1-0.89-2-1.99-2H4c-1.1 0-2 0.9-2 2v12c0 1.1 0.9 2 2 2h14l4 +4-0.01-18zM18 14H6v-2h12v2zm0-3H6V9h12v2zm0-3H6V6h12v2z" /> \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_contacts_black_24px.xml b/app/src/main/res/drawable/ic_contacts_black_24px.xml index e8aa75ab..674b66b7 100644 --- a/app/src/main/res/drawable/ic_contacts_black_24px.xml +++ b/app/src/main/res/drawable/ic_contacts_black_24px.xml @@ -1,16 +1,9 @@ - - + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> - - \ No newline at end of file + android:fillColor="#FF000000" + android:pathData="M20,0L4,0v2h16L20,0zM4,24h16v-2L4,22v2zM20,4L4,4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2zM12,6.75c1.24,0 2.25,1.01 2.25,2.25s-1.01,2.25 -2.25,2.25S9.75,10.24 9.75,9 10.76,6.75 12,6.75zM17,17L7,17v-1.5c0,-1.67 3.33,-2.5 5,-2.5s5,0.83 5,2.5L17,17z"/> + diff --git a/app/src/main/res/drawable/ic_format_list_bulleted_black_24px.xml b/app/src/main/res/drawable/ic_format_list_bulleted_black_24px.xml index 3eef7771..6cb93c69 100644 --- a/app/src/main/res/drawable/ic_format_list_bulleted_black_24px.xml +++ b/app/src/main/res/drawable/ic_format_list_bulleted_black_24px.xml @@ -1,16 +1,9 @@ - - + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> - - \ No newline at end of file + android:fillColor="#FF000000" + android:pathData="M4,10.5c-0.83,0 -1.5,0.67 -1.5,1.5s0.67,1.5 1.5,1.5 1.5,-0.67 1.5,-1.5 -0.67,-1.5 -1.5,-1.5zM4,4.5c-0.83,0 -1.5,0.67 -1.5,1.5S3.17,7.5 4,7.5 5.5,6.83 5.5,6 4.83,4.5 4,4.5zM4,16.5c-0.83,0 -1.5,0.68 -1.5,1.5s0.68,1.5 1.5,1.5 1.5,-0.68 1.5,-1.5 -0.67,-1.5 -1.5,-1.5zM7,19h14v-2L7,17v2zM7,13h14v-2L7,11v2zM7,5v2h14L21,5L7,5z"/> + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..10e5177c --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 00000000..2610c60c --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_mode_edit_black_48px.xml b/app/src/main/res/drawable/ic_mode_edit_black_48px.xml new file mode 100644 index 00000000..7575f1a5 --- /dev/null +++ b/app/src/main/res/drawable/ic_mode_edit_black_48px.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_notifications_black_24px.xml b/app/src/main/res/drawable/ic_notifications_black_24px.xml index d74fba00..7009a676 100644 --- a/app/src/main/res/drawable/ic_notifications_black_24px.xml +++ b/app/src/main/res/drawable/ic_notifications_black_24px.xml @@ -1,13 +1,9 @@ - - + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> - \ No newline at end of file + android:fillColor="#FF000000" + android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.89,2 2,2zM18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2z"/> + diff --git a/app/src/main/res/drawable/ic_open_in_new_black_24px.xml b/app/src/main/res/drawable/ic_open_in_new_black_24px.xml index d6abe89b..6c4a8cb2 100644 --- a/app/src/main/res/drawable/ic_open_in_new_black_24px.xml +++ b/app/src/main/res/drawable/ic_open_in_new_black_24px.xml @@ -9,6 +9,6 @@ android:pathData="M0 0h24v24H0z" /> \ No newline at end of file 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/drawable/ic_people_black_24px.xml b/app/src/main/res/drawable/ic_people_black_24px.xml index 8cbf2980..4cfd8696 100644 --- a/app/src/main/res/drawable/ic_people_black_24px.xml +++ b/app/src/main/res/drawable/ic_people_black_24px.xml @@ -1,16 +1,9 @@ - - + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> - - \ No newline at end of file + android:fillColor="#FF000000" + android:pathData="M16,11c1.66,0 2.99,-1.34 2.99,-3S17.66,5 16,5c-1.66,0 -3,1.34 -3,3s1.34,3 3,3zM8,11c1.66,0 2.99,-1.34 2.99,-3S9.66,5 8,5C6.34,5 5,6.34 5,8s1.34,3 3,3zM8,13c-2.33,0 -7,1.17 -7,3.5L1,19h14v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5zM16,13c-0.29,0 -0.62,0.02 -0.97,0.05 1.16,0.84 1.97,1.97 1.97,3.45L17,19h6v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5z"/> + diff --git a/app/src/main/res/drawable/ic_person_pin_black_48px.xml b/app/src/main/res/drawable/ic_person_pin_black_48px.xml index 3c0c9441..62a0a5d6 100644 --- a/app/src/main/res/drawable/ic_person_pin_black_48px.xml +++ b/app/src/main/res/drawable/ic_person_pin_black_48px.xml @@ -1,16 +1,4 @@ - - - - - - \ No newline at end of file + + + diff --git a/app/src/main/res/drawable/ic_question_answer_black_48px.xml b/app/src/main/res/drawable/ic_question_answer_black_48px.xml index 2683c740..5216a9ae 100644 --- a/app/src/main/res/drawable/ic_question_answer_black_48px.xml +++ b/app/src/main/res/drawable/ic_question_answer_black_48px.xml @@ -9,6 +9,6 @@ android:pathData="M0 0h24v24H0z" /> + android:pathData="M21 6h-2v9H6v2c0 0.55 0.45 1 1 1h11l4 4V7c0-0.55-0.45-1-1-1zm-4 +6V3c0-0.55-0.45-1-1-1H3c-0.55 0-1 0.45-1 1v14l4-4h10c0.55 0 1-0.45 1-1z" /> \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_report_black_48px.xml b/app/src/main/res/drawable/ic_report_black_48px.xml index 7777c6b4..586f1aab 100644 --- a/app/src/main/res/drawable/ic_report_black_48px.xml +++ b/app/src/main/res/drawable/ic_report_black_48px.xml @@ -7,8 +7,8 @@ diff --git a/app/src/main/res/drawable/ic_screen_rotation_black_24px.xml b/app/src/main/res/drawable/ic_screen_rotation_black_24px.xml index 9163901e..b63eaae7 100644 --- a/app/src/main/res/drawable/ic_screen_rotation_black_24px.xml +++ b/app/src/main/res/drawable/ic_screen_rotation_black_24px.xml @@ -9,10 +9,10 @@ android:pathData="M0 0h24v24H0z" /> +0.29 C4.25 19.94 1.91 16.76 1.55 13H0.05C0.56 19.16 5.71 24 12 +24l0.66-0.03-3.81-3.81-1.33 1.32z" /> \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_thumb_up_black_24px.xml b/app/src/main/res/drawable/ic_thumb_up_black_24px.xml index 5d721440..1fca08b3 100644 --- a/app/src/main/res/drawable/ic_thumb_up_black_24px.xml +++ b/app/src/main/res/drawable/ic_thumb_up_black_24px.xml @@ -9,8 +9,8 @@ android:pathData="M0 0h24v24H0z" /> + android:pathData="M1 21h4V9H1v12zm22-11c0-1.1-0.9-2-2-2h-6.31l0.95-4.57 0.03 +-0.32c0-0.41-0.17-0.79-0.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1 0.9 2 +2 2h9c0.83 0 1.54-0.5 1.84-1.22l3.02-7.05c0.09-0.23 0.14 -0.47 0.14 +-0.73v-1.91l-0.01-0.01L23 10z" /> \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_thumb_up_black_48px.xml b/app/src/main/res/drawable/ic_thumb_up_black_48px.xml index 5d721440..1fca08b3 100644 --- a/app/src/main/res/drawable/ic_thumb_up_black_48px.xml +++ b/app/src/main/res/drawable/ic_thumb_up_black_48px.xml @@ -9,8 +9,8 @@ android:pathData="M0 0h24v24H0z" /> + android:pathData="M1 21h4V9H1v12zm22-11c0-1.1-0.9-2-2-2h-6.31l0.95-4.57 0.03 +-0.32c0-0.41-0.17-0.79-0.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1 0.9 2 +2 2h9c0.83 0 1.54-0.5 1.84-1.22l3.02-7.05c0.09-0.23 0.14 -0.47 0.14 +-0.73v-1.91l-0.01-0.01L23 10z" /> \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_touch_app_black_24px.xml b/app/src/main/res/drawable/ic_touch_app_black_24px.xml index 1c1e60b3..dda5c85c 100644 --- a/app/src/main/res/drawable/ic_touch_app_black_24px.xml +++ b/app/src/main/res/drawable/ic_touch_app_black_24px.xml @@ -1,16 +1,9 @@ - - + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> - \ No newline at end of file + android:fillColor="#FF000000" + android:pathData="M9,11.24L9,7.5C9,6.12 10.12,5 11.5,5S14,6.12 14,7.5v3.74c1.21,-0.81 2,-2.18 2,-3.74C16,5.01 13.99,3 11.5,3S7,5.01 7,7.5c0,1.56 0.79,2.93 2,3.74zM18.84,15.87l-4.54,-2.26c-0.17,-0.07 -0.35,-0.11 -0.54,-0.11L13,13.5v-6c0,-0.83 -0.67,-1.5 -1.5,-1.5S10,6.67 10,7.5v10.74l-3.43,-0.72c-0.08,-0.01 -0.15,-0.03 -0.24,-0.03 -0.31,0 -0.59,0.13 -0.79,0.33l-0.79,0.8 4.94,4.94c0.27,0.27 0.65,0.44 1.06,0.44h6.79c0.75,0 1.33,-0.55 1.44,-1.28l0.75,-5.27c0.01,-0.07 0.02,-0.14 0.02,-0.2 0,-0.62 -0.38,-1.16 -0.91,-1.38z"/> + diff --git a/app/src/main/res/drawable/ic_visibility_black_24dp.xml b/app/src/main/res/drawable/ic_visibility_black_24dp.xml new file mode 100644 index 00000000..e02f1d19 --- /dev/null +++ b/app/src/main/res/drawable/ic_visibility_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_visibility_off_black_24px.xml b/app/src/main/res/drawable/ic_visibility_off_black_24px.xml index 1eeca64f..b3bb7460 100644 --- a/app/src/main/res/drawable/ic_visibility_off_black_24px.xml +++ b/app/src/main/res/drawable/ic_visibility_off_black_24px.xml @@ -9,11 +9,11 @@ android:pathData="M0 0h24v24H0zm0 0h24v24H0zm0 0h24v24H0zm0 0h24v24H0z" /> + android:pathData="M12 7c2.76 0 5 2.24 5 5 0 0.65-0.13 1.26-0.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 +3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74 0.25 -3.98 0.7 l2.16 2.16C10.74 7.13 +11.35 7 12 7zM2 4.27l2.28 2.28 0.46 0.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 +11 7.5 1.55 0 3.03-0.3 4.38-0.84l0.42 0.42 L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 +9.8l1.55 1.55c-0.05 0.21 -0.08 0.43 -0.08 0.65 0 1.66 1.34 3 3 3 0.22 0 0.44-0.03 0.65 +-0.08l1.55 1.55c-0.67 0.33 -1.41 0.53 -2.2 0.53 -2.76 0-5-2.24-5-5 0-0.79 0.2 -1.53 0.53 +-2.2zm4.31-0.78l3.15 3.15 0.02 -0.16c0-1.66-1.34-3-3-3l-0.17 0.01 z" /> \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_vpn_lock_black_24px.xml b/app/src/main/res/drawable/ic_vpn_lock_black_24px.xml index 14dd79ed..266f1578 100644 --- a/app/src/main/res/drawable/ic_vpn_lock_black_24px.xml +++ b/app/src/main/res/drawable/ic_vpn_lock_black_24px.xml @@ -7,13 +7,13 @@ + android:pathData="M22 4v-0.5C22 2.12 20.88 1 19.5 1S17 2.12 17 3.5V4c-0.55 0-1 0.45-1 1v4c0 0.55 0.45 1 +1 1h5c0.55 0 1-0.45 1-1V5c0-0.55-0.45-1-1-1zm-0.8 0h-3.4v-0.5c0-0.94 0.76 -1.7 +1.7-1.7s1.7 0.76 1.7 1.7V4zm-2.28 8c0.04 0.33 0.08 0.66 0.08 1 0 2.08-0.8 3.97-2.1 +5.39-0.26-0.81-1-1.39-1.9-1.39h-1v-3c0-0.55-0.45-1-1-1H7v-2h2c0.55 0 1-0.45 +1-1V8h2c1.1 0 2-0.9 2-2V3.46c-0.95-0.3-1.95-0.46-3-0.46C5.48 3 1 7.48 1 13s4.48 10 10 +10 10-4.48 10-10c0-0.34-0.02-0.67-0.05-1h-2.03zM10 20.93c-3.95-0.49-7-3.85-7-7.93 +0-0.62 0.08 -1.21 0.21 -1.79L8 16v1c0 1.1 0.9 2 2 2v1.93z" /> \ No newline at end of file diff --git a/app/src/main/res/drawable/sc_aspect.xml b/app/src/main/res/drawable/sc_aspect.xml new file mode 100644 index 00000000..3ca0e79f --- /dev/null +++ b/app/src/main/res/drawable/sc_aspect.xml @@ -0,0 +1,15 @@ + + + + + + + diff --git a/app/src/main/res/drawable/sc_edit.xml b/app/src/main/res/drawable/sc_edit.xml new file mode 100644 index 00000000..fb9e361a --- /dev/null +++ b/app/src/main/res/drawable/sc_edit.xml @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/sc_history.xml b/app/src/main/res/drawable/sc_history.xml new file mode 100644 index 00000000..a100d665 --- /dev/null +++ b/app/src/main/res/drawable/sc_history.xml @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/sc_tags.xml b/app/src/main/res/drawable/sc_tags.xml new file mode 100644 index 00000000..ed8ad05f --- /dev/null +++ b/app/src/main/res/drawable/sc_tags.xml @@ -0,0 +1,15 @@ + + + + + + + + + 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"/>