diff --git a/.atomignore b/.atomignore new file mode 100755 index 000000000..033e25f69 --- /dev/null +++ b/.atomignore @@ -0,0 +1,23 @@ +.git +.github +.gradle +.idea +build +gradle +.gitlab-ci.yml +*.iml +local.properties +settings.gradle +gradlew +gradlew.bat +LICENSE.md +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 new file mode 100644 index 000000000..1cfcaa79d --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,27 @@ +#### General information + +* **App version:** +* **System:** +* **Pod:** + +#### Description + + +#### Log + + diff --git a/.github/PULL_REQUEST_TEMPLATE b/.github/PULL_REQUEST_TEMPLATE new file mode 100644 index 000000000..863cd4268 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE @@ -0,0 +1,27 @@ + + + + + + + diff --git a/.gitignore b/.gitignore index c6cbe562a..6aa6839b7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,103 @@ +############## +### Common ### +*~* +tmp/ +*.tmp +*.bak +*.log + +################ +### Intellij ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio *.iml +/out/ + +.idea/ +# if you remove the above rule, at least ignore the following: +## User-specific stuff: +# .idea/workspace.xml +# .idea/tasks.xml +# .idea/dictionaries +## Sensitive or high-churn files: +# .idea/dataSources.ids +# .idea/dataSources.xml +# .idea/sqlDataSources.xml +# .idea/dynamic.xml +# .idea/uiDesigner.xml + +# Gradle: +# .idea/gradle.xml +# .idea/libraries + +## File-based project format: +*.ipr +*.iws + +### Gradle ### .gradle -/local.properties -/.idea/workspace.xml -/.idea/libraries -.DS_Store -/build -/captures +build/ +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +############### +### Eclipse ### +#.project +*.pydevproject +.metadata +*.swp +*~.nib +.settings/ +.loadpath +.externalToolBuilders/ +*.launch +.cproject +.classpath +.factorypath +.buildpath +.target +.texlipse + +############ +### Java ### +*.class +.mtj.tmp/ +*.jar +*.war +*.ear +hs_err_pid* + +############### +### Android ### +# Built application files +*.apk +*.ap_ + +# Files for the Dalvik VM +*.dex + +# Generated files +bin/ +gen/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard +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/.hidden b/.hidden new file mode 100755 index 000000000..51b5ac342 --- /dev/null +++ b/.hidden @@ -0,0 +1,7 @@ +crowdin.yaml +diaspora-android.iml +gradle +gradle.properties +gradlew +gradlew.bat +local.properties diff --git a/.idea/.name b/.idea/.name deleted file mode 100644 index 27df2eec3..000000000 --- a/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -Diaspora \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index 96cc43efa..000000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml deleted file mode 100644 index e7bedf337..000000000 --- a/.idea/copyright/profiles_settings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml deleted file mode 100644 index 39139a6c2..000000000 --- a/.idea/gradle.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index fbb68289f..000000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index b52c2f0c3..000000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml deleted file mode 100644 index 7f68460d8..000000000 --- a/.idea/runConfigurations.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 6564d52db..000000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..b4204f729 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,48 @@ +language: android +jdk: oraclejdk8 + +before_cache: + # Do not cache a few Gradle files/directories (see https://docs.travis-ci.com/user/languages/java/#Caching) + - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock + - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ + +cache: + directories: + # Android SDK + - $HOME/android-sdk-dl + - $HOME/android-sdk + + # Gradle dependencies + - $HOME/.gradle/caches/ + - $HOME/.gradle/wrapper/ + + # Android build cache (see http://tools.android.com/tech-docs/build-cache) + - $HOME/.android/build-cache + +install: + # Download and unzip the Android SDK tools (if not already there thanks to the cache mechanism) + # Latest version available here: https://developer.android.com/studio/index.html#downloads + - if test ! -e $HOME/android-sdk-dl/sdk-tools.zip ; then curl https://dl.google.com/android/repository/sdk-tools-linux-3859397.zip > $HOME/android-sdk-dl/sdk-tools.zip ; fi + - unzip -qq -n $HOME/android-sdk-dl/sdk-tools.zip -d $HOME/android-sdk + + # Install or update Android SDK components (will not do anything if already up to date thanks to the cache mechanism) + - echo y | $HOME/android-sdk/tools/bin/sdkmanager 'tools' > /dev/null + - echo y | $HOME/android-sdk/tools/bin/sdkmanager 'platform-tools' > /dev/null + - echo y | $HOME/android-sdk/tools/bin/sdkmanager 'build-tools;26.0.2' > /dev/null + - echo y | $HOME/android-sdk/tools/bin/sdkmanager 'platforms;android-27' > /dev/null + - echo y | $HOME/android-sdk/tools/bin/sdkmanager 'extras;google;m2repository' > /dev/null + +branches: + except: + - gh-pages + - l10n_master + - crowdin + +env: + global: + - ANDROID_HOME=$HOME/android-sdk + matrix: + - TASK="clean lintFlavorDefaultDebug --stacktrace" + - TASK="clean build check -x lint --stacktrace" + +script: "./gradlew --no-daemon --parallel $TASK" diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..5e8072e6f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,224 @@ +### 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 +- Improve Shared by notice +- Use opoc/Helpers,AdBlock + +### v0.2.7 (2017-05-26) +- Small improvements + +### v0.2.6 (2017-05-03) +- Fixed #156 #158 #159 +- Added chinese traditional language +- Added NavSlider option: Statistics +- Changed shared-by-notice text +- Fix bottom bar hint text background color (Fix #157) +- Color improvements + +### v0.2.5 (2017-04-10) +- Introduce minimalistic Markdown Parser +- Show LICENSE at first start +- Show CHANGELOG after update +- Convert existing Markup files to Markdown +- Update existing markdown files +- Update AboutActivity to use new Parser +- Added translations: Danish, Korean, Galician + +### v0.2.4 (2017-03-19) +- Different icon and color for secondlion +- Language switcher +- Handle dia.so links +- Improve security at internal browser decision +- More icons for notification dropdown +- Update gradle build scripts +- Added CircleCI + +### v0.2.3 (2017-02-24) +- Add Czech translation (thanks @bezcitu) +- Add option to copy image urls to clipboard +- Fixed some bugs related to image upload/download +- Published secondlion\* (nighly version of dandelion\*) + +### v0.2.2 +- Move "toggle mobile/deskop" to nav-slider +- Reduce messages sent via broadcast +- Allow to jump to last visited page on stream +- New language: Hungarian +- FIX NullPtr in shared text methods +- FIX #117 - Reset NavHeader on change account, reset web profile +- FIX #92 Roation settings +- FIX #111 Remove legacy code + +### v0.2.1 +- App name changed to **dandelion*** +- Rotation options +- Top toolbar loads screen again (toggleable in settings) +- Fixed overlapping fragments +- Visual rework of the About-section of the app + +### v0.2.0a +- Added: Customizable Theme Colors! +- Improved account setup with easy tor hidden service configuration +- Eye candy for the settings activity +- Added: "Contacts" shortcut in the navigation slider +- Increased the overall performance by using Fragments +- Lots of bugfixes +- Fixes for the bugfixes! + +### v0.1.6 +- Added: New languages +- Changed: New delicious visual style + launcher icon +- Changed: Notifications-/Messages-indicator does now display number of events! +- Changed: Redesigned Navigation Drawer +- Fixed: Immediately apply preference changes +- Added: About screen that shows useful information +- Changed: Updated NetCipher library to 2.0.0-alpha1 +- Fixed: Do not reload stream on orientation changes +- Fixed: Image upload for older devices +- Added: Option to open external links in Chrome CustomTab + +### v0.1.5 +- Update title depending on what the user is doing +- New greenish color scheme +- Replaced SwipeToRefresh functionality with refresh button +- Fixed some layout bugs (toolbars) +- New translations! (Japanese, Portuguese-Brazilian, Russian, Espanol) Thanks translators! +- Increased Min-API to 17 (Jelly Bean) to mitigate CVE-2012-6636 +- Updated icons to vector graphics +- Improvements to new-message/new-notification counters +- Click on profile picture now opens users profile +- Disabled backup functionality to prevent attackers to steal login cookies +- Rework settings +- Allow slider customization +- Show aspect name after selection + +### v0.1.4 (2016-07-31) +- by @vanitasvitae, @gsantner, @di72nn +- Allow turning off toolbar intellihide +- Handle links from browseable intent filter #38 +- Intent filter for pods +- Update license infos of source files +- Update license infos of source files +- Localization lint; Translation; Readme +- Add an option to clear WebView cache +- Don't use startActivityForResult on SettingsActivity +- Disable swipe refresh in some parts of the app +- Add "Followed tags" listing +- Share screenshot fix; Minor Aspects rework +- Update to SDK 24 (Android N) + +### v0.1.3 (2016-07-04) +- Added titles on top toolbar (by @scoute-dich) +- Made bottom toolbar automatically disappear +- Added option to share images to external app +- Added option to enable proxy (by @vanitasvitae) +- Added french translation (thanks to @SansPseudoFix) +- Added new settings section (by @vanitasvitae) +- Fixed buggy snackbars +- Removed swipe-to-refresh functionality in some places +- Big thanks and good luck to @scoute-dich and @martinchodev for accompanying this project :) + +### v0.1.2 (2016-06-05) +- Extract and show aspects (by @gsantner) +- Cache last podlist +- Better sharing from app +- Collapsing top menu +- ProgressBar material, Improve search dialog +- fix keyboard. #4 +- Reworked sharing from activity #12 +- toolbar/actions/menu changes, replaced fab +- Refactor layout & menu files, dialogs +- Lots of refactoring; Reworked Splash,PodSelectionActivity; Switch Pod; Clear settings; +- Activity transitions, usability MainActivity, green accent color + +### v0.1.1 +- Sharing updated (by @scoute-dich) +- Screenshotting updated +- Gitter integration (by @gsantner) +- Code refactoring +- Start working on #6 +- Waffle.io integration +- Travis CI integration +- Bump Gradle, Build-Tools, Libs to Android Studio 2.1 defaults + +### v0.1.0 (Diaspora for Android) +First version of the organization *Diaspora for Android* +Consists mostly of code from: +- Diaspora-Native-Webapp (by @martinchodev ) +- scoutedich additions (by @scoute-dich) +- gsantner additions (by @gsantner) + +### v1.3 (scoutedich) +*big thanks to gsantner* +- gitignore +- Link to profile +- Move menu actions +- Refactoring part1 +- bump libs + +### v1.2 (scoutedich) +- using strings in podactivity +- improved share activity + +### v1.1 (scoutedich) +- new about app and help dialogs +- better snackbar integration + +### v1.0.1 (scoutedich) +- click toolbar to load strem + +### v1.0 (Diaspora-Native-Webapp as base + scoutedich additions) + +First release: +- all features of original Diaspora-Native-Webapp +- popup menus (view settings, diaspora settings, share function) +- share function (link, screenshot) +- design improvements +- implemented android marshmallow perimssion model +- implemented swipe to refresh diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..47984ecfb --- /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/CONTRIBUTORS.md b/CONTRIBUTORS.md new file mode 100644 index 000000000..7969e4cde --- /dev/null +++ b/CONTRIBUTORS.md @@ -0,0 +1,32 @@ + +* **[Gregor Santner](http://gsantner.net)**
~° Current developer 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 diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 000000000..4292c03f8 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,25 @@ +# dandelion\* +`---------------` +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see https://www.gnu.org/licenses/. +`---------------` + +If you want to publish dandelion\* in an app store, you have to change the app name (dandelion\*), package name & launcher icon (blue). See "7. Additional Terms" of GPL. +You also have to provide the modifications in source code somewhere online for free. This is explicitly not about building the app and sharing with some friends, it's about app stores. +The reason is, that most app stores allow an app id just once, which would block our uploads if somebody else uploaded. Also, we want to keep control on where the app is published, and want to make sure there is no malware in it. +The F-Droid project team is explicitly allowed to publish dandelion\* without listed required modifications above at official F-Droid repository. + +## Miscellaneous + +We took some inspiration and code from LeafPic. Go check it out, its free software as well! + diff --git a/README.md b/README.md new file mode 100644 index 000000000..bcdadee7e --- /dev/null +++ b/README.md @@ -0,0 +1,84 @@ +[![GitHub release](https://img.shields.io/github/tag/diaspora-for-android/dandelion.svg)](https://github.com/diaspora-for-android/dandelion/releases) +[![Build Status](https://travis-ci.org/Diaspora-for-Android/dandelion.svg?branch=master)](https://travis-ci.org/Diaspora-for-Android/dandelion) +[![Translate - with Stringlate](https://img.shields.io/badge/stringlate-translate-green.svg)](https://lonamiwebs.github.io/stringlate/translate?git=https%3A%2F%2Fgithub.com%2Fdiaspora-for-android%2Fdandelion.git&mail=gro.xobliam@@rentnasg) +[![Chat - Matrix](https://img.shields.io/badge/chat-on%20matrix-blue.svg)](https://matrix.to/#/#dandelion:matrix.org) [![Chat - FreeNode IRC](https://img.shields.io/badge/chat-on%20irc-blue.svg)](https://kiwiirc.com/client/irc.freenode.net/?nick=dandelion-anon|?##dandelion) +[![Donate Bitcoin](https://img.shields.io/badge/donate-bitcoin-orange.svg)](https://gsantner.net/supportme/?project=dandelion&source=readme) +[![Donate LiberaPay](https://img.shields.io/badge/donate-liberapay-orange.svg)](https://liberapay.com/gsantner/donate) + +# dandelion\* + +This is an unofficial webview based client for the community-run, distributed social network diaspora*. + +
+ + Get it on F-Droid + + +

+ + +## 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. + +#### 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 +The minimum Android version supported is Jelly Bean, Android v4.2.0 / API 17 + +### 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. + + +## 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 or via [E-Mail](http://gsantner.net/#contact). You can use Stringlate ([![Translate - with Stringlate](https://img.shields.io/badge/stringlate-translate-green.svg)](https://lonamiwebs.github.io/stringlate/translate?git=https%3A%2F%2Fgithub.com%2Fdiaspora-for-android%2Fdandelion.git)) to translate the project directly on your Android phone. It allows you to export as E-Mail attachement and to post on GitHub. + +Join our IRC or Matrix channel (bridged) and say hello! Don't be afraid to start talking. [![Chat - Matrix](https://img.shields.io/badge/chat-on%20matrix-blue.svg)](https://matrix.to/#/#dandelion:matrix.org) [![Chat - FreeNode IRC](https://img.shields.io/badge/chat-on%20irc-blue.svg)](https://kiwiirc.com/client/irc.freenode.net/?nick=dandelion-anon|?##dandelion) + +Note that the main project members are working on this project for free during leisure time, are mostly busy with their job/university/school, and may not react or start coding immediately. + + +#### Resources +* Project: [Changelog](/CHANGELOG.md) | [Issues level/beginner](https://github.com/diaspora-for-android/dandelion/issues?q=is%3Aissue+is%3Aopen+label%3Alevel%2Fbeginner) | [License](/LICENSE.txt) | [CoC](/CODE_OF_CONDUCT.md) +* Project diaspora\* account: [dandelion00@diasp.org](https://diasp.org/people/48b78420923501341ef3782bcb452bd5) +* diaspora\*: [GitHub](https://github.com/diaspora/diaspora) | [Web](https://diasporafoundation.org) | [d\* HQ account](https://pod.diaspora.software/people/7bca7c80311b01332d046c626dd55703) +* App on F-Droid: [Metadata](https://gitlab.com/fdroid/fdroiddata/blob/master/metadata/com.github.dfa.diaspora_android.txt) | [Page](https://f-droid.org/packages/com.github.dfa.diaspora_android/) | [Wiki](https://f-droid.org/wiki/page/com.github.dfa.diaspora_android) | [Build log](https://f-droid.org/wiki/page/com.github.dfa.diaspora_android/lastbuild) + + +## Licensing +dandelion\* is released under GNU GENERAL PUBLIC LICENSE (see [LICENCE](https://github.com/Diaspora-for-Android/dandelion/blob/master/LICENSE.md)). +The app is licensed GPL v3. Localization files and resources (strings\*.xml) are licensed CC0 1.0. +For more licensing informations, see [`3rd party licenses`](/app/src/main/res/raw/licenses_3rd_party.md). + +## Screenshots +
+ + + + + +
+ +
+ + + + +
+ +### Notice +#### Maintainers +- gsantner ([GitHub](https://github.com/gsantner), [Web](https://gsantner.net), [diaspora*](https://pod.geraspora.de/people/d1cbdd70095301341e834860008dbc6c)) + - Bitcoin: [1B9ZyYdQoY9BxMe9dRUEKaZbJWsbQqfXU5](http://gsantner.net/donate/#donate) +- vanitasvitae ([GitHub](https://github.com/vanitasvitae), [diaspora*](https://pod.geraspora.de/people/bbd7af90fbec013213e34860008dbc6c)) + - Bitcoin: 1Ao3W6NaQv3xKppviB7RSFKjHo6PGd8RTy diff --git a/SCREENSHOTS.md b/SCREENSHOTS.md new file mode 100644 index 000000000..9403120db --- /dev/null +++ b/SCREENSHOTS.md @@ -0,0 +1,16 @@ +## Screenshots + +
+ + + + + +
+ +
+ + + + +
diff --git a/app/build.gradle b/app/build.gradle index 0c09c6e6a..a11ef9b98 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,29 +1,107 @@ apply plugin: 'com.android.application' +if (enable_plugin_kotlin) { + apply plugin: 'kotlin-android' + apply plugin: 'kotlin-android-extensions' + apply plugin: 'kotlin-kapt' +} android { - compileSdkVersion 23 - buildToolsVersion "23.0.2" - useLibrary 'org.apache.http.legacy' + compileSdkVersion version_setup_compileSdk + flavorDimensions "default" defaultConfig { - applicationId "de.baumann.diaspora" - minSdkVersion 15 - targetSdkVersion 23 - versionCode 1 - versionName "1.0" + minSdkVersion version_setup_minSdk + targetSdkVersion version_setup_targetSdk + buildConfigField "boolean", "IS_TEST_BUILD", "false" + buildConfigField "boolean", "IS_GPLAY_BUILD", "false" + buildConfigField "String[]", "DETECTED_ANDROID_LOCALES", "${findUsedAndroidLocales()}" + buildConfigField "String", "GITHASH", "\"${getGitHash()}\"" + resValue "string", "manifest_package_id", "com.github.dfa.diaspora_android" + + applicationId "com.github.dfa.diaspora_android" + versionName "1.1.3" + versionCode 32 + + + vectorDrawables.useSupportLibrary = true + resValue 'string', 'app_name', "dandelion*" + manifestPlaceholders = [appIcon: "@drawable/ic_launcher"] } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } + + sourceSets { + if (enable_plugin_kotlin) { + main.java.srcDirs += 'src/main/kotlin' + } + } + + productFlavors { + flavorDefault { + } + + /* + flavorGplay { + buildConfigField "boolean", "IS_GPLAY_BUILD", "true" + }*/ + + flavorTest { + applicationId "com.github.dfa.secondlion" + resValue 'string', 'app_name', "secondlion*" + manifestPlaceholders = [appIcon: "@drawable/ic_launcher_test"] + versionCode = Integer.parseInt(new Date().format('yyMMdd')) + versionName = new Date().format('yyMMdd') + buildConfigField "boolean", "IS_TEST_BUILD", "true" + } + } + lintOptions { + disable 'MissingTranslation' + } } dependencies { - compile fileTree(dir: 'libs', include: ['*.jar']) - testCompile 'junit:junit:4.12' - compile 'com.android.support:appcompat-v7:23.1.1' - compile 'com.android.support:design:23.1.1' - compile 'com.getbase:floatingactionbutton:1.9.1' + // Sub-Projects + //implementation project(':subprojectFromRoot') + + // Jars + implementation fileTree(dir: 'libs', include: ['*.jar']) + testImplementation 'junit:junit:4.12' + + // Android standard libs + 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}" + + // UI libraries + implementation "com.github.DASAR:ShiftColorPicker:v0.5" + + // Tool libraries + implementation "info.guardianproject.netcipher:netcipher:${version_library_netcipher}" + implementation "info.guardianproject.netcipher:netcipher-webkit:${version_library_netcipher}" + implementation "com.jakewharton:butterknife:${version_library_butterknife}" + if (enable_plugin_kotlin) { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$version_plugin_kotlin" + } + + // Processors + def anpros = ["com.jakewharton:butterknife-compiler:${version_library_butterknife}"] + for (anpro in anpros) { + if (enable_plugin_kotlin) { + kapt anpro + } else { + annotationProcessor anpro + } + } } diff --git a/app/src/androidTest/java/de/baumann/diaspora/ApplicationTest.java b/app/src/androidTest/java/de/baumann/diaspora/ApplicationTest.java deleted file mode 100644 index 4e7d2ba85..000000000 --- a/app/src/androidTest/java/de/baumann/diaspora/ApplicationTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package de.baumann.diaspora; - -import android.app.Application; -import android.test.ApplicationTestCase; - -/** - * Testing Fundamentals - */ -public class ApplicationTest extends ApplicationTestCase { - public ApplicationTest() { - super(Application.class); - } -} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index aeeebb370..91d956805 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,64 +1,807 @@ + xmlns:tools="http://schemas.android.com/tools" + package="com.github.dfa.diaspora_android" + tools:ignore="GoogleAppIndexingWarning"> + + + android:theme="@style/DiasporaLight"> + + + + + + + + + + + + + + android:launchMode="singleTop" + android:theme="@style/DiasporaLight.NoActionBar" + android:windowSoftInputMode="adjustResize"> + + + + + + + - - - - - - - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/ic_launcher-web.png b/app/src/main/ic_launcher-web.png new file mode 100644 index 000000000..0a8685ceb Binary files /dev/null and b/app/src/main/ic_launcher-web.png differ diff --git a/app/src/main/ic_launcher_test-web.png b/app/src/main/ic_launcher_test-web.png new file mode 100644 index 000000000..8f31241cc Binary files /dev/null and b/app/src/main/ic_launcher_test-web.png differ diff --git a/app/src/main/ic_launcher_test.png b/app/src/main/ic_launcher_test.png new file mode 100755 index 000000000..18e3ea1d0 Binary files /dev/null and b/app/src/main/ic_launcher_test.png 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 new file mode 100644 index 000000000..032716370 --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/App.java @@ -0,0 +1,118 @@ +/* + 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; + +import android.app.Application; +import android.content.Context; +import android.os.Build; +import android.support.annotation.Nullable; +import android.support.v7.app.AppCompatDelegate; +import android.webkit.CookieManager; +import android.webkit.CookieSyncManager; +import android.webkit.WebView; + +import com.github.dfa.diaspora_android.data.DiasporaUserProfile; +import com.github.dfa.diaspora_android.service.AvatarImageLoader; +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 net.gsantner.opoc.util.AdBlock; + +public class App extends Application { + private volatile static App app; + private AppSettings appSettings; + private AvatarImageLoader avatarImageLoader; + private CookieManager cookieManager; + private DiasporaUserProfile diasporaUserProfile; + + public static App get() { + return app; + } + + @Override + public void onCreate() { + super.onCreate(); + app = this; + final Context c = getApplicationContext(); + appSettings = AppSettings.get(); + + // Init app log + AppLog.setLoggingEnabled(appSettings.isLoggingEnabled()); + AppLog.setLoggingSpamEnabled(appSettings.isLoggingSpamEnabled()); + + // Init pod profile + avatarImageLoader = new AvatarImageLoader(c); + diasporaUserProfile = new DiasporaUserProfile(this); + + + // Get cookie manager + cookieManager = CookieManager.getInstance(); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + //noinspection deprecation + CookieSyncManager.createInstance(c); + } + cookieManager.setAcceptCookie(true); + AppCompatDelegate.setCompatVectorFromResourcesEnabled(true); + AdBlock.getInstance().loadHostsFromRawAssetsAsync(this); + } + + public void resetPodData(@Nullable WebView webView) { + if (webView != null) { + webView.stopLoading(); + webView.loadUrl(DiasporaUrlHelper.URL_BLANK); + webView.clearFormData(); + webView.clearHistory(); + webView.clearCache(true); + } + + // Clear avatar image + new AvatarImageLoader(this).clearAvatarImage(); + + // Clear preferences__master + appSettings.resetPodSettings(); + + // Clear User profile - reload empty AppSettingsBase data + diasporaUserProfile.loadFromAppSettings(); + + // Clear cookies + //noinspection deprecation + cookieManager.removeAllCookie(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + cookieManager.removeAllCookies(null); + } + } + + public DiasporaUserProfile getDiasporaUserProfile() { + return diasporaUserProfile; + } + + public AppSettings getSettings() { + return appSettings; + } + + public AvatarImageLoader getAvatarImageLoader() { + return avatarImageLoader; + } + + public CookieManager getCookieManager() { + return cookieManager; + } +} 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 new file mode 100644 index 000000000..cd43fb888 --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/activity/AboutActivity.java @@ -0,0 +1,432 @@ +/* + 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.ClipData; +import android.content.ClipboardManager; +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; +import android.support.v4.app.FragmentManager; +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.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +import com.github.dfa.diaspora_android.App; +import com.github.dfa.diaspora_android.R; +import com.github.dfa.diaspora_android.listener.IntellihideToolbarActivityListener; +import com.github.dfa.diaspora_android.ui.HtmlTextView; +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.ThemedFragment; +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 java.util.Observable; +import java.util.Observer; + +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; + +/** + * Activity that holds some fragments that show information about the app in a tab layout + */ +public class AboutActivity extends ThemedActivity + implements IntellihideToolbarActivityListener { + + @BindView(R.id.about__appbar) + protected AppBarLayout _appBarLayout; + + @BindView(R.id.main__topbar) + protected Toolbar _toolbar; + + @BindView(R.id.appbar_linear_layout) + protected LinearLayout _linearLayout; + + @BindView(R.id.tabs) + protected TabLayout _tabLayout; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + 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() { + @Override + public void onClick(View v) { + AboutActivity.this.onBackPressed(); + } + }); + // Create the adapter that will return a fragment for each of the three + // primary sections of the activity. + SectionsPagerAdapter mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager()); + + // Set up the ViewPager with the sections adapter. + ViewPager mViewPager = ButterKnife.findById(this, R.id.container); + mViewPager.setAdapter(mSectionsPagerAdapter); + + _tabLayout.setupWithViewPager(mViewPager); + } + + @Override + public void onResume() { + super.onResume(); + setToolbarIntellihide(getAppSettings().isIntellihideToolbars()); + } + + @Override + protected void applyColorToViews() { + ThemeHelper.updateToolbarColor(_toolbar); + ThemeHelper.updateTabLayoutColor(_tabLayout); + ThemeHelper.setPrimaryColorAsBackground(_linearLayout); + } + + public void setToolbarIntellihide(boolean enable) { + AppBarLayout.LayoutParams params = (AppBarLayout.LayoutParams) _linearLayout.getLayoutParams(); + if (enable) { + AppLog.d(this, "Enable Intellihide"); + params.setScrollFlags(toolbarDefaultScrollFlags); + + } else { + AppLog.d(this, "Disable Intellihide"); + params.setScrollFlags(0); // clear all scroll flags + } + _appBarLayout.setExpanded(true, true); + } + + /** + * Fragment that shows general information about the app + */ + public static class AboutFragment extends ThemedFragment { + + public static final String TAG = "com.github.dfa.diaspora_android.AboutActivity.AboutFragment"; + + @BindView(R.id.fragment_about__app_version) + TextView appVersion; + + @BindView(R.id.fragment_about__spread_the_word_text) + HtmlTextView spreadText; + + @BindView(R.id.fragment_about__contribute_button) + Button contributeBtn; + + @BindView(R.id.fragment_about__translate_button) + Button translateBtn; + + @BindView(R.id.fragment_about__feedback_button) + Button feedbackBtn; + + @BindView(R.id.fragment_about__spread_the_word_button) + Button spreadBtn; + + public AboutFragment() { + } + + @Override + 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 + ")")); + + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + } + } + } + + @Override + protected void applyColorToViews() { + ThemeHelper.getInstance(getAppSettings()); + ThemeHelper.updateTextViewLinkColor(spreadText); + ThemeHelper.updateButtonTextColor(contributeBtn); + ThemeHelper.updateButtonTextColor(feedbackBtn); + ThemeHelper.updateButtonTextColor(spreadBtn); + ThemeHelper.updateButtonTextColor(translateBtn); + } + + @Override + public String getFragmentTag() { + return TAG; + } + + @Override + public boolean onBackPressed() { + return false; + } + + @OnClick({R.id.fragment_about__contribute_button, R.id.fragment_about__translate_button, R.id.fragment_about__feedback_button, R.id.fragment_about__spread_the_word_button}) + public void buttonClicked(View view) { + switch (view.getId()) { + case R.id.fragment_about__contribute_button: + ContextUtils.get().openWebpageInExternalBrowser(getString(R.string.fragment_about__contribute_link)); + break; + case R.id.fragment_about__translate_button: + ContextUtils.get().openWebpageInExternalBrowser(getString(R.string.fragment_about__translate_link)); + break; + case R.id.fragment_about__feedback_button: + 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))); + break; + } + } + } + + /** + * Fragment that shows information about the license of the app and used 3rd party libraries + */ + public static class LicenseFragment extends ThemedFragment { + public static final String TAG = "com.github.dfa.diaspora_android.AboutActivity.LicenseFragment"; + + @BindView(R.id.fragment_license__maintainers_text) + HtmlTextView maintainers; + + @BindView(R.id.fragment_license__contributors_text) + HtmlTextView contributors; + + @BindView(R.id.fragment_license__thirdparty_libs_text) + HtmlTextView thirdPartyLibs; + + @BindView(R.id.fragment_license__license_button) + Button licenseBtn; + + @BindView(R.id.fragment_license__leafpic_button) + Button leafpicBtn; + + private String accentColor; + + + public LicenseFragment() { + } + + @Override + protected int getLayoutResId() { + return R.layout.about__fragment_license; + } + + @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.fragment_license__maintainers_text, + ContextUtils.get().loadMarkdownForTextViewFromRaw(R.raw.maintainers, ""))); + contributors.setTextFormatted(getString(R.string.fragment_license__contributors_thank_you, + ContextUtils.get().loadMarkdownForTextViewFromRaw(R.raw.contributors, ""))); + thirdPartyLibs.setTextFormatted( + 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: + ContextUtils.get().openWebpageInExternalBrowser(getString(R.string.fragment_licesen__misc_leafpic_link)); + break; + case R.id.fragment_license__license_button: + ContextUtils.get().openWebpageInExternalBrowser(getString(R.string.fragment_license__license_gpl_link)); + break; + } + } + + @Override + protected void applyColorToViews() { + ThemeHelper.getInstance(getAppSettings()); + ThemeHelper.updateButtonTextColor(leafpicBtn); + ThemeHelper.updateButtonTextColor(licenseBtn); + ThemeHelper.updateTextViewLinkColor(maintainers); + ThemeHelper.updateTextViewLinkColor(thirdPartyLibs); + } + + @Override + public String getFragmentTag() { + return TAG; + } + + @Override + public boolean onBackPressed() { + return false; + } + } + + /** + * Fragment that shows debug information like app version, pod version... + */ + public static class DebugFragment extends Fragment implements Observer { + public static final String TAG = "com.github.dfa.diaspora_android.AboutActivity.DebugFragment"; + + @BindView(R.id.fragment_debug__package_name) + TextView packageName; + + @BindView(R.id.fragment_debug__app_version) + TextView appVersion; + + @BindView(R.id.fragment_debug__android_version) + TextView osVersion; + + @BindView(R.id.fragment_debug__device_name) + TextView deviceName; + + @BindView(R.id.fragment_debug__account_profile_name) + TextView podName; + + @BindView(R.id.fragment_debug__account_profile_domain) + TextView podDomain; + + @BindView(R.id.fragment_debug__log_box) + TextView logBox; + + public DebugFragment() { + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.about__fragment_debug, container, false); + ButterKnife.bind(this, rootView); + App app = (App) getActivity().getApplication(); + logBox.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View view) { + if (isAdded()) { + 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(); + } else { + AppLog.d(this, "Not Added!"); + } + return true; + } + }); + AppLog.Log.addLogObserver(this); + update(AppLog.Log.getInstance(), null); + + if (isAdded()) { + try { + 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 + ")")); + + 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)); + 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())); + } + + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + } + + } + return rootView; + } + + @Override + public void onDestroyView() { + AppLog.Log.removeLogObserver(this); + super.onDestroyView(); + } + + @Override + public void update(Observable observable, Object o) { + if (logBox != null) { + logBox.setText(AppLog.Log.getLogBuffer()); + } + } + } + + /** + * A {@link FragmentPagerAdapter} that returns a fragment corresponding to + * one of the sections/tabs/pages. + */ + public class SectionsPagerAdapter extends FragmentPagerAdapter { + + public SectionsPagerAdapter(FragmentManager fm) { + super(fm); + } + + @Override + public Fragment getItem(int position) { + switch (position) { + case 0: //About + return new AboutFragment(); + case 1: //License + return new LicenseFragment(); + case 2: //Debug + default: + return new DebugFragment(); + } + } + + @Override + public int getCount() { + // Show 3 total pages. + return 3; + } + + @Override + public CharSequence getPageTitle(int position) { + switch (position) { + case 0: + return getString(R.string.about_activity__title_about_app); + case 1: + return getString(R.string.about_activity__title_about_license); + case 2: + return getString(R.string.about_activity__title_debug_info); + } + 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 new file mode 100644 index 000000000..50cf66af3 --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/activity/AspectListFragment.java @@ -0,0 +1,208 @@ +/* + This file is part of the dandelion*. + + dandelion* is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + dandelion* is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with the dandelion*. + + If not, see . + */ +package com.github.dfa.diaspora_android.activity; + +import android.content.Context; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v7.widget.AppCompatImageView; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import com.github.dfa.diaspora_android.App; +import com.github.dfa.diaspora_android.R; +import com.github.dfa.diaspora_android.data.DiasporaAspect; +import com.github.dfa.diaspora_android.listener.OnSomethingClickListener; +import com.github.dfa.diaspora_android.ui.theme.ThemedFragment; +import com.github.dfa.diaspora_android.util.AppSettings; +import com.github.dfa.diaspora_android.util.ContextUtils; +import com.github.dfa.diaspora_android.util.DiasporaUrlHelper; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import butterknife.BindView; +import butterknife.ButterKnife; + +/** + * Fragment that shows a list of the Aspects + */ +public class AspectListFragment extends ThemedFragment implements OnSomethingClickListener { + + public static final String TAG = "com.github.dfa.diaspora_android.AspectListFragment"; + + @BindView(R.id.fragment_list__recycler_view) + public RecyclerView aspectsRecyclerView; + + @BindView(R.id.fragment_list__spacer) + public View space; + + @BindView(R.id.fragment_list__root) + public RelativeLayout rootView; + + protected App app; + protected DiasporaUrlHelper urls; + + @Override + protected int getLayoutResId() { + return R.layout.recycler_list__fragment; + } + + @Override + public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + ButterKnife.bind(this, view); + app = (App) getActivity().getApplication(); + AppSettings appSettings = app.getSettings(); + urls = new DiasporaUrlHelper(appSettings); + + aspectsRecyclerView.setHasFixedSize(true); + aspectsRecyclerView.setNestedScrollingEnabled(false); + + RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(getContext()); + aspectsRecyclerView.setLayoutManager(layoutManager); + + final AspectAdapter adapter = new AspectAdapter(appSettings, this); + aspectsRecyclerView.setAdapter(adapter); + + //Set window title + getActivity().setTitle(R.string.nav_aspects); + } + + @Override + public String getFragmentTag() { + return TAG; + } + + @Override + public boolean onBackPressed() { + return false; + } + + @Override + public void onSomethingClicked(Object null1, Integer null2, String aspectId) { + ((MainActivity) getActivity()).openDiasporaUrl(urls.getAspectUrl(aspectId)); + } + + @Override + protected void applyColorToViews() { + aspectsRecyclerView.invalidate(); + if (getAppSettings().isAmoledColorMode()) { + rootView.setBackgroundColor(Color.BLACK); + space.setBackgroundColor(Color.BLACK); + } + } + + public static class AspectAdapter extends RecyclerView.Adapter { + private boolean isAmoledColorMode; + private final AppSettings appSettings; + private final DiasporaAspect[] aspectList; + private final List aspectFavsList; + private final OnSomethingClickListener aspectClickedListener; + + static class ViewHolder extends RecyclerView.ViewHolder { + @BindView(R.id.recycler_view__list_item__text) + public TextView title; + @BindView(R.id.recycler_view__list_item__favourite) + AppCompatImageView favouriteImage; + @BindView(R.id.recycler_view__list_item__root) + RelativeLayout root; + + ViewHolder(View v) { + super(v); + ButterKnife.bind(this, v); + } + } + + + AspectAdapter(AppSettings appSettings, OnSomethingClickListener aspectClickedListener) { + this.appSettings = appSettings; + this.aspectList = appSettings.getAspects(); + this.aspectFavsList = new ArrayList<>(Arrays.asList(appSettings.getAspectFavs())); + this.aspectClickedListener = aspectClickedListener; + this.isAmoledColorMode = appSettings.isAmoledColorMode(); + } + + @Override + public int getItemCount() { + return aspectList.length; + } + + @Override + public AspectAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View v = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.recycler_list__list_item_with_fav, parent, false); + return new ViewHolder(v); + } + + @Override + public void onBindViewHolder(final ViewHolder holder, int position) { + // Alternating colors + final Context c = holder.root.getContext(); + final DiasporaAspect aspect = aspectList[position]; + holder.title.setText(aspect.name); + if (position % 2 == 1) { + holder.root.setBackgroundColor(isAmoledColorMode ? Color.BLACK : ContextUtils.get().rcolor(R.color.alternate_row_color)); + holder.title.setTextColor(isAmoledColorMode ? Color.GRAY : Color.BLACK); + } else { + holder.root.setBackgroundColor(isAmoledColorMode ? Color.BLACK : Color.WHITE); + holder.title.setTextColor(isAmoledColorMode ? Color.GRAY : Color.BLACK); + } + + // Favourite (Star) Image + applyFavouriteImage(holder.favouriteImage, isAspectFaved(aspect.name)); + + // Click on fav button + holder.favouriteImage.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + if (isAspectFaved(aspect.name)) { + aspectFavsList.remove(aspectFavsList.indexOf(aspect.name)); + } else { + aspectFavsList.add(aspect.name); + } + appSettings.setAspectFavs(aspectFavsList); + applyFavouriteImage(holder.favouriteImage, isAspectFaved(aspect.name)); + } + }); + + holder.root.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + aspectClickedListener.onSomethingClicked(null, null, aspect.id + ""); + } + }); + } + + private boolean isAspectFaved(String tag) { + return aspectFavsList.contains(tag); + } + + private void applyFavouriteImage(AppCompatImageView imageView, boolean isFaved) { + imageView.setImageResource(isFaved ? R.drawable.ic_star_filled_48px : R.drawable.ic_star_border_black_48px); + imageView.setColorFilter(isFaved ? appSettings.getAccentColor() : (isAmoledColorMode ? Color.GRAY : 0), PorterDuff.Mode.SRC_ATOP); + } + } +} diff --git a/app/src/main/java/com/github/dfa/diaspora_android/activity/DiasporaStreamFragment.java b/app/src/main/java/com/github/dfa/diaspora_android/activity/DiasporaStreamFragment.java new file mode 100644 index 000000000..77bd31fd9 --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/activity/DiasporaStreamFragment.java @@ -0,0 +1,416 @@ +/* + 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.Manifest; +import android.animation.ObjectAnimator; +import android.annotation.SuppressLint; +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; +import android.view.View; +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.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 + * Created by vanitas on 26.09.16. + */ + +public class DiasporaStreamFragment extends BrowserFragment { + public static final String TAG = "com.github.dfa.diaspora_android.StreamFragment"; + + protected DiasporaUrlHelper urls; + + private ValueCallback imageUploadFilePathCallbackNew; + private ValueCallback imageUploadFilePathCallbackOld; + private String mCameraPhotoPath; + + @SuppressLint("SetJavaScriptEnabled") + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + this.urls = new DiasporaUrlHelper(appSettings); + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + webView.setWebChromeClient(new DiasporaStreamWebChromeClient(webView, progressBar, fileUploadCallback, sharedTextCallback)); + webView.getSettings().setJavaScriptEnabled(true); + webView.addJavascriptInterface(new JavaScriptInterface(), "AndroidBridge"); + if (((MainActivity) getActivity()).getTextToBeShared() != null) { + loadUrl(urls.getNewPostUrl()); + } else if (webView.getUrl() == null) { + loadUrl(urls.getStreamUrl()); + } + } + }); + + } + + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + inflater.inflate(R.menu.stream__menu_top, 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 + public void onActivityResult(int requestCode, int resultCode, Intent data) { + AppLog.d(this, "onActivityResult(): " + requestCode); + switch (requestCode) { + case MainActivity.INPUT_FILE_REQUEST_CODE_NEW: + case MainActivity.INPUT_FILE_REQUEST_CODE_OLD: + AppLog.v(this, "INPUT_FILE_REQUEST_CODE: " + requestCode); + onImageUploadResult(requestCode, resultCode, data); + return; + } + super.onActivityResult(requestCode, resultCode, data); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + AppLog.d(this, "StreamFragment.onOptionsItemSelected()"); + ShareUtil shu = new ShareUtil(getContext()).setFileProviderAuthority(BuildConfig.APPLICATION_ID); + PermissionChecker permc = new PermissionChecker(getActivity()); + switch (item.getItemId()) { + case R.id.action_reload: { + if (WebHelper.isOnline(getContext())) { + reloadUrl(); + return true; + } else { + return false; + } + } + + case R.id.action_go_to_top: { + ObjectAnimator anim = ObjectAnimator.ofInt(webView, "scrollY", webView.getScrollY(), 0); + anim.setDuration(400); + anim.start(); + return true; + } + + case R.id.action_share_link: { + Intent sharingIntent = new Intent(Intent.ACTION_SEND); + 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))); + 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.share__toast_link_address_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: { + if (permc.doIfExtStoragePermissionGranted(getString(R.string.permissions_screenshot))) { + File fileSaveDirectory = appSettings.getAppSaveDirectory(); + if (permc.mkdirIfStoragePermissionGranted(fileSaveDirectory)) { + Bitmap bmp = ShareUtil.getBitmapFromWebView(webView); + String filename = "dandelion-" + ShareUtil.SDF_SHORT.format(new Date()) + ".jpg"; + _cu.writeImageToFileJpeg(new File(fileSaveDirectory, filename), bmp); + Snackbar.make(webView, getString(R.string.share__toast_screenshot) + + " " + filename, Snackbar.LENGTH_LONG).show(); + } + } + return true; + } + + case R.id.action_share_screenshot: { + if (permc.doIfExtStoragePermissionGranted(getString(R.string.permissions_screenshot))) { + shu.shareImage(ShareUtil.getBitmapFromWebView(webView), Bitmap.CompressFormat.JPEG); + } + return true; + } + } + return super.onOptionsItemSelected(item); + } + + public void onImageUploadResult(int requestCode, int resultCode, Intent data) { + AppLog.d(this, "onImageUploadResult"); + switch (requestCode) { + case MainActivity.INPUT_FILE_REQUEST_CODE_NEW: { + AppLog.v(this, "Upload image using recent method (Lollipop+)"); + if (imageUploadFilePathCallbackNew == null || resultCode != Activity.RESULT_OK) { + AppLog.e(this, "Callback is null: " + (imageUploadFilePathCallbackNew == null) + + " resultCode: " + resultCode); + if (imageUploadFilePathCallbackNew != null) + imageUploadFilePathCallbackNew.onReceiveValue(new Uri[]{}); + return; + } + Uri[] results = null; + if (data == null) { + if (mCameraPhotoPath != null) { + AppLog.v(this, "Intent data is null. Try to parse cameraPhotoPath"); + results = new Uri[]{Uri.parse(mCameraPhotoPath)}; + } else { + AppLog.w(this, "Intent data is null and cameraPhotoPath is null"); + } + } else { + String dataString = data.getDataString(); + if (dataString != null) { + AppLog.v(this, "Intent has data. Try to parse dataString"); + results = new Uri[]{Uri.parse(dataString)}; + } else { + AppLog.w(this, "dataString is null"); + } + } + AppLog.v(this, "handle received result over to callback"); + imageUploadFilePathCallbackNew.onReceiveValue(results); + imageUploadFilePathCallbackNew = null; + return; + } + case MainActivity.INPUT_FILE_REQUEST_CODE_OLD: { + AppLog.v(this, "Upload image using legacy method (Jelly Bean, Kitkat)"); + if (imageUploadFilePathCallbackOld == null || resultCode != Activity.RESULT_OK) { + AppLog.e(this, "Callback is null: " + (imageUploadFilePathCallbackOld == null) + + " resultCode: " + resultCode); + if (imageUploadFilePathCallbackOld != null) + imageUploadFilePathCallbackOld.onReceiveValue(null); + return; + } + Uri results = null; + if (data == null) { + if (mCameraPhotoPath != null) { + AppLog.v(this, "Intent has no data. Try to parse cameraPhotoPath"); + results = Uri.parse(mCameraPhotoPath); + } else { + AppLog.w(this, "Intent has no data and cameraPhotoPath is null"); + } + } else { + String dataString = data.getDataString(); + if (dataString != null) { + AppLog.v(this, "Intent has data. Try to parse dataString"); + results = Uri.parse(dataString); + } else { + AppLog.w(this, "dataString is null"); + } + } + AppLog.v(this, "handle received result over to callback"); + imageUploadFilePathCallbackOld.onReceiveValue(results); + imageUploadFilePathCallbackOld = null; + } + } + } + + protected DiasporaStreamWebChromeClient.SharedTextCallback sharedTextCallback = new DiasporaStreamWebChromeClient.SharedTextCallback() { + @Override + public String getSharedText() { + if (getActivity() != null) { + return ((MainActivity) getActivity()).getTextToBeShared(); + } + return ""; + } + + @Override + public void setSharedText(String shared) { + ((MainActivity) getActivity()).setTextToBeShared(shared); + } + }; + + protected FileUploadWebChromeClient.FileUploadCallback fileUploadCallback = new FileUploadWebChromeClient.FileUploadCallback() { + @Override + public boolean imageUpload(WebView webView, ValueCallback filePathCallback, WebChromeClient.FileChooserParams fileChooserParams) { + if (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 ThemedAlertDialogBuilder(getContext(), appSettings) + .setMessage(R.string.permissions_image) + .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; + } + } + AppLog.v(this, "onOpenFileChooser"); + imageUploadFilePathCallbackNew = filePathCallback; + Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + if (takePictureIntent.resolveActivity(getContext().getPackageManager()) != null) { + // Create the File where the photo should go + File photoFile; + try { + photoFile = ContextUtils.get().createImageFile(); + takePictureIntent.putExtra("PhotoPath", mCameraPhotoPath); + } catch (IOException ex) { + AppLog.e(this, "ERROR creating temp file: " + ex.toString()); + // Error occurred while creating the File + Snackbar.make(webView, R.string.unable_to_load_image, Snackbar.LENGTH_LONG).show(); + return false; + } + // Continue only if the File was successfully created + if (photoFile != null) { + mCameraPhotoPath = "file:" + photoFile.getAbsolutePath(); + takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, + Uri.fromFile(photoFile)); + } else { + takePictureIntent = null; + } + } + Intent contentSelectionIntent = new Intent(Intent.ACTION_GET_CONTENT); + contentSelectionIntent.addCategory(Intent.CATEGORY_OPENABLE); + contentSelectionIntent.setType("image/*"); + Intent[] intentArray; + if (takePictureIntent != null) { + intentArray = new Intent[]{takePictureIntent}; + } else { + intentArray = new Intent[0]; + } + Intent chooserIntent = new Intent(Intent.ACTION_CHOOSER); + chooserIntent.putExtra(Intent.EXTRA_INTENT, contentSelectionIntent); + chooserIntent.putExtra(Intent.EXTRA_TITLE, "Image Chooser"); + chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, intentArray); + AppLog.d(this, "startActivityForResult"); + startActivityForResult(chooserIntent, MainActivity.INPUT_FILE_REQUEST_CODE_NEW); + return true; + } + + @Override + public void legacyImageUpload(ValueCallback uploadMsg, String acceptType, String capture) { + AppLog.v(this, "openFileChooser(ValCallback, String, String"); + imageUploadFilePathCallbackOld = uploadMsg; + Intent intent = new Intent(); + intent.setType("image/*"); + intent.setAction(Intent.ACTION_GET_CONTENT); + intent.putExtra("return-data", true); + intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + AppLog.v(this, "startActivityForResult"); + startActivityForResult(Intent.createChooser(intent, "Select Picture"), MainActivity.INPUT_FILE_REQUEST_CODE_OLD); + } + }; + + private class JavaScriptInterface { + @SuppressWarnings("unused") + @JavascriptInterface + public void setUserProfile(final String webMessage) throws JSONException { + final Activity activity = getActivity(); + if (activity != null) { + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + 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()); + } + }); + } + } + }); + } + } + + @SuppressWarnings("unused") + @JavascriptInterface + public void contentHasBeenShared() { + if (getActivity() != null) { + ((MainActivity) getActivity()).setTextToBeShared(null); + } + } + } + + @Override + public String getFragmentTag() { + return TAG; + } +} 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 new file mode 100644 index 000000000..099d505f0 --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/activity/MainActivity.java @@ -0,0 +1,1253 @@ +/* + 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.annotation.SuppressLint; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.content.res.ColorStateList; +import android.graphics.Color; +import android.graphics.drawable.LayerDrawable; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.support.annotation.NonNull; +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.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.Toolbar; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.WindowManager; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; +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.DiasporaPodList; +import com.github.dfa.diaspora_android.data.DiasporaUserProfile; +import com.github.dfa.diaspora_android.listener.DiasporaUserProfileChangedListener; +import com.github.dfa.diaspora_android.listener.IntellihideToolbarActivityListener; +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.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; +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; + +public class MainActivity extends ThemedActivity + implements NavigationView.OnNavigationItemSelectedListener, + DiasporaUserProfileChangedListener, + CustomTabActivityHelper.ConnectionCallback, + IntellihideToolbarActivityListener, + PodSelectionDialog.PodSelectionDialogResultListener { + + + public static final int REQUEST_CODE_ASK_PERMISSIONS = 123; + public static final int REQUEST_CODE__ACCESS_EXTERNAL_STORAGE = 124; + public static final int INPUT_FILE_REQUEST_CODE_NEW = 1; + public static final int INPUT_FILE_REQUEST_CODE_OLD = 2; + + public static final String ACTION_OPEN_URL = "com.github.dfa.diaspora_android.MainActivity.open_url"; + public static final String ACTION_OPEN_EXTERNAL_URL = "com.github.dfa.diaspora_android.MainActivity.open_external_url"; + public static final String ACTION_CHANGE_ACCOUNT = "com.github.dfa.diaspora_android.MainActivity.change_account"; + public static final String ACTION_CLEAR_CACHE = "com.github.dfa.diaspora_android.MainActivity.clear_cache"; + public static final String ACTION_UPDATE_TITLE_FROM_URL = "com.github.dfa.diaspora_android.MainActivity.set_title"; + public static final String URL_MESSAGE = "URL_MESSAGE"; + public static final String EXTRA_URL = "com.github.dfa.diaspora_android.extra_url"; + public static final String CONTENT_HASHTAG = "content://com.github.dfa.diaspora_android.mainactivity/"; + + private App app; + private CustomTabActivityHelper customTabActivityHelper; + private AppSettings _appSettings; + private DiasporaUrlHelper urls; + private DiasporaUserProfile diasporaUserProfile; + private final Handler uiHandler = new Handler(); + private OpenExternalLinkReceiver brOpenExternalLink; + private BroadcastReceiver brSetTitle; + private Snackbar snackbarExitApp, snackbarNoInternet, snackbarLastVisitedTimestampInStream; + private FragmentManager fm; + private CustomTabsSession customTabsSession; + + /** + * UI Bindings + */ + @BindView(R.id.main__appbar) + AppBarLayout appBarLayout; + + @BindView(R.id.main__topbar) + Toolbar toolbarTop; + + @BindView(R.id.fragment_container) + FrameLayout fragmentContainer; + + @BindView(R.id.main__navigaion_view) + NavigationView navView; + + @BindView(R.id.main__navdrawer) + DrawerLayout navDrawer; + + RelativeLayout navDrawerLayout; + LinearLayout navProfilePictureArea; + + + // NavHeader cannot be bound by Butterknife + private TextView navheaderTitle; + private TextView navheaderDescription; + private ImageView navheaderImage; + + private String textToBeShared; + + + /** + * END UI Bindings + */ + + @SuppressLint("SetJavaScriptEnabled") + @Override + protected void onCreate(Bundle savedInstanceState) { + 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(); + diasporaUserProfile = app.getDiasporaUserProfile(); + diasporaUserProfile.setCallbackHandler(uiHandler); + diasporaUserProfile.setListener(this); + urls = new DiasporaUrlHelper(_appSettings); + customTabActivityHelper = new CustomTabActivityHelper(); + customTabActivityHelper.setConnectionCallback(this); + ProxyHandler.getInstance().updateProxySettings(this); + + fm = getSupportFragmentManager(); + setupUI(); + + brOpenExternalLink = new OpenExternalLinkReceiver(this); + brSetTitle = new UpdateTitleReceiver(app, urls, new UpdateTitleReceiver.TitleCallback() { + public void setTitle(String url, int resId) { + ThemedFragment top = getTopFragment(); + if (top != null && top.getFragmentTag().equals(DiasporaStreamFragment.TAG)) { + MainActivity.this.setTitle(resId); + showLastVisitedTimestampMessageIfNeeded(url); + } + } + + public void setTitle(String url, String title) { + ThemedFragment top = getTopFragment(); + if (top != null && top.getFragmentTag().equals(DiasporaStreamFragment.TAG)) { + MainActivity.this.setTitle(title); + } + } + }); + + if (!_appSettings.hasPod()) { + AppLog.d(this, "We have no pod. Show PodSelectionFragment"); + updateNavigationViewEntryVisibilities(); + showFragment(getFragment(PodSelectionFragment.TAG)); + } else { + AppLog.d(this, "Pod found. Handle intents."); + //Handle intent + Intent intent = getIntent(); + if (intent != null && intent.getAction() != null) { + handleIntent(intent); + } else { + openDiasporaUrl(urls.getStreamUrl()); + } + } + + // Show first start / update dialog + try { + 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(); + } + } + + /** + * Setup the user interface. Set up both toolbars and initialize the snackbars. + * Initialize the navigation drawer and apply intellihide settings. + */ + private void setupUI() { + AppLog.i(this, "setupUI()"); + + // Setup _toolbar + setSupportActionBar(toolbarTop); + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setDisplayHomeAsUpEnabled(true); + } + setTitle(R.string.app_name); + + //Setup snackbar + snackbarExitApp = Snackbar + .make(fragmentContainer, R.string.confirm_exit, Snackbar.LENGTH_LONG) + .setAction(android.R.string.yes, new View.OnClickListener() { + public void onClick(View view) { + finish(); + moveTaskToBack(true); + } + }); + snackbarLastVisitedTimestampInStream = + Snackbar.make(fragmentContainer, + R.string.jump_to_last_visited_timestamp_in_stream, 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); + + // Load app settings + setupNavigationSlider(); + AppLog.v(this, "UI successfully set up"); + } + + /** + * 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 ThemedFragment getFragment(String fragmentTag) { + ThemedFragment fragment = (ThemedFragment) fm.findFragmentByTag(fragmentTag); + if (fragment != null) { + return fragment; + } else { + switch (fragmentTag) { + case DiasporaStreamFragment.TAG: + DiasporaStreamFragment dsf = new DiasporaStreamFragment(); + fm.beginTransaction().add(dsf, fragmentTag).commit(); + return dsf; + case BrowserFragment.TAG: + 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(); + return psf; + default: + AppLog.e(this, "Invalid Fragment Tag: " + fragmentTag + + "\nAdd Fragments Tag to getFragment()'s switch case."); + return getTopFragment(); + } + } + } + + /** + * Show DiasporaStreamFragment if necessary and load URL url + * + * @param url URL to load in the DiasporaStreamFragment + */ + public void openDiasporaUrl(final String url) { + AppLog.v(this, "openDiasporaUrl()"); + 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/Diaspora-for-Android/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) { + if (url.equals(urls.getStreamUrl()) && diasporaUserProfile.hasLastVisitedTimestampInStream()) { + snackbarLastVisitedTimestampInStream.show(); + diasporaUserProfile.resetLastVisitedPositionInStream(); + } + } + + /** + * Show the Fragment fragment in R.id.fragment_container. If the fragment was already visible, do nothing. + * + * @param fragment Fragment to show + */ + protected void showFragment(ThemedFragment fragment) { + AppLog.v(this, "showFragment()"); + 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(); + setToolbarIntellihide(_appSettings.isIntellihideToolbars() && fragment.isAllowedIntellihide()); + } else { + AppLog.v(this, "Fragment was already visible. Do nothing."); + } + } + + /** + * Initialize the navigation slider + */ + private void setupNavigationSlider() { + ActionBarDrawerToggle toggle = new ActionBarDrawerToggle( + this, navDrawer, toolbarTop, R.string.navigation_drawer_open, R.string.navigation_drawer_close); + navDrawer.addDrawerListener(toggle); + toggle.syncState(); + + //NavigationView navView = ButterKnife.findById(this, R.id.nav_view); + navView.setNavigationItemSelectedListener(this); + + View navHeader = navView.getHeaderView(0); + navProfilePictureArea = ButterKnife.findById(navHeader, R.id.nav_profile_picture); + navDrawerLayout = ButterKnife.findById(navHeader, R.id.nav_drawer); + //Handle clicks on profile picture + navProfilePictureArea.setOnClickListener(new View.OnClickListener() { + public void onClick(View view) { + navDrawer.closeDrawer(GravityCompat.START); + if (!_appSettings.getProfileId().equals("")) { + openDiasporaUrl(urls.getProfileUrl()); + } + } + }); + navheaderTitle = ButterKnife.findById(navHeader, R.id.navheader_title); + 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.getPod() != null) { + navheaderDescription.setText(_appSettings.getPod().getName()); + } + 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) + if (avatarUrl.startsWith("/assets/user/default")) { + AppLog.v(this, "Avatar appears to be an asset. Display launcher icon instead (avatarUrl=" + avatarUrl + ")"); + navheaderImage.setImageResource(R.drawable.ic_launcher); + } else { + // Try to load image + if (!app.getAvatarImageLoader().loadToImageView(navheaderImage)) { + // If not yet loaded, start download + AppLog.v(this, "Avatar not cached. Start download: " + avatarUrl); + app.getAvatarImageLoader().startImageDownload(navheaderImage, avatarUrl); + } + } + } else if (BuildConfig.IS_TEST_BUILD) { + navheaderImage.setImageResource(R.drawable.ic_launcher_test); + } + updateNavigationViewEntryVisibilities(); + } + + protected void updateNavigationViewEntryVisibilities() { + Menu navMenu = navView.getMenu(); + + // Initially show all items visible when logged in + 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_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.isVisibleInNavToggleMobileDesktop()); + navMenu.findItem(R.id.nav_dandelion).setVisible(_appSettings.isVisibleInNavDandelionAccount()); + + + // Hide whole group (for logged in use) if no pod was selected + 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 + * + * @param view selected view + */ + @OnClick(R.id.main__topbar) + public void onToolBarClicked(View view) { + AppLog.i(this, "onToolBarClicked()"); + if (_appSettings.isTopbarStreamShortcutEnabled() && _appSettings.hasPod()) { + onNavigationItemSelected(navView.getMenu().findItem(R.id.nav_stream)); + } + } + + /** + * Forward incoming intents to handleIntent() + * + * @param intent incoming + */ + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + handleIntent(intent); + } + + /** + * Handle intents and execute intent specific actions + * + * @param intent intent to get handled + */ + private void handleIntent(Intent intent) { + AppLog.i(this, "handleIntent()"); + if (intent == null) { + AppLog.v(this, "Intent was null"); + return; + } + + String action = intent.getAction(); + String type = intent.getType(); + String loadUrl = null; + AppLog.v(this, "Action: " + action + " Type: " + type); + if (Intent.ACTION_MAIN.equals(action)) { + loadUrl = urls.getStreamUrl(); + } else if (ACTION_OPEN_URL.equals(action)) { + loadUrl = intent.getStringExtra(URL_MESSAGE); + } else if (Intent.ACTION_VIEW.equals(action) && intent.getDataString() != null) { + Uri data = intent.getData(); + if (data != null && data.toString().startsWith(CONTENT_HASHTAG)) { + handleHashtag(intent); + return; + } else { + loadUrl = intent.getDataString(); + AppLog.v(this, "Intent has a delicious URL for us: " + loadUrl); + } + } else if (ACTION_CHANGE_ACCOUNT.equals(action)) { + AppLog.v(this, "Reset pod data and show PodSelectionFragment"); + _appSettings.setPod(null); + runOnUiThread(new Runnable() { + public void run() { + navheaderTitle.setText(R.string.app_name); + navheaderDescription.setText(R.string.app_subtitle); + navheaderImage.setImageResource(R.drawable.ic_launcher); + app.resetPodData(((DiasporaStreamFragment) getFragment(DiasporaStreamFragment.TAG)).getWebView()); + } + }); + showFragment(getFragment(PodSelectionFragment.TAG)); + } else if (ACTION_CLEAR_CACHE.equals(action)) { + AppLog.v(this, "Clear WebView cache"); + runOnUiThread(new Runnable() { + public void run() { + ContextMenuWebView wv = ((DiasporaStreamFragment) getFragment(DiasporaStreamFragment.TAG)).getWebView(); + if (wv != null) { + wv.clearCache(true); + } + } + }); + + } else if (Intent.ACTION_SEND.equals(action) && type != null) { + switch (type) { + case "text/plain": + if (intent.hasExtra(Intent.EXTRA_SUBJECT)) { + handleSendSubject(intent); + } else { + handleSendText(intent); + } + break; + case "image/*": + handleSendImage(intent); //TODO: Add intent filter to Manifest and implement method + break; + } + } 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_nav_followed_tags".equals(action)) { + showFragment(getFragment(TagListFragment.TAG)); + return; + } else if ("sc_aspects".equals(action)) { + showFragment(getFragment(AspectListFragment.TAG)); + return; + } else if ("sc_activities".equals(action)) { + openDiasporaUrl(urls.getActivityUrl()); + return; + } + //Catch split screen recreation + if (action != null && action.equals(Intent.ACTION_MAIN) && getTopFragment() != null) { + return; + } + + if (loadUrl != null) { + navDrawer.closeDrawers(); + openDiasporaUrl(loadUrl); + } + } + + /** + * Handle activity results + * + * @param requestCode reqCode + * @param resultCode resCode + * @param data data + */ + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + AppLog.v(this, "onActivityResult(): " + requestCode); + super.onActivityResult(requestCode, resultCode, data); + } + + /** + * Return the fragment which is currently displayed in R.id.fragment_container + * + * @return top fragment or null if there is none displayed + */ + private ThemedFragment getTopFragment() { + return (ThemedFragment) fm.findFragmentById(R.id.fragment_container); + } + + /** + * Handle presses on the back button + */ + @Override + public void onBackPressed() { + AppLog.v(this, "onBackPressed()"); + if (navDrawer.isDrawerOpen(navView)) { + navDrawer.closeDrawer(navView); + return; + } + ThemedFragment top = getTopFragment(); + if (top != null) { + AppLog.v(this, "Top Fragment is not null"); + if (!top.onBackPressed()) { + AppLog.v(this, "Top Fragment.onBackPressed was false"); + AppLog.v(this, "BackStackEntryCount: " + fm.getBackStackEntryCount()); + if (fm.getBackStackEntryCount() > 0) { + fm.popBackStack(); + } else { + snackbarExitApp.show(); + } + return; + } else { + AppLog.v(this, "Top Fragment.onBackPressed was true"); + return; + } + } + + if (!snackbarExitApp.isShown()) { + snackbarExitApp.show(); + } + } + + @Override + protected void onStart() { + super.onStart(); + customTabActivityHelper.bindCustomTabsService(this); + } + + @Override + protected void onStop() { + super.onStop(); + customTabActivityHelper.unbindCustomTabsService(this); + } + + @Override + protected void onPause() { + AppLog.v(this, "onPause()"); + AppLog.v(this, "Unregister BroadcastReceivers"); + LocalBroadcastManager.getInstance(this).unregisterReceiver(brSetTitle); + LocalBroadcastManager.getInstance(this).unregisterReceiver(brOpenExternalLink); + super.onPause(); + } + + @Override + protected void onResume() { + AppLog.v(this, "onResume()"); + super.onResume(); + AppLog.v(this, "Register BroadcastReceivers"); + LocalBroadcastManager.getInstance(this).registerReceiver(brSetTitle, new IntentFilter(ACTION_UPDATE_TITLE_FROM_URL)); + LocalBroadcastManager.getInstance(this).registerReceiver(brOpenExternalLink, new IntentFilter(ACTION_OPEN_EXTERNAL_URL)); + invalidateOptionsMenu(); + _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. + * Also add menu items of the displayed fragment + * + * @param menu top _toolbar + * @return boolean + */ + @Override + public boolean onCreateOptionsMenu(Menu menu) { + AppLog.v(this, "onCreateOptionsMenu()"); + boolean cache; + + //Clear the menus + menu.clear(); + + ThemedFragment top = getTopFragment(); + if (top != null) { + 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 + * + * @param menu menu + * @return boolean + */ + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + MenuItem item; + updateNavigationViewEntryVisibilities(); + + if ((item = menu.findItem(R.id.action_notifications)) != null) { + LayerDrawable icon = (LayerDrawable) item.getIcon(); + BadgeDrawable.setBadgeCount(this, icon, diasporaUserProfile.getNotificationCount()); + } + + if ((item = menu.findItem(R.id.action_conversations)) != null) { + LayerDrawable icon = (LayerDrawable) item.getIcon(); + BadgeDrawable.setBadgeCount(this, icon, diasporaUserProfile.getUnreadMessagesCount()); + } + return super.onPrepareOptionsMenu(menu); + } + + /** + * Handle clicks on the optionsmenu + * + * @param item item + * @return boolean + */ + @Override + public boolean onOptionsItemSelected(MenuItem item) { + AppLog.i(this, "onOptionsItemSelected()"); + switch (item.getItemId()) { + case R.id.action_notifications: { + if (_appSettings.isExtendedNotificationsActivated()) { + return true; + } + //Otherwise we execute the action of action_notifications_all + } + case R.id.action_notifications_all: { + if (WebHelper.isOnline(MainActivity.this)) { + openDiasporaUrl(urls.getNotificationsUrl()); + return true; + } else { + snackbarNoInternet.show(); + return false; + } + } + + + case R.id.action_notifications_also_commented: { + if (WebHelper.isOnline(MainActivity.this)) { + openDiasporaUrl(urls.getSuburlNotificationsAlsoCommentedUrl()); + return true; + } else { + snackbarNoInternet.show(); + return false; + } + } + + case R.id.action_notifications_comment_on_post: { + if (WebHelper.isOnline(MainActivity.this)) { + openDiasporaUrl(urls.getSuburlNotificationsCommentOnPostUrl()); + return true; + } else { + snackbarNoInternet.show(); + return false; + } + } + + case R.id.action_notifications_liked: { + if (WebHelper.isOnline(MainActivity.this)) { + openDiasporaUrl(urls.getSuburlNotificationsLikedUrl()); + return true; + } else { + snackbarNoInternet.show(); + return false; + } + } + + case R.id.action_notifications_mentioned: { + if (WebHelper.isOnline(MainActivity.this)) { + openDiasporaUrl(urls.getSuburlNotificationsMentionedUrl()); + return true; + } else { + snackbarNoInternet.show(); + return false; + } + } + + case R.id.action_notifications_reshared: { + if (WebHelper.isOnline(MainActivity.this)) { + openDiasporaUrl(urls.getSuburlNotificationsResharedUrl()); + return true; + } else { + snackbarNoInternet.show(); + return false; + } + } + + case R.id.action_notifications_started_sharing: { + if (WebHelper.isOnline(MainActivity.this)) { + openDiasporaUrl(urls.getSuburlNotificationsStartedSharingUrl()); + return true; + } else { + snackbarNoInternet.show(); + return false; + } + } + + case R.id.action_conversations: { + if (WebHelper.isOnline(MainActivity.this)) { + openDiasporaUrl(urls.getConversationsUrl()); + return true; + } else { + snackbarNoInternet.show(); + return false; + } + } + + case R.id.action_compose: { + if (WebHelper.isOnline(MainActivity.this)) { + openDiasporaUrl(urls.getNewPostUrl()); + } else { + snackbarNoInternet.show(); + } + return true; + } + + case R.id.action_search: { + if (WebHelper.isOnline(MainActivity.this)) { + 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 = layout.findViewById(R.id.dialog_search__input); + input.setMaxLines(1); + input.setSingleLine(true); + ThemeHelper.updateEditTextColor(input); + final DialogInterface.OnClickListener clickListener = new DialogInterface.OnClickListener() { + @Override + 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(); + } else { + openDiasporaUrl(which == DialogInterface.BUTTON_NEGATIVE ? urls.getSearchPeopleUrl(query) : urls.getSearchTagsUrl(query)); + } + getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN); + imm.hideSoftInputFromWindow(input.getWindowToken(), 0); + } + }; + + 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) + .create(); + + input.setOnEditorActionListener(new TextView.OnEditorActionListener() { + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if (actionId == EditorInfo.IME_ACTION_DONE) { + dialog.hide(); + clickListener.onClick(null, 0); + return true; + } + return false; + } + }); + + // Popup keyboard + dialog.show(); + input.requestFocus(); + getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); + imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0); + + } else { + snackbarNoInternet.show(); + } + return true; + } + } + + return super.onOptionsItemSelected(item); + } + + @Override + public void onUserProfileNameChanged(DiasporaUserProfile diasporaUserProfile, String name) { + AppLog.i(this, "onUserProfileNameChanged()"); + // Update the profile name in the navigation slider + navheaderTitle.setText(name); + } + + @Override + public void onUserProfileAvatarChanged(DiasporaUserProfile diasporaUserProfile, String avatarUrl) { + AppLog.i(this, "onUserProfileAvatarChanged()"); + // Update the profile picture in the navigation slider + app.getAvatarImageLoader().startImageDownload(navheaderImage, avatarUrl); + } + + /** + * Handle hashtag clicks. Open the new-post-url and inject the clicked hashtag into the post-editor + * + * @param intent intent + */ + private void handleHashtag(Intent intent) { + AppLog.v(this, "handleHashtag()"); + try { + setSharedTexts(null, intent.getData().toString().split("/")[3]); + } catch (Exception e) { + AppLog.e(this, e.toString()); + } + openDiasporaUrl(urls.getNewPostUrl()); + } + + /** + * Open the new-post-url and inject text that was shared into the app into the post editors text field + * + * @param intent shareTextIntent + */ + private void handleSendText(Intent intent) { + AppLog.v(this, "handleSendText()"); + try { + setSharedTexts(null, intent.getStringExtra(Intent.EXTRA_TEXT)); + openDiasporaUrl(urls.getNewPostUrl()); + } catch (Exception e) { + AppLog.e(this, e.toString()); + } + } + + /** + * Handle sent text + subject + * + * @param intent intent + */ + private void handleSendSubject(Intent intent) { + AppLog.v(this, "handleSendSubject()"); + try { + setSharedTexts(intent.getStringExtra(Intent.EXTRA_SUBJECT), intent.getStringExtra(Intent.EXTRA_TEXT)); + openDiasporaUrl(urls.getNewPostUrl()); + } catch (Exception e) { + AppLog.e(this, e.toString()); + } + } + + /** + * TODO: MOVE + * Set sharedText variable to escaped and formatted subject + body. + * If subject is null, only the body will be set. Else the subject will be set as header. + * Depending on whether the user has the setting isAppendSharedViaApp set, a reference to + * the app will be added at the bottom + * + * @param sharedSubject post subject or null + * @param sharedBody post text + */ + private void setSharedTexts(String sharedSubject, String sharedBody) { + AppLog.i(this, "setSharedTexts()"); + String body = WebHelper.replaceUrlWithMarkdown(sharedBody); + if (_appSettings.isAppendSharedViaApp()) { + AppLog.v(this, "Append app reference to shared text"); + body = body + "\n\n" + getString(R.string.shared_via_app); + } + final String escapedBody = WebHelper.escapeHtmlText(body); + if (sharedSubject != null) { + AppLog.v(this, "Append subject to shared text"); + String escapedSubject = WebHelper.escapeHtmlText(WebHelper.replaceUrlWithMarkdown(sharedSubject)); + AppLog.v(this, "Set shared text; Subject: \"" + escapedSubject + "\" Body: \"" + escapedBody + "\""); + textToBeShared = "**" + escapedSubject + "** " + escapedBody; + } else { + AppLog.v(this, "Set shared text; Subject: \"null\" Body: \"" + sharedBody + "\""); + textToBeShared = escapedBody; + } + } + + /** + * Share an image shared to the app via diaspora + * + * @param intent shareImageIntent + */ + //TODO: Implement some day + private void handleSendImage(Intent intent) { + AppLog.i(this, "handleSendImage()"); + final Uri imageUri = intent.getParcelableExtra(Intent.EXTRA_STREAM); + if (imageUri != null) { + AppLog.v(this, "imageUri is not null. Handle shared image"); + } else { + AppLog.w(this, "imageUri is null. Cannot precede."); + } + Toast.makeText(this, "Not yet implemented.", Toast.LENGTH_SHORT).show(); + } + + @Override + 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 + invalidateOptionsMenu(); + } + + + @Override + 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 + invalidateOptionsMenu(); + } + + @Override + public void onCustomTabsConnected() { + if (customTabsSession == null) { + AppLog.i(this, "CustomTabs warmup: " + customTabActivityHelper.warmup(0)); + customTabsSession = customTabActivityHelper.getSession(); + } + } + + @Override + public void onPodSelectionDialogResult(DiasporaPodList.DiasporaPod pod, boolean accepted) { + if (accepted) { + invalidateOptionsMenu(); + navheaderDescription.setText(pod.getName()); + } + } + + @Override + public void onCustomTabsDisconnected() { + + } + + @SuppressWarnings("StatementWithEmptyBody") + @Override + public boolean onNavigationItemSelected(MenuItem item) { + AppLog.v(this, "onNavigationItemsSelected()"); + // Handle navigation view item clicks here. + switch (item.getItemId()) { + case R.id.nav_stream: { + if (WebHelper.isOnline(MainActivity.this)) { + openDiasporaUrl(urls.getStreamUrl()); + } else { + snackbarNoInternet.show(); + } + } + break; + + case R.id.nav_profile: { + if (!_appSettings.getProfileId().equals("")) { + openDiasporaUrl(urls.getProfileUrl()); + } + } + break; + + case R.id.nav_followed_tags: { + showFragment(getFragment(TagListFragment.TAG)); + } + break; + + case R.id.nav_aspects: { + showFragment(getFragment(AspectListFragment.TAG)); + } + break; + + case R.id.nav_contacts: { + if (WebHelper.isOnline(MainActivity.this)) { + openDiasporaUrl(urls.getContactsUrl()); + } else { + snackbarNoInternet.show(); + } + } + break; + + case R.id.nav_activities: { + if (WebHelper.isOnline(MainActivity.this)) { + openDiasporaUrl(urls.getActivityUrl()); + } else { + snackbarNoInternet.show(); + } + } + break; + + case R.id.nav_liked: { + if (WebHelper.isOnline(MainActivity.this)) { + openDiasporaUrl(urls.getLikedPostsUrl()); + } else { + snackbarNoInternet.show(); + } + } + break; + + case R.id.nav_commented: { + if (WebHelper.isOnline(MainActivity.this)) { + openDiasporaUrl(urls.getCommentedUrl()); + } else { + snackbarNoInternet.show(); + } + } + break; + + case R.id.nav_mentions: { + if (WebHelper.isOnline(MainActivity.this)) { + openDiasporaUrl(urls.getMentionsUrl()); + } else { + snackbarNoInternet.show(); + } + } + break; + + case R.id.nav_public: { + if (WebHelper.isOnline(MainActivity.this)) { + openDiasporaUrl(urls.getPublicUrl()); + } else { + snackbarNoInternet.show(); + } + } + break; + + case R.id.nav_reports: { + if (WebHelper.isOnline(MainActivity.this)) { + openDiasporaUrl(urls.getReportsUrl()); + } else { + snackbarNoInternet.show(); + } + } + break; + + case R.id.nav_statistics: { + if (WebHelper.isOnline(MainActivity.this)) { + openDiasporaUrl(urls.getStatisticsUrl()); + } else { + snackbarNoInternet.show(); + } + } + break; + + case R.id.nav_toggle_desktop_page: { + openDiasporaUrl(urls.getToggleMobileUrl()); + } + break; + + case R.id.nav_dandelion: { + openDiasporaUrl(urls.getProfileUrl("48b78420923501341ef3782bcb452bd5")); + } + break; + + case R.id.nav_exit: { + moveTaskToBack(true); + finish(); + } + break; + + case R.id.nav_settings: { + startActivity(new Intent(this, SettingsActivity.class)); + } + break; + + case R.id.nav_about: { + startActivity(new Intent(MainActivity.this, AboutActivity.class)); + } + break; + } + + navDrawer.closeDrawer(GravityCompat.START); + return true; + } + + /** + * React to results of requestPermission + * + * @param requestCode resCode + * @param permissions requested permissions + * @param grantResults granted results + */ + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + switch (requestCode) { + case REQUEST_CODE__ACCESS_EXTERNAL_STORAGE: + if (grantResults.length == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + AppLog.i(this, "onRequestPermissionsResult: Permission to access external storage granted"); + Toast.makeText(this, R.string.permission_granted_try_again, Toast.LENGTH_SHORT).show(); + } else { + AppLog.w(this, "onRequestPermissionsResult: Permission to access external storage denied"); + Toast.makeText(this, R.string.permission_denied, Toast.LENGTH_SHORT).show(); + } + return; + + default: + super.onRequestPermissionsResult(requestCode, permissions, + grantResults); + } + } + + /** + * Return the string that will be shared into the new-post-editor + * + * @return String + */ + public String getTextToBeShared() { + return textToBeShared; + } + + /** + * Set the string that will be shared into the new-post-editor + * + * @param textToBeShared text that will be shared into the post-editor + */ + public void setTextToBeShared(String textToBeShared) { + this.textToBeShared = textToBeShared; + } + + @Override + protected void applyColorToViews() { + ThemeHelper.updateToolbarColor(toolbarTop); + 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); + } + + public void setToolbarIntellihide(boolean enable) { + AppBarLayout.LayoutParams params = (AppBarLayout.LayoutParams) toolbarTop.getLayoutParams(); + if (enable) { + AppLog.d(this, "Enable Intellihide"); + params.setScrollFlags(toolbarDefaultScrollFlags); + + } 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 new file mode 100644 index 000000000..8f6c5ad55 --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/activity/PodSelectionFragment.java @@ -0,0 +1,311 @@ +/* + 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.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +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.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.webkit.CookieManager; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.ListView; +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.DiasporaPodList; +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.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 org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; + +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; + +/** + * Fragment that lets the user choose a Pod + * Created by vanitas on 01.10.16. + */ + +public class PodSelectionFragment extends ThemedFragment implements SearchView.OnQueryTextListener, PodSelectionDialog.PodSelectionDialogResultListener { + public static final String TAG = "com.github.dfa.diaspora_android.PodSelectionFragment"; + + @BindView(R.id.podselection__fragment__listpods) + protected ListView listViewPod; + + @BindView(R.id.podselection__fragment__root) + RelativeLayout rootView; + + @BindView(R.id.podselection__fragment__button_use_custom_pod) + AppCompatButton buttonUseCustomPod; + + + protected App app; + protected AppSettings appSettings; + private DiasporaPodList podList; + private ArrayAdapter listViewPodAdapter; + private String filterString = ""; + + @Override + 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(); + + // Load local podlist + podList = new DiasporaPodList(); + mergePodlistWithRessources(podList); + podList.setTrackMergeChanges(true); + updateListedPods(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + listViewPod.setNestedScrollingEnabled(true); + } + + listViewPod.setTextFilterEnabled(true); + listViewPod.setOnItemClickListener(new AdapterView.OnItemClickListener() { + public void onItemClick(AdapterView adapterView, View view, int i, long l) { + String text = ((TextView) view).getText().toString(); + for (DiasporaPod pod : podList) { + if (pod.getPodUrl().getHost().equals(text)) { + showPodSelectionDialog(pod); + return; + } + } + + } + }); + LocalBroadcastManager.getInstance(getContext()).registerReceiver(podListReceiver, new IntentFilter(FetchPodsService.MESSAGE_PODS_RECEIVED)); + ActivityUtils.get(getActivity()).showInfoIfUserNotConnectedToInternet(listViewPod); + } + + public void mergePodlistWithRessources(DiasporaPodList podlist) { + String sPodlist = ContextUtils.get().readTextfileFromRawRes(R.raw.podlist, "", ""); + try { + JSONObject jPodlist = new JSONObject(sPodlist); + podlist.mergeWithNewerEntries(new DiasporaPodList().fromJson(jPodlist)); + } catch (JSONException e) { + e.printStackTrace(); + } + } + + @OnClick(R.id.podselection__fragment__button_use_custom_pod) + public void onPodButtonClicked(View v) { + showPodSelectionDialog(new DiasporaPod()); + } + + @Override + public String getFragmentTag() { + return TAG; + } + + private final BroadcastReceiver podListReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent.hasExtra(FetchPodsService.EXTRA_PODLIST)) { + Bundle extras = intent.getExtras(); + DiasporaPodList newPods = (DiasporaPodList) extras.get(FetchPodsService.EXTRA_PODLIST); + if (newPods != null && newPods.getPods().size() > 0) { + try { + podList.mergeWithNewerEntries(newPods); + updateListedPods(); + } catch (JSONException ignored) { + } + } else { + Snackbar.make(listViewPod, R.string.podlist_error, Snackbar.LENGTH_SHORT).show(); + } + } + } + }; + + @Override + protected void applyColorToViews() { + int dividerHeight = listViewPod.getDividerHeight(); + rootView.setBackgroundColor(appSettings.isAmoledColorMode() ? Color.BLACK : Color.WHITE); + listViewPod.setDivider(new ColorDrawable(Color.GRAY)); + listViewPod.setDividerHeight(dividerHeight); + int bgcolor = appSettings.isAmoledColorMode() ? Color.BLACK : appSettings.getAccentColor(); + buttonUseCustomPod.setBackgroundColor(bgcolor); + buttonUseCustomPod.setTextColor(_cu.shouldColorOnTopBeLight(bgcolor) ? Color.WHITE : Color.BLACK); + + } + + @Override + public void onResume() { + super.onResume(); + Intent i = new Intent(getContext(), FetchPodsService.class); + getContext().startService(i); + } + + private void updateListedPods() { + final ArrayList listedPodsList = new ArrayList<>(); + for (DiasporaPod pod : this.podList) { + listedPodsList.add(pod.getPodUrl().getHost()); + } + + listViewPodAdapter = new ArrayAdapter( + getContext(), + android.R.layout.simple_list_item_1, + listedPodsList) { + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View view = super.getView(position, convertView, parent); + TextView textView = view.findViewById(android.R.id.text1); + textView.setTextColor(appSettings.isAmoledColorMode() ? Color.GRAY : Color.BLACK); + return view; + } + }; + + // save index and top position + int index = listViewPod.getFirstVisiblePosition(); + View v = listViewPod.getChildAt(0); + int top = (v == null) ? 0 : (v.getTop() - listViewPod.getPaddingTop()); + listViewPod.setAdapter(listViewPodAdapter); + listViewPod.setSelectionFromTop(index, top); + + listViewPodAdapter.getFilter().filter(filterString); + } + + private void showPodSelectionDialog(final DiasporaPod selectedPod) { + PodSelectionDialog dialog = PodSelectionDialog.newInstance(selectedPod, this); + dialog.show(getFragmentManager(), PodSelectionDialog.TAG); + } + + @Override + public void onDestroy() { + LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(podListReceiver); + super.onDestroy(); + } + + @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); + if (searchItem != null) { + SearchView searchView = (SearchView) MenuItemCompat.getActionView(searchItem); + searchView.setOnQueryTextListener(this); + } + + 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 (!ActivityUtils.get(getActivity()).showInfoIfUserNotConnectedToInternet(listViewPod)) { + Intent i = new Intent(getContext(), FetchPodsService.class); + getContext().startService(i); + return true; + } + } + } + return super.onOptionsItemSelected(item); + } + + @Override + public boolean onQueryTextChange(String newText) { + if (listViewPodAdapter != null) { + (listViewPodAdapter).getFilter().filter(newText); + } + return true; + } + + @Override + public void onPodSelectionDialogResult(DiasporaPod pod, boolean accepted) { + System.out.println(accepted + ": " + pod.toString()); + if (accepted) { + app.getSettings().setPod(pod); + + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + CookieManager.getInstance().removeAllCookies(null); + CookieManager.getInstance().removeSessionCookies(null); + } else { + //noinspection deprecation + CookieManager.getInstance().removeAllCookie(); + //noinspection deprecation + CookieManager.getInstance().removeSessionCookie(); + } + } catch (Exception e) { + e.printStackTrace(); + } + + MainActivity mainActivity = (MainActivity) getActivity(); + DiasporaUrlHelper urlHelper = new DiasporaUrlHelper(appSettings); + mainActivity.onPodSelectionDialogResult(pod, accepted); + mainActivity.openDiasporaUrl(urlHelper.getSignInUrl()); + } + } + + + /* + * Dummy implementations + */ + @Override + public boolean onQueryTextSubmit(String query) { + return false; + } + + @Override + public boolean onBackPressed() { + return false; + } + + @Override + public boolean isAllowedIntellihide() { + return false; + } +} \ No newline at end of file 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 new file mode 100644 index 000000000..943dbfcc0 --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/activity/SettingsActivity.java @@ -0,0 +1,496 @@ +package com.github.dfa.diaspora_android.activity; + +import android.annotation.SuppressLint; +import android.app.AlarmManager; +import android.app.FragmentTransaction; +import android.app.PendingIntent; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Build; +import android.os.Bundle; +import android.preference.Preference; +import android.preference.PreferenceFragment; +import android.preference.PreferenceScreen; +import android.support.design.widget.AppBarLayout; +import android.support.v7.widget.Toolbar; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.TextView; +import android.widget.Toast; + +import com.github.dfa.diaspora_android.App; +import com.github.dfa.diaspora_android.R; +import com.github.dfa.diaspora_android.ui.theme.ColorPalette; +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.ThemedPreferenceFragment; +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.web.ProxyHandler; + +import butterknife.BindView; +import butterknife.ButterKnife; +import uz.shift.colorpicker.LineColorPicker; +import uz.shift.colorpicker.OnColorChangedListener; + +public class SettingsActivity extends ThemedActivity implements SharedPreferences.OnSharedPreferenceChangeListener { + + //Toolbar + @BindView(R.id.settings__appbar) + protected AppBarLayout appBarLayout; + + @BindView(R.id.settings__toolbar) + protected Toolbar toolbar; + + private ProxyHandler.ProxySettings oldProxySettings; + + public void onCreate(Bundle b) { + super.onCreate(b); + setContentView(R.layout.settings__activity); + ButterKnife.bind(this); + toolbar.setTitle(R.string.settings); + setSupportActionBar(toolbar); + + toolbar.setNavigationIcon(getResources().getDrawable(R.drawable.ic_arrow_back_white_24px)); + toolbar.setNavigationOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + SettingsActivity.this.onBackPressed(); + } + }); + getAppSettings().registerPreferenceChangedListener(this); + oldProxySettings = getAppSettings().getProxySettings(); + showFragment(SettingsFragmentMaster.TAG, false); + } + + protected void showFragment(String tag, boolean addToBackStack) { + PreferenceFragment fragment = (PreferenceFragment) getFragmentManager().findFragmentByTag(tag); + if (fragment == null) { + switch (tag) { + case SettingsFragmentThemes.TAG: + fragment = new SettingsFragmentThemes(); + break; + case SettingsFragmentNavSlider.TAG: + fragment = new SettingsFragmentNavSlider(); + break; + case SettingsFragmentProxy.TAG: + fragment = new SettingsFragmentProxy(); + break; + case SettingsFragmentDebugging.TAG: + fragment = new SettingsFragmentDebugging(); + break; + case SettingsFragmentMaster.TAG: + default: + fragment = new SettingsFragmentMaster(); + break; + } + } + FragmentTransaction t = getFragmentManager().beginTransaction(); + if (addToBackStack) { + t.addToBackStack(tag); + } + t.replace(R.id.settings__fragment_container, fragment, tag).commit(); + } + + @Override + public void applyColorToViews() { + //Toolbar + ThemeHelper.updateToolbarColor(toolbar); + } + + @Override + protected void onStop() { + ProxyHandler.ProxySettings newProxySettings = getAppSettings().getProxySettings(); + if (!oldProxySettings.equals(newProxySettings)) { + AppLog.d(this, "ProxySettings changed."); + //Proxy on-off? => Restart app + if (oldProxySettings.isEnabled() && !newProxySettings.isEnabled()) { + AppLog.d(this, "Proxy deactivated. Restarting app..."); + Intent restartActivity = new Intent(SettingsActivity.this, MainActivity.class); + PendingIntent pendingIntent = PendingIntent.getActivity(SettingsActivity.this, 12374, restartActivity, PendingIntent.FLAG_CANCEL_CURRENT); + AlarmManager mgr = (AlarmManager) SettingsActivity.this.getSystemService(Context.ALARM_SERVICE); + mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 100, pendingIntent); + System.exit(0); + } //Proxy changed? => Update + else { + ProxyHandler.getInstance().updateProxySettings(this); + } + } + getAppSettings().unregisterPreferenceChangedListener(this); + super.onStop(); + } + + @Override + public void onBackPressed() { + ThemedPreferenceFragment top = getTopFragment(); + 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(); + } + } + super.onBackPressed(); + } + + /** + * Return the fragment which is currently displayed in R.id.fragment_container + * + * @return top fragment or null if there is none displayed + */ + private ThemedPreferenceFragment getTopFragment() { + return (ThemedPreferenceFragment) getFragmentManager().findFragmentById(R.id.settings__fragment_container); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if (key.equals(getString(R.string.pref_key__screen_rotation))) { + this.updateScreenRotation(); + } + } + + public static class SettingsFragmentMaster extends ThemedPreferenceFragment { + public static final String TAG = "com.github.dfa.diaspora_android.settings.SettingsFragmentMaster"; + + public void onCreate(Bundle savedInstances) { + super.onCreate(savedInstances); + getPreferenceManager().setSharedPreferencesName("app"); + addPreferencesFromResource(R.xml.preferences__master); + } + + @Override + public void updateViewColors() { + + } + + @Override + public boolean onPreferenceTreeClick(PreferenceScreen screen, Preference preference) { + if (isAdded() && preference.hasKey()) { + AppSettings settings = ((App) getActivity().getApplication()).getSettings(); + DiasporaUrlHelper diasporaUrlHelper = new DiasporaUrlHelper(settings); + String key = preference.getKey(); + /** Sub-Categories */ + if (settings.isKeyEqual(key, R.string.pref_key__cat_themes)) { + ((SettingsActivity) getActivity()).showFragment(SettingsFragmentThemes.TAG, true); + return true; + } else if (settings.isKeyEqual(key, R.string.pref_key__cat_nav_slider)) { + ((SettingsActivity) getActivity()).showFragment(SettingsFragmentNavSlider.TAG, true); + return true; + } else if (settings.isKeyEqual(key, R.string.pref_key__cat_proxy)) { + ((SettingsActivity) getActivity()).showFragment(SettingsFragmentProxy.TAG, true); + return true; + } else if (settings.isKeyEqual(key, R.string.pref_key__cat_debugging)) { + ((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); + intent.setAction(MainActivity.ACTION_CLEAR_CACHE); + startActivity(intent); + getActivity().finish(); + return true; + } + /** Pod Settings */ + if (settings.isKeyEqual(key, R.string.pref_key__personal_settings)) { + Intent intent = new Intent(getActivity(), MainActivity.class); + intent.setAction(MainActivity.ACTION_OPEN_URL); + intent.putExtra(MainActivity.URL_MESSAGE, diasporaUrlHelper.getPersonalSettingsUrl()); + startActivity(intent); + getActivity().finish(); + return true; + } else if (settings.isKeyEqual(key, R.string.pref_key__manage_tags)) { + Intent intent = new Intent(getActivity(), MainActivity.class); + intent.setAction(MainActivity.ACTION_OPEN_URL); + intent.putExtra(MainActivity.URL_MESSAGE, diasporaUrlHelper.getManageTagsUrl()); + startActivity(intent); + getActivity().finish(); + return true; + } else if (settings.isKeyEqual(key, R.string.pref_key__manage_contacts)) { + Intent intent = new Intent(getActivity(), MainActivity.class); + intent.setAction(MainActivity.ACTION_OPEN_URL); + intent.putExtra(MainActivity.URL_MESSAGE, diasporaUrlHelper.getContactsUrl()); + startActivity(intent); + getActivity().finish(); + return true; + } 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)) + .setNegativeButton(android.R.string.no, null) + .setPositiveButton(android.R.string.yes, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + Intent intent = new Intent(getActivity(), MainActivity.class); + intent.setAction(MainActivity.ACTION_CHANGE_ACCOUNT); + startActivity(intent); + getActivity().finish(); + } + }) + .show(); + return true; + + } + } + return super.onPreferenceTreeClick(screen, preference); + } + + @Override + public String getFragmentTag() { + return TAG; + } + } + + public static class SettingsFragmentThemes extends ThemedPreferenceFragment { + public static final String TAG = "com.github.dfa.diaspora_android.settings.SettingsFragmentThemes"; + + public void onCreate(Bundle savedInstances) { + super.onCreate(savedInstances); + getPreferenceManager().setSharedPreferencesName("app"); + addPreferencesFromResource(R.xml.preferences__sub_themes); + } + + @Override + public void updateViewColors() { + if (isAdded()) { + //Trigger redraw of whole preference screen in order to reflect changes + setPreferenceScreen(null); + addPreferencesFromResource(R.xml.preferences__sub_themes); + } + } + + @Override + public boolean onPreferenceTreeClick(PreferenceScreen screen, Preference preference) { + if (isAdded() && preference.hasKey()) { + String key = preference.getKey(); + if (key.equals(getString(R.string.pref_key__primary_color__preference_click))) { + showColorPickerDialog(1); + return true; + } else if (key.equals(getString(R.string.pref_key__accent_color__preference_click))) { + showColorPickerDialog(2); + return true; + } + } + return super.onPreferenceTreeClick(screen, preference); + } + + @Override + public String getFragmentTag() { + return TAG; + } + + /** + * Show a colorPicker Dialog + * + * @param type 1 -> Primary Color, 2 -> Accent Color + */ + @SuppressLint("InflateParams") + public void showColorPickerDialog(final int type) { + final AppSettings appSettings = ((App) getActivity().getApplication()).getSettings(); + final Context context = getActivity(); + + //Inflate dialog layout + LayoutInflater inflater = getActivity().getLayoutInflater(); + View dialogLayout = inflater.inflate(R.layout.ui__dialog__color_picker, null); + final ThemedAlertDialogBuilder builder = new ThemedAlertDialogBuilder(context, appSettings); + builder.setView(dialogLayout); + + 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.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))); + base.setSelectedColor(current[0]); + shade.setColors(ColorPalette.getColors(context, current[0])); + shade.setSelectedColor(current[1]); + titleBackground.setBackgroundColor(shade.getColor()); + base.setOnColorChangedListener(new OnColorChangedListener() { + @Override + public void onColorChanged(int i) { + shade.setColors(ColorPalette.getColors(context, i)); + titleBackground.setBackgroundColor(i); + if (i == current[0]) { + shade.setSelectedColor(current[1]); + titleBackground.setBackgroundColor(shade.getColor()); + } else { + shade.setSelectedColor(i); + } + } + }); + shade.setOnColorChangedListener(new OnColorChangedListener() { + @Override + public void onColorChanged(int i) { + titleBackground.setBackgroundColor(i); + } + }); + + //Build dialog + builder + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + if (type == 1) { + appSettings.setPrimaryColorSettings(base.getColor(), shade.getColor()); + if (Build.VERSION.SDK_INT >= 21) { + getActivity().getWindow().setStatusBarColor(ThemeHelper.getPrimaryDarkColor()); + } + ((SettingsActivity) getActivity()).applyColorToViews(); + } else { + appSettings.setAccentColorSettings(base.getColor(), shade.getColor()); + } + updateViewColors(); + } + }).show(); + } + } + + public static class SettingsFragmentNavSlider extends ThemedPreferenceFragment { + public static final String TAG = "com.github.dfa.diaspora_android.settings.SettingsFragmentNavSlider"; + + public void onCreate(Bundle savedInstances) { + super.onCreate(savedInstances); + getPreferenceManager().setSharedPreferencesName("app"); + addPreferencesFromResource(R.xml.preferences__sub_navslider_vis); + } + + @Override + public void updateViewColors() { + + } + + @Override + public String getFragmentTag() { + return TAG; + } + } + + public static class SettingsFragmentProxy extends ThemedPreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener { + public static final String TAG = "com.github.dfa.diaspora_android.settings.SettingsFragmentProxy"; + + public void onCreate(Bundle savedInstances) { + super.onCreate(savedInstances); + getPreferenceManager().setSharedPreferencesName("app"); + addPreferencesFromResource(R.xml.preferences__sub_proxy); + SharedPreferences sharedPreferences = getPreferenceScreen().getSharedPreferences(); + sharedPreferences.registerOnSharedPreferenceChangeListener(this); + } + + @Override + public void onActivityCreated(Bundle bundle) { + super.onActivityCreated(bundle); + updateSummaries(); + } + + public void updateSummaries() { + if (isAdded()) { + AppSettings settings = ((App) getActivity().getApplication()).getSettings(); + findPreference(settings.rstr(R.string.pref_key__http_proxy_host)).setSummary(settings.getProxyHttpHost()); + findPreference(settings.rstr(R.string.pref_key__http_proxy_port)).setSummary(Integer.toString(settings.getProxyHttpPort())); + } + } + + @Override + public boolean onPreferenceTreeClick(PreferenceScreen screen, Preference preference) { + if (isAdded() && preference.hasKey()) { + AppSettings appSettings = ((App) getActivity().getApplication()).getSettings(); + String key = preference.getKey(); + 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(); + return true; + } + } + return super.onPreferenceTreeClick(screen, preference); + } + + @Override + public String getFragmentTag() { + return TAG; + } + + @Override + public void updateViewColors() { + + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if (isAdded()) { + if (key.equals(getString(R.string.pref_key__http_proxy_host)) || + key.equals(getString(R.string.pref_key__http_proxy_port))) { + updateSummaries(); + } + } + } + } + + public static class SettingsFragmentDebugging extends ThemedPreferenceFragment { + public static final String TAG = "com.github.dfa.diaspora_android.settings.SettingsFragmentDebugging"; + + public void onCreate(Bundle savedInstances) { + super.onCreate(savedInstances); + getPreferenceManager().setSharedPreferencesName("app"); + addPreferencesFromResource(R.xml.preferences__sub_debugging); + } + + @Override + public void updateViewColors() { + + } + + @Override + public String getFragmentTag() { + return TAG; + } + + @Override + public boolean onPreferenceTreeClick(PreferenceScreen screen, Preference preference) { + if (isAdded() && preference.hasKey()) { + AppSettings appSettings = ((App) getActivity().getApplication()).getSettings(); + String key = preference.getKey(); + if (appSettings.isKeyEqual(key, R.string.pref_key__wipe_settings)) { + showWipeSettingsDialog(); + return true; + } + } + return super.onPreferenceTreeClick(screen, preference); + } + + private void showWipeSettingsDialog() { + final AppSettings appSettings = AppSettings.get(); + + ThemedAlertDialogBuilder builder = new ThemedAlertDialogBuilder(getActivity(), appSettings); + builder.setTitle(R.string.confirmation) + .setMessage(R.string.dialog_content__wipe_settings) + .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); + } + }).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 new file mode 100644 index 000000000..adb832c5f --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/activity/TagListFragment.java @@ -0,0 +1,206 @@ +/* + This file is part of the dandelion*. + + dandelion* is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + dandelion* is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with the dandelion*. + + If not, see . + */ +package com.github.dfa.diaspora_android.activity; + +import android.content.Context; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.os.Bundle; +import android.support.v7.widget.AppCompatImageView; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import com.github.dfa.diaspora_android.App; +import com.github.dfa.diaspora_android.R; +import com.github.dfa.diaspora_android.listener.OnSomethingClickListener; +import com.github.dfa.diaspora_android.ui.theme.ThemedFragment; +import com.github.dfa.diaspora_android.util.AppSettings; +import com.github.dfa.diaspora_android.util.ContextUtils; +import com.github.dfa.diaspora_android.util.DiasporaUrlHelper; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import butterknife.BindView; +import butterknife.ButterKnife; + +/** + * Fragment that shows a list of the HashTags the user follows + */ +public class TagListFragment extends ThemedFragment implements OnSomethingClickListener { + + public static final String TAG = "com.github.dfa.diaspora_android.TagListFragment"; + + @BindView(R.id.fragment_list__recycler_view) + public RecyclerView followedTagsRecyclerView; + + @BindView(R.id.fragment_list__spacer) + public View space; + + @BindView(R.id.fragment_list__root) + public RelativeLayout rootView; + + protected App app; + protected DiasporaUrlHelper urls; + + @Override + protected int getLayoutResId() { + return R.layout.recycler_list__fragment; + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + ButterKnife.bind(this, view); + app = (App) getActivity().getApplication(); + AppSettings appSettings = app.getSettings(); + urls = new DiasporaUrlHelper(appSettings); + + followedTagsRecyclerView.setHasFixedSize(true); + followedTagsRecyclerView.setNestedScrollingEnabled(false); + + RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(getContext()); + followedTagsRecyclerView.setLayoutManager(layoutManager); + + final FollowedTagsAdapter adapter = new FollowedTagsAdapter(appSettings, this); + followedTagsRecyclerView.setAdapter(adapter); + + //Set window title + getActivity().setTitle(R.string.nav_followed_tags); + } + + @Override + public String getFragmentTag() { + return TAG; + } + + @Override + public boolean onBackPressed() { + return false; + } + + @Override + public void onSomethingClicked(Object null1, Integer null2, String tag) { + ((MainActivity) getActivity()).openDiasporaUrl(urls.getSearchTagsUrl(tag)); + } + + @Override + protected void applyColorToViews() { + followedTagsRecyclerView.invalidate(); + if (getAppSettings().isAmoledColorMode()) { + rootView.setBackgroundColor(Color.BLACK); + space.setBackgroundColor(Color.BLACK); + } + } + + public static class FollowedTagsAdapter extends RecyclerView.Adapter { + private boolean isAmoledColorMode; + private AppSettings appSettings; + private String[] followedTagsList; + private List followedTagsFavsList; + private OnSomethingClickListener tagClickedListener; + + static class ViewHolder extends RecyclerView.ViewHolder { + @BindView(R.id.recycler_view__list_item__text) + public TextView title; + @BindView(R.id.recycler_view__list_item__favourite) + AppCompatImageView favouriteImage; + @BindView(R.id.recycler_view__list_item__root) + RelativeLayout root; + + ViewHolder(View v) { + super(v); + ButterKnife.bind(this, v); + } + } + + + FollowedTagsAdapter(AppSettings appSettings, OnSomethingClickListener tagClickedListener) { + this.appSettings = appSettings; + this.followedTagsList = appSettings.getFollowedTags(); + this.followedTagsFavsList = new ArrayList<>(Arrays.asList(appSettings.getFollowedTagsFavs())); + this.tagClickedListener = tagClickedListener; + this.isAmoledColorMode = appSettings.isAmoledColorMode(); + } + + @Override + public int getItemCount() { + return followedTagsList.length; + } + + @Override + public FollowedTagsAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View v = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.recycler_list__list_item_with_fav, parent, false); + return new ViewHolder(v); + } + + @Override + public void onBindViewHolder(final ViewHolder holder, final int position) { + // Alternating colors + final Context c = holder.root.getContext(); + final String tag = followedTagsList[position]; + holder.title.setText(tag); + if (position % 2 == 1) { + holder.root.setBackgroundColor(isAmoledColorMode ? Color.BLACK : ContextUtils.get().rcolor(R.color.alternate_row_color)); + holder.title.setTextColor(isAmoledColorMode ? Color.GRAY : Color.BLACK); + } else { + holder.root.setBackgroundColor(isAmoledColorMode ? Color.BLACK : Color.WHITE); + holder.title.setTextColor(isAmoledColorMode ? Color.GRAY : Color.BLACK); + } + + // Favourite (Star) Image + applyFavouriteImage(holder.favouriteImage, isFollowedTagFaved(tag)); + + // Click on fav button + holder.favouriteImage.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + if (isFollowedTagFaved(tag)) { + followedTagsFavsList.remove(followedTagsFavsList.indexOf(tag)); + } else { + followedTagsFavsList.add(tag); + } + appSettings.setFollowedTagsFavs(followedTagsFavsList); + applyFavouriteImage(holder.favouriteImage, isFollowedTagFaved(tag)); + } + }); + + holder.root.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + tagClickedListener.onSomethingClicked(null, null, tag); + } + }); + } + + private boolean isFollowedTagFaved(String tag) { + return followedTagsFavsList.contains(tag); + } + + private void applyFavouriteImage(AppCompatImageView imageView, boolean isFaved) { + imageView.setImageResource(isFaved ? R.drawable.ic_star_filled_48px : R.drawable.ic_star_border_black_48px); + imageView.setColorFilter(isFaved ? appSettings.getAccentColor() : (isAmoledColorMode ? Color.GRAY : 0), PorterDuff.Mode.SRC_ATOP); + } + } +} diff --git a/app/src/main/java/com/github/dfa/diaspora_android/data/DiasporaAspect.java b/app/src/main/java/com/github/dfa/diaspora_android/data/DiasporaAspect.java new file mode 100644 index 000000000..97ad868df --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/data/DiasporaAspect.java @@ -0,0 +1,91 @@ +/* + 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.data; + +import com.github.dfa.diaspora_android.App; +import com.github.dfa.diaspora_android.util.AppSettings; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Locale; + +public class DiasporaAspect { + public long id; + public String name; + public boolean selected; + + public DiasporaAspect(long id, String name, boolean selected) { + this.id = id; + this.name = name; + this.selected = selected; + } + + + public DiasporaAspect(String shareabletext) { + // fromShareAbleText + String[] str = shareabletext.split("%"); + selected = Integer.parseInt(str[0]) == 1; + id = Long.parseLong(str[1]); + name = shareabletext.substring(shareabletext.indexOf(str[1]) + str[1].length() + 1); + } + + public DiasporaAspect(JSONObject json) throws JSONException { + if (json.has("id")) { + id = json.getLong("id"); + } + if (json.has("name")) { + name = json.getString("name"); + } + if (json.has("selected")) { + selected = json.getBoolean("selected"); + } + } + + public String toJsonString() { + JSONObject j = new JSONObject(); + try { + j.put("id", id); + j.put("name", name); + j.put("selected", selected); + } catch (JSONException e) {/*Nothing*/} + return j.toString(); + } + + public String toHtmlLink(final App app) { + final AppSettings appSettings = app.getSettings(); + return String.format(Locale.getDefault(), + "%s", + appSettings.getPod().getPodUrl().getBaseUrl(), id, name); + } + + @Override + public String toString() { + return toShareAbleText(); + } + + @Override + public boolean equals(Object o) { + return o instanceof DiasporaAspect && ((DiasporaAspect) o).id == id; + } + + public String toShareAbleText() { + return String.format(Locale.getDefault(), "%d%%%d%%%s", selected ? 1 : 0, id, name); + } +} 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 new file mode 100644 index 000000000..3c56cef34 --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/data/DiasporaPodList.java @@ -0,0 +1,524 @@ +package com.github.dfa.diaspora_android.data; + +import org.json.JSONArray; +import org.json.JSONException; +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 (http://gsantner.net/ 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_POST_COUNT_LOCAL = true; + private List pods = new ArrayList<>(); + private boolean trackMergeChanges = false; + private Integer trackAddedIndexStart = -1; + private List trackUpdatedIndexes = new ArrayList<>(); + private boolean keepOldNameDuringMerge = false; + private long timestamp; + + public DiasporaPodList() { + } + + /** + * Load DiasporaPodList from Json + * + * @param json Json Object + */ + public DiasporaPodList fromJson(JSONObject json) throws JSONException { + JSONArray jarr; + pods.clear(); + + if (json.has("pods")) { + jarr = json.getJSONArray("pods"); + for (int i = 0; i < jarr.length(); i++) { + DiasporaPod pod = new DiasporaPod().fromJson(jarr.getJSONObject(i)); + pods.add(pod); + } + } + if (json.has("timestamp")) { + timestamp = json.getLong("timestamp"); + } + return this; + } + + /** + * Convert DiasporaPodList to JSON + */ + public JSONObject toJson() throws JSONException { + JSONObject json = new JSONObject(); + JSONArray jpods = new JSONArray(); + for (DiasporaPod pod : pods) { + jpods.put(pod.toJson()); + } + json.put("pods", jpods); + json.put("timestamp", System.currentTimeMillis()); + return json; + } + + /** + * Merge newer entries into this podlist + * Will add new pods, and update data of pods with data from the new list + * + * @param newPodList Another podlist + */ + public void mergeWithNewerEntries(final DiasporaPodList newPodList) throws JSONException { + if (isTrackMergeChanges()) { + trackAddedIndexStart = -1; + trackUpdatedIndexes.clear(); + } + for (DiasporaPod newPod : newPodList) { + int index = pods.indexOf(newPod); + if (index >= 0) { + DiasporaPod updatePodBak = new DiasporaPod().fromJson(pods.get(index).toJson()); + DiasporaPod updatePod = pods.get(index); + updatePod.fromJson(newPod.toJson()); + + // Restore Pod id (if was set to zero) + if (updatePodBak.getId() != 0 && updatePod.getId() == 0) { + updatePod.setId(updatePodBak.getId()); + } + if (updatePodBak.getPostCountLocal() != 0 && updatePod.getPostCountLocal() == 0) { + updatePod.setPostCountLocal(updatePodBak.getPostCountLocal()); + } + if (updatePodBak.getScore() != 0 && updatePod.getScore() == 0) { + updatePod.setScore(updatePodBak.getScore()); + } + if (!updatePodBak.getName().equals("") && keepOldNameDuringMerge) { + updatePod.setName(updatePodBak.getName()); + } + if (isTrackMergeChanges()) { + trackUpdatedIndexes.add(index); + } + } else { + pods.add(newPod); + if (isTrackMergeChanges() && trackAddedIndexStart == -1) { + trackAddedIndexStart = pods.size() - 1; + } + } + } + } + + /** + * Sort the pod list + */ + public void sortPods() { + Collections.sort(pods); + } + + /** + * Iterator for Iterable interface (forEach, ..) + */ + public Iterator iterator() { + return pods.iterator(); + } + + public int size() { + return pods.size(); + } + + public int indexOf(DiasporaPod pod) { + return pods.indexOf(pod); + } + + public List getPods() { + return pods; + } + + public void setPods(List pods) { + this.pods = pods; + } + + public DiasporaPod getPodAt(int index) { + if (index >= 0 && index < pods.size()) { + return pods.get(index); + } + return null; + } + + public boolean isTrackMergeChanges() { + return trackMergeChanges; + } + + public void setTrackMergeChanges(boolean trackMergeChanges) { + this.trackMergeChanges = trackMergeChanges; + } + + public Integer getTrackAddedIndexStart() { + return trackAddedIndexStart; + } + + public List getTrackUpdatedIndexes() { + return trackUpdatedIndexes; + } + + public boolean isKeepOldNameDuringMerge() { + return keepOldNameDuringMerge; + } + + public void setKeepOldNameDuringMerge(boolean keepOldNameDuringMerge) { + this.keepOldNameDuringMerge = keepOldNameDuringMerge; + } + + + /* ██████╗ ██████╗ ██████╗ + * ██╔══██╗██╔═══██╗██╔══██╗ + * ██████╔╝██║ ██║██║ ██║ + * ██╔═══╝ ██║ ██║██║ ██║ + * ██║ ╚██████╔╝██████╔╝ + * ╚═╝ ╚═════╝ ╚═════╝ + */ + 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 _postCountLocal = 0; + + + public DiasporaPod() { + } + + /** + * Load a DiasporaPod from JSON + * + * @param json Json Object + */ + public DiasporaPod fromJson(JSONObject json) throws JSONException { + JSONArray jarr; + + if (json.has("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 (json.has("podUrls")) { + 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 (json.has("score")) { + _score = json.getInt("score"); + } + if (json.has("postCountLocal")) { + _postCountLocal = json.getLong("postCountLocal"); + } + if (json.has("id")) { + _id = json.getInt("id"); + } + return this; + } + + /** + * Convert DiasporaPod to JSON + */ + public JSONObject toJson() throws JSONException { + JSONObject json = new JSONObject(); + 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_POST_COUNT_LOCAL && _postCountLocal > 0) { + json.put("postCountLocal", _postCountLocal); + } + + // Pod urls + JSONArray jarr = new JSONArray(); + for (DiasporaPodUrl value : _podUrls) { + jarr.put(value.toJson()); + } + json.put("podUrls", jarr); + + // main langs + jarr = new JSONArray(); + for (String value : _mainLangs) { + jarr.put(value); + } + json.put("mainLangs", jarr); + return json; + } + + @Override + public boolean equals(Object o) { + boolean ret = false; + if (o instanceof DiasporaPod) { + DiasporaPod otherPod = (DiasporaPod) o; + + // Check if id is equal + ret = _id != 0 && _id == otherPod._id; + + // Check if _host is the same (fallback if id is 0) + if (!ret) { + for (DiasporaPodUrl podUrl : _podUrls) { + for (DiasporaPodUrl otherPodUrl : otherPod.getPodUrls()) { + if (podUrl.getBaseUrl().equals(otherPodUrl.getBaseUrl())) { + ret = true; + } + } + } + } + } + return ret; + } + + @Override + public int compareTo(DiasporaPod otherPod) { + if (otherPod != null) { + List myPodUrls = getPodUrls(); + List otherPodUrls = otherPod.getPodUrls(); + if (!myPodUrls.isEmpty() && !otherPodUrls.isEmpty()) { + return myPodUrls.get(0).getHost().compareTo(otherPodUrls.get(0).getHost()); + } + return _name.compareTo(otherPod.getName()); + } + return _name.compareTo(""); + } + + @Override + public String toString() { + return _name + "(" + _id + ")"; + } + + /** + * Iterator for Iterable interface (forEach, ..) + */ + public Iterator iterator() { + return _podUrls.iterator(); + } + + /* + * Getter & Setter + */ + public List getPodUrls() { + return _podUrls; + } + + public DiasporaPod setPodUrls(List podUrls) { + _podUrls = podUrls; + return this; + } + + public List getMainLangs() { + return _mainLangs; + } + + public DiasporaPod setMainLangs(List mainLangs) { + _mainLangs = mainLangs; + return this; + } + + public DiasporaPod appendMainLangs(String... values) { + _mainLangs.addAll(Arrays.asList(values)); + return this; + } + + /** + * Returns the first DiasporaPodUrl in the list + */ + public DiasporaPodUrl getPodUrl() { + if (_podUrls.size() > 0) { + return _podUrls.get(0); + } + return null; + } + + public DiasporaPod appendPodUrls(DiasporaPodUrl... values) { + _podUrls.addAll(Arrays.asList(values)); + return this; + } + + public String getName() { + return _name; + } + + public DiasporaPod setName(String name) { + _name = name; + return this; + } + + public int getScore() { + return _score; + } + + public DiasporaPod setScore(int score) { + _score = score; + return this; + } + + public long getPostCountLocal() { + return _postCountLocal; + } + + public DiasporaPod setPostCountLocal(long postCountLocal) { + _postCountLocal = postCountLocal; + return this; + } + + public int getId() { + return _id; + } + + public DiasporaPod setId(int id) { + _id = id; + return this; + } + + /* ██████╗ ██████╗ ██████╗ ██╗ ██╗██████╗ ██╗ + * ██╔══██╗██╔═══██╗██╔══██╗ ██║ ██║██╔══██╗██║ + * ██████╔╝██║ ██║██║ ██║ ██║ ██║██████╔╝██║ + * ██╔═══╝ ██║ ██║██║ ██║ ██║ ██║██╔══██╗██║ + * ██║ ╚██████╔╝██████╔╝ ╚██████╔╝██║ ██║███████╗ + * ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ + */ + public static class DiasporaPodUrl implements Serializable { + private String _host = ""; + private String _protocol = "https"; + private Integer _port = 443; + + public DiasporaPodUrl() { + } + + public DiasporaPodUrl(JSONObject json) throws JSONException { + fromJson(json); + } + + /** + * Get the base url + * + * @return + */ + public String getBaseUrl() { + return _protocol + "://" + _host + (isPortNeeded() ? _port : ""); + } + + /** + * Convert JSON to DiasporaPodList + * + * @param json JSON Object + */ + public DiasporaPodUrl fromJson(JSONObject json) throws JSONException { + if (json.has("host")) { + _host = json.getString("host"); + } + if (json.has("protocol")) { + _protocol = json.getString("protocol"); + } + if (json.has("port")) { + _port = json.getInt("port"); + } + return this; + } + + /*** + * Convert DiasporaPodList to JSON + */ + public JSONObject toJson() throws JSONException { + JSONObject json = new JSONObject(); + json.put("host", _host); + if (!_protocol.equals("https")) { + json.put("protocol", _protocol); + } + if (_port != 443) { + json.put("port", _port); + } + return json; + } + + /** + * Set default values for https + */ + public void setHttpsDefaults() { + setProtocol("https"); + setPort(443); + } + + + /** + * Set default values for http + */ + public void setHttpDefaults() { + setProtocol("http"); + setPort(80); + } + + /** + * Tells if the ports needs to shown + */ + public boolean isPortNeeded() { + return !((_port == 80 && _protocol.equals("http")) || (_port == 443 && _protocol.equals("https"))); + } + + @Override + public String toString() { + return getBaseUrl(); + } + + @Override + @SuppressWarnings("SimplifiableIfStatement") + public boolean equals(Object o) { + if (o instanceof DiasporaPodUrl) { + return getBaseUrl().equals(((DiasporaPodUrl) o).getBaseUrl()); + } + return false; + } + + /* + * GETTER & SETTER + */ + public String getHost() { + return _host; + } + + public DiasporaPodUrl setHost(String host) { + _host = host; + return this; + } + + public String getProtocol() { + return _protocol; + } + + public DiasporaPodUrl setProtocol(String protocol) { + _protocol = protocol; + return this; + } + + public Integer getPort() { + return _port; + } + + public DiasporaPodUrl setPort(Integer 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 new file mode 100644 index 000000000..863b7c6f1 --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/data/DiasporaUserProfile.java @@ -0,0 +1,327 @@ +/* + 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.data; + +import android.os.Handler; + +import com.github.dfa.diaspora_android.App; +import com.github.dfa.diaspora_android.listener.DiasporaUserProfileChangedListener; +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 org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * User profile + * Created by gsantner (http://gsantner.net/) on 24.03.16. Part of dandelion*. + */ +public class DiasporaUserProfile { + private static final int MINIMUM_USERPROFILE_LOAD_TIMEDIFF = 5000; + + private Handler callbackHandler; + private DiasporaUserProfileChangedListener listener; + private final App app; + private final AppSettings appSettings; + DiasporaUrlHelper urls; + private JSONObject json; + private long userProfileLastLoadedTimestamp; + private boolean isWebUserProfileLoaded; + + private String avatarUrl; + private String guid; + private String name; + private DiasporaAspect[] aspects; + private String[] followedTags; + private int notificationCount; + private int unreadMessagesCount; + private long lastVisitedPositionInStream = -1; + + + public DiasporaUserProfile(App app) { + this.app = app; + appSettings = app.getSettings(); + urls = new DiasporaUrlHelper(appSettings); + loadFromAppSettings(); + } + + public void loadFromAppSettings() { + avatarUrl = appSettings.getAvatarUrl(); + guid = appSettings.getProfileId(); + name = appSettings.getName(); + aspects = appSettings.getAspects(); + followedTags = appSettings.getFollowedTags(); + notificationCount = appSettings.getNotificationCount(); + unreadMessagesCount = appSettings.getUnreadMessageCount(); + lastVisitedPositionInStream = appSettings.getLastVisitedPositionInStream(); + } + + public DiasporaUserProfile(App app, Handler callbackHandler, DiasporaUserProfileChangedListener listener) { + this(app); + this.listener = listener; + this.callbackHandler = callbackHandler; + } + + public boolean isRefreshNeeded() { + return (System.currentTimeMillis() - userProfileLastLoadedTimestamp) >= MINIMUM_USERPROFILE_LOAD_TIMEDIFF; + } + + public boolean isWebUserProfileLoaded() { + return isWebUserProfileLoaded; + } + + public boolean parseJson(String jsonStr) { + try { + json = new JSONObject(jsonStr); + userProfileLastLoadedTimestamp = System.currentTimeMillis(); + + // Avatar + if (json.has("avatar")) { + JSONObject avatarJson = json.getJSONObject("avatar"); + if (avatarJson.has("large") && setAvatarUrl(avatarJson.getString("large"))) { + app.getAvatarImageLoader().clearAvatarImage(); + appSettings.setAvatarUrl(avatarUrl); + } + } + + // GUID (User id) + if (json.has("guid") && loadGuid(json.getString("guid")) && !guid.isEmpty()) { + appSettings.setProfileId(guid); + } + + // Name + if (json.has("name") && loadName(json.getString("name"))) { + appSettings.setName(name); + } + + // Unread message count + if (json.has("notifications_count") && loadNotificationCount(json.getInt("notifications_count"))) { + appSettings.setNotificationCount(notificationCount); + } + + // Unread message count + if (json.has("unread_messages_count") && loadUnreadMessagesCount(json.getInt("unread_messages_count"))) { + appSettings.setUnreadMessageCount(unreadMessagesCount); + } + + // Aspect + if (json.has("aspects") && loadAspects(json.getJSONArray("aspects"))) { + appSettings.setPodAspects(aspects); + } + + // Followed tags + if (json.has("android_app.followed_tags") + && loadFollowedTags(json.getJSONArray("android_app.followed_tags"))) { + appSettings.setFollowedTags(followedTags); + } + + isWebUserProfileLoaded = true; + } catch (JSONException e) { + AppLog.d(this, e.getMessage()); + isWebUserProfileLoaded = false; + } + userProfileLastLoadedTimestamp = System.currentTimeMillis(); + return isWebUserProfileLoaded; + } + + public void analyzeUrl(String url) { + String prefix = urls.getPodUrl() + DiasporaUrlHelper.SUBURL_STREAM_WITH_TIMESTAMP; + if (url.startsWith(prefix)) { + try { + setLastVisitedPositionInStream(Long.parseLong(url.replace(prefix, ""))); + } catch (NumberFormatException ignored) { + } + } + } + + /* + // Getters & Setters + */ + + public String getAvatarUrl() { + return avatarUrl; + } + + public String getGuid() { + return guid; + } + + public String getName() { + return name; + } + + public int getNotificationCount() { + return notificationCount; + } + + public int getUnreadMessagesCount() { + return unreadMessagesCount; + } + + public DiasporaAspect[] getAspects() { + return aspects; + } + + public String[] getFollowedTags() { + return followedTags; + } + + public long getLastVisitedPositionInStream() { + return lastVisitedPositionInStream; + } + + public void setLastVisitedPositionInStream(long lastVisitedPositionInStream) { + this.lastVisitedPositionInStream = lastVisitedPositionInStream; + appSettings.setLastVisitedPositionInStream(lastVisitedPositionInStream); + } + + public boolean hasLastVisitedTimestampInStream() { + return appSettings.getLastVisitedPositionInStream() != -1; + } + + public void resetLastVisitedPositionInStream() { + appSettings.setLastVisitedPositionInStream(-1); + } + + /* + * Private property setters + */ + + /** + * Sets the avatar, returns true if this was a new one, false if already the old one + * + * @param avatarUrl url + * @return true if new avatar url + */ + private boolean setAvatarUrl(final String avatarUrl) { + if (!this.avatarUrl.equals(avatarUrl)) { + this.avatarUrl = avatarUrl; + if (listener != null && callbackHandler != null) { + callbackHandler.post(new Runnable() { + public void run() { + listener.onUserProfileAvatarChanged(DiasporaUserProfile.this, avatarUrl); + } + }); + } + return true; + } + return false; + } + + private boolean loadGuid(final String guid) { + if (!this.guid.equals(guid)) { + this.guid = guid; + return true; + } + return false; + } + + private boolean loadName(final String name) { + if (!this.name.equals(name)) { + this.name = name; + if (listener != null && callbackHandler != null) { + callbackHandler.post(new Runnable() { + public void run() { + listener.onUserProfileNameChanged(DiasporaUserProfile.this, name); + } + }); + } + return true; + } + return false; + } + + private boolean loadNotificationCount(final int notificationCount) { + if (this.notificationCount != notificationCount) { + this.notificationCount = notificationCount; + if (listener != null && callbackHandler != null) { + callbackHandler.post(new Runnable() { + public void run() { + listener.onNotificationCountChanged(DiasporaUserProfile.this, notificationCount); + } + }); + } + return true; + } + return false; + } + + private boolean loadAspects(final JSONArray jsonAspects) throws JSONException { + aspects = new DiasporaAspect[jsonAspects.length()]; + for (int i = 0; i < jsonAspects.length(); i++) { + aspects[i] = new DiasporaAspect(jsonAspects.getJSONObject(i)); + } + return true; + } + + private boolean loadFollowedTags(final JSONArray jsonTags) throws JSONException { + followedTags = new String[jsonTags.length()]; + for (int i = 0; i < jsonTags.length(); i++) { + followedTags[i] = jsonTags.getString(i); + } + return true; + } + + private boolean loadUnreadMessagesCount(final int unreadMessagesCount) { + if (this.unreadMessagesCount != unreadMessagesCount) { + this.unreadMessagesCount = unreadMessagesCount; + if (listener != null && callbackHandler != null) { + callbackHandler.post(new Runnable() { + public void run() { + listener.onUnreadMessageCountChanged(DiasporaUserProfile.this, unreadMessagesCount); + } + }); + } + return true; + } + return false; + } + + public Handler getCallbackHandler() { + return callbackHandler; + } + + public void setCallbackHandler(Handler callbackHandler) { + this.callbackHandler = callbackHandler; + } + + public DiasporaUserProfileChangedListener getListener() { + return listener; + } + + public void setListener(DiasporaUserProfileChangedListener listener) { + this.listener = listener; + } + + /* + * Not implemented / not needed yet: + * string "diasporaAddress" + * int "id" + * boolean "admin" + * int "following_count" + * boolean "moderator" + * + * array "services" + * ? ? + * array "configured_services" + * ? ? + */ +} 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 new file mode 100644 index 000000000..aff25a2ab --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/listener/DiasporaUserProfileChangedListener.java @@ -0,0 +1,59 @@ +/* + 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.listener; + +import com.github.dfa.diaspora_android.data.DiasporaUserProfile; + +/** + * Created by gsantner (http://gsantner.net/) on 26.03.16. + * Interface that needs to be implemented by classes that listen for Profile related changes + */ +public interface DiasporaUserProfileChangedListener { + /** + * Called when the DiasporaUserProfile name changed + * + * @param diasporaUserProfile The profile + * @param name The new name + */ + void onUserProfileNameChanged(DiasporaUserProfile diasporaUserProfile, String name); + + /** + * Called when the DiasporaUserProfile avatarUrl changed + * + * @param diasporaUserProfile The profile + * @param avatarUrl The new name + */ + void onUserProfileAvatarChanged(DiasporaUserProfile diasporaUserProfile, String avatarUrl); + + /** + * Called when the DiasporaUserProfile notificationCount changed + * + * @param diasporaUserProfile The profile + * @param notificationCount The new notificationCount + */ + void onNotificationCountChanged(DiasporaUserProfile diasporaUserProfile, int notificationCount); + + /** + * Called when the DiasporaUserProfile unreadMessageCount changed + * + * @param diasporaUserProfile The profile + * @param unreadMessageCount The new unreadMessageCount + */ + void onUnreadMessageCountChanged(DiasporaUserProfile diasporaUserProfile, int unreadMessageCount); +} 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 new file mode 100644 index 000000000..58f047040 --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/listener/IntellihideToolbarActivityListener.java @@ -0,0 +1,14 @@ +package com.github.dfa.diaspora_android.listener; + +import android.support.design.widget.AppBarLayout; + +/** + * interface that adds options to control intellihide of toolbars to the Activity + * Created by vanitas on 08.10.16. + */ + +public interface IntellihideToolbarActivityListener { + int toolbarDefaultScrollFlags = AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP; + + void setToolbarIntellihide(boolean enable); +} diff --git a/app/src/main/java/com/github/dfa/diaspora_android/listener/OnSomethingClickListener.java b/app/src/main/java/com/github/dfa/diaspora_android/listener/OnSomethingClickListener.java new file mode 100644 index 000000000..86ae67890 --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/listener/OnSomethingClickListener.java @@ -0,0 +1,15 @@ +package com.github.dfa.diaspora_android.listener; + +/** + * Listener for different types of click events + */ +public interface OnSomethingClickListener { + /** + * Triggered when something was clicked + * + * @param o Some object, or null + * @param i Some index, int value or null + * @param s Some String, or null + */ + void onSomethingClicked(T o, Integer i, String s); +} diff --git a/app/src/main/java/com/github/dfa/diaspora_android/receiver/OpenExternalLinkReceiver.java b/app/src/main/java/com/github/dfa/diaspora_android/receiver/OpenExternalLinkReceiver.java new file mode 100644 index 000000000..496a8c6e1 --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/receiver/OpenExternalLinkReceiver.java @@ -0,0 +1,83 @@ +/* + 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.receiver; + +import android.app.Activity; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.support.customtabs.CustomTabsIntent; + +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.util.AppLog; +import com.github.dfa.diaspora_android.util.AppSettings; +import com.github.dfa.diaspora_android.web.custom_tab.BrowserFallback; +import com.github.dfa.diaspora_android.web.custom_tab.CustomTabActivityHelper; + +/** + * BroadcastReceiver that opens links in a Chrome CustomTab + * Created by vanitas on 11.09.16. + */ +public class OpenExternalLinkReceiver extends BroadcastReceiver { + private final Activity parent; + + public OpenExternalLinkReceiver(Activity parent) { + this.parent = parent; + } + + @Override + public void onReceive(Context c, Intent receiveIntent) { + AppSettings appSettings = AppSettings.get(); + ThemeHelper.getInstance(appSettings); + + AppLog.v(this, "OpenExternalLinkReceiver.onReceive(): url"); + + Uri url; + try { + String sUrl = receiveIntent.getStringExtra(MainActivity.EXTRA_URL); + url = Uri.parse(sUrl); + } catch (Exception _ignored) { + AppLog.v(this, "Could not open Chrome Custom Tab (bad URL)"); + return; + } + + if (appSettings.isChromeCustomTabsEnabled()) { + // Setup Chrome Custom Tab + CustomTabsIntent.Builder customTab = new CustomTabsIntent.Builder(); + customTab.setToolbarColor(ThemeHelper.getPrimaryColor()); + customTab.addDefaultShareMenuItem(); + + Bitmap backButtonIcon = BitmapFactory.decodeResource(c.getResources(), R.drawable.chrome_custom_tab__back); + customTab.setCloseButtonIcon(backButtonIcon); + + // Launch Chrome Custom Tab + CustomTabActivityHelper.openCustomTab(parent, customTab.build(), url, new BrowserFallback()); + } else { + // Open in normal browser (via intent) + Intent openBrowserIntent = new Intent(Intent.ACTION_VIEW, url); + openBrowserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + c.startActivity(openBrowserIntent); + } + } +} diff --git a/app/src/main/java/com/github/dfa/diaspora_android/receiver/UpdateTitleReceiver.java b/app/src/main/java/com/github/dfa/diaspora_android/receiver/UpdateTitleReceiver.java new file mode 100644 index 000000000..c6dddb6ba --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/receiver/UpdateTitleReceiver.java @@ -0,0 +1,103 @@ +/* + 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.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +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.util.AppLog; +import com.github.dfa.diaspora_android.util.AppSettings; +import com.github.dfa.diaspora_android.util.DiasporaUrlHelper; + +/** + * BroadcastReceiver used to update the title of the MainActivity depending on the url of the ui__webview + * Created by vanitas on 11.09.16. + */ +public class UpdateTitleReceiver extends BroadcastReceiver { + private DiasporaUrlHelper urls; + private AppSettings appSettings; + private App app; + private TitleCallback callback; + private String lastUrl; + + public UpdateTitleReceiver(App app, DiasporaUrlHelper urls, TitleCallback callback) { + this.urls = urls; + this.app = app; + this.appSettings = app.getSettings(); + this.callback = callback; + } + + @Override + public void onReceive(Context context, Intent intent) { + lastUrl = intent.getStringExtra(MainActivity.EXTRA_URL); + if (lastUrl != null && lastUrl.startsWith(urls.getPodUrl())) { + String subUrl = lastUrl.substring((urls.getPodUrl()).length()); + AppLog.spam(this, "onReceive()- Set title for subUrl " + subUrl); + if (subUrl.startsWith(DiasporaUrlHelper.SUBURL_STREAM)) { + setTitle(R.string.nav_stream); + } else if (subUrl.startsWith(DiasporaUrlHelper.SUBURL_POSTS)) { + setTitle(R.string.app_name); + } else if (subUrl.startsWith(DiasporaUrlHelper.SUBURL_NOTIFICATIONS)) { + setTitle(R.string.notifications); + } else if (subUrl.startsWith(DiasporaUrlHelper.SUBURL_CONVERSATIONS)) { + setTitle(R.string.conversations); + } else if (subUrl.startsWith(DiasporaUrlHelper.SUBURL_NEW_POST)) { + setTitle(R.string.new_post); + } else if (subUrl.startsWith(DiasporaUrlHelper.SUBURL_STATISTICS)) { + setTitle(R.string.statistics); + } else if (subUrl.startsWith(DiasporaUrlHelper.SUBURL_CONTACTS)) { + setTitle(R.string.contacts); + } else if (subUrl.startsWith(DiasporaUrlHelper.SUBURL_PEOPLE + appSettings.getProfileId())) { + setTitle(R.string.nav_profile); + } else if (subUrl.startsWith(DiasporaUrlHelper.SUBURL_ACTIVITY)) { + setTitle(R.string.nav_activities); + } else if (subUrl.startsWith(DiasporaUrlHelper.SUBURL_LIKED)) { + setTitle(R.string.nav_liked); + } else if (subUrl.startsWith(DiasporaUrlHelper.SUBURL_COMMENTED)) { + setTitle(R.string.nav_commented); + } else if (subUrl.startsWith(DiasporaUrlHelper.SUBURL_MENTIONS)) { + setTitle(R.string.nav_mentions); + } else if (subUrl.startsWith(DiasporaUrlHelper.SUBURL_PUBLIC)) { + setTitle(R.string.public_); + } else if (urls.isAspectUrl(lastUrl)) { + setTitle(urls.getAspectNameFromUrl(lastUrl, app)); + } + } else { + AppLog.spam(this, "onReceive()- Invalid url: " + lastUrl); + } + } + + private void setTitle(int rId) { + callback.setTitle(lastUrl, rId); + } + + private void setTitle(String title) { + callback.setTitle(lastUrl, title); + } + + public interface TitleCallback { + void setTitle(String url, int resId); + + void setTitle(String url, String title); + } +} 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 new file mode 100644 index 000000000..ab975326d --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/service/AvatarImageLoader.java @@ -0,0 +1,62 @@ +/* + 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.content.Context; +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 { + private File avatarFile; + + public AvatarImageLoader(Context context) { + avatarFile = new File(context.getFilesDir(), "avatar0.png"); + } + + public boolean clearAvatarImage() { + return (!isAvatarDownloaded() || avatarFile.delete()); + } + + public boolean loadToImageView(ImageView imageView) { + if (avatarFile.exists()) { + Bitmap bitmap = BitmapFactory.decodeFile(avatarFile.getAbsolutePath()); + imageView.setImageBitmap(bitmap); + return true; + } + return false; + } + + public boolean isAvatarDownloaded() { + return avatarFile.exists(); + } + + public void startImageDownload(ImageView imageView, String avatarUrl) { + if (!avatarUrl.equals("")) { + 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 new file mode 100644 index 000000000..3e5943088 --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/service/FetchPodsService.java @@ -0,0 +1,114 @@ +/* + 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.app.Service; +import android.content.Intent; +import android.os.AsyncTask; +import android.os.IBinder; +import android.support.v4.content.LocalBroadcastManager; + +import com.github.dfa.diaspora_android.data.DiasporaPodList; +import com.github.dfa.diaspora_android.util.AppLog; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; + +import javax.net.ssl.HttpsURLConnection; + +import info.guardianproject.netcipher.NetCipher; + +public class FetchPodsService extends Service { + public static final String MESSAGE_PODS_RECEIVED = "com.github.dfa.diaspora.podsreceived"; + public static final String EXTRA_PODLIST = "pods"; + + public FetchPodsService() { + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + new GetPodsTask(this).execute(); + return super.onStartCommand(intent, flags, startId); + } + + @Override + public IBinder onBind(Intent intent) { + // TODO: Return the communication channel to the service. + throw new UnsupportedOperationException("Not yet implemented"); + } +} + +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 final Service service; + + GetPodsTask(Service service) { + this.service = service; + } + + @Override + protected DiasporaPodList doInBackground(Void... params) { + StringBuilder sb = new StringBuilder(); + BufferedReader br = null; + try { + HttpsURLConnection con = NetCipher.getHttpsURLConnection(PODDY_PODLIST_URL); + if (con.getResponseCode() == HttpsURLConnection.HTTP_OK) { + br = new BufferedReader(new InputStreamReader(con.getInputStream())); + String line; + while ((line = br.readLine()) != null) { + sb.append(line); + } + + // Parse JSON & return pod list + JSONObject json = new JSONObject(sb.toString()); + return new DiasporaPodList().fromJson(json); + } else { + AppLog.e(this, "Failed to download list of pods"); + } + } catch (IOException | JSONException e) { + e.printStackTrace(); + } finally { + if (br != null) { + try { + br.close(); + } catch (IOException ignored) { + } + } + } + + // Could not fetch list of pods :( + return new DiasporaPodList(); + } + + @Override + protected void onPostExecute(DiasporaPodList pods) { + if (pods == null) { + pods = new DiasporaPodList(); + } + Intent broadcastIntent = new Intent(FetchPodsService.MESSAGE_PODS_RECEIVED); + broadcastIntent.putExtra(FetchPodsService.EXTRA_PODLIST, pods); + LocalBroadcastManager.getInstance(service.getApplicationContext()).sendBroadcast(broadcastIntent); + service.stopSelf(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/dfa/diaspora_android/service/HashtagContentProvider.java b/app/src/main/java/com/github/dfa/diaspora_android/service/HashtagContentProvider.java new file mode 100644 index 000000000..4c7b744ed --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/service/HashtagContentProvider.java @@ -0,0 +1,60 @@ +/* + 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 . + + This file is inspired from sourabhsoni.com/implementing-hashtags-in-android-application/ + */ +package com.github.dfa.diaspora_android.service; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.database.Cursor; +import android.net.Uri; + +public class HashtagContentProvider extends ContentProvider { + + @Override + public int delete(Uri arg0, String arg1, String[] arg2) { + return 0; + } + + @Override + public String getType(Uri arg0) { + return "vnd.android.cursor.item/vnd.cc.tag"; + } + + @Override + public Uri insert(Uri arg0, ContentValues arg1) { + return null; + } + + @Override + public boolean onCreate() { + return false; + } + + @Override + public Cursor query(Uri arg0, String[] arg1, String arg2, String[] arg3, + String arg4) { + return null; + } + + @Override + public int update(Uri arg0, ContentValues arg1, String arg2, String[] arg3) { + return 0; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/dfa/diaspora_android/service/ProfileFetchTask.java b/app/src/main/java/com/github/dfa/diaspora_android/service/ProfileFetchTask.java new file mode 100644 index 000000000..4f0b3491f --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/service/ProfileFetchTask.java @@ -0,0 +1,109 @@ +/* + 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.content.Context; +import android.os.AsyncTask; +import android.webkit.CookieManager; + +import com.github.dfa.diaspora_android.App; +import com.github.dfa.diaspora_android.data.DiasporaUserProfile; +import com.github.dfa.diaspora_android.util.AppLog; +import com.github.dfa.diaspora_android.util.DiasporaUrlHelper; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URL; + +import javax.net.ssl.HttpsURLConnection; + +import info.guardianproject.netcipher.NetCipher; + +/** + * AsyncTask to fetch a users profile + */ +public class ProfileFetchTask extends AsyncTask { + // Code for getting the profile async without any UI/WebView + // TODO: This is an early version,needs to be converted to Service + + private final App app; + private final Context context; + private final DiasporaUrlHelper urls; + + public ProfileFetchTask(final App app) { + this.context = app.getApplicationContext(); + this.app = app; + this.urls = new DiasporaUrlHelper(app.getSettings()); + } + + + @Override + protected Void doInBackground(Void... params) { + String extractedProfileData = null; + final CookieManager cookieManager = app.getCookieManager(); + String cookies = cookieManager.getCookie(urls.getPodUrl()); + AppLog.d(this, cookies); + + HttpsURLConnection connection; + InputStream inStream; + try { + URL url = new URL(urls.getStreamUrl()); + connection = NetCipher.getHttpsURLConnection(url); + connection.setReadTimeout(10000); + connection.setConnectTimeout(15000); + connection.setRequestMethod("GET"); + if (cookies != null) { + connection.setRequestProperty("Cookie", cookies); + } + connection.connect(); + + inStream = connection.getInputStream(); + BufferedReader br = new BufferedReader(new InputStreamReader(inStream)); + String line; + final String TARGET_TAG = "window.gon={};gon.user="; + while ((line = br.readLine()) != null && !line.startsWith(". + */ +package com.github.dfa.diaspora_android.ui; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; +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 _badgeText; + private Rect _textRect = new Rect(); + + 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); + + _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(@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) + 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; + float textY = centerY + (textHeight / 2f); + canvas.drawText(_badgeValue.length() > 2 ? BADGE_VALUE_OVERFLOW : _badgeValue, + centerX, textY, _badgeText); + } + + // 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 the value isn't a zero + _shouldDraw = !text.equalsIgnoreCase("0"); + invalidateSelf(); + } + + @Override + public void setAlpha(int alpha) { + } + + @Override + public void setColorFilter(ColorFilter cf) { + } + + @Override + public int getOpacity() { + return PixelFormat.UNKNOWN; + } + + public static void setBadgeCount(Context context, LayerDrawable icon, Integer count) { + setBadgeText(context, icon, count.toString()); + } + + // Max of 2 characters + public static void setBadgeText(Context context, LayerDrawable icon, String text) { + BadgeDrawable badge; + + // Reuse drawable if possible + Drawable reuse = icon.findDrawableByLayerId(R.id.ic_badge); + if (reuse != null && reuse instanceof BadgeDrawable) { + badge = (BadgeDrawable) reuse; + } else { + badge = new BadgeDrawable(context); + } + + badge.setBadgeText(text); + icon.mutate(); + icon.setDrawableByLayerId(R.id.ic_badge, badge); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/dfa/diaspora_android/ui/BottomBarBehavior.java b/app/src/main/java/com/github/dfa/diaspora_android/ui/BottomBarBehavior.java new file mode 100644 index 000000000..c9648574a --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/ui/BottomBarBehavior.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2015 takahirom + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.dfa.diaspora_android.ui; + +import android.content.Context; +import android.support.design.widget.AppBarLayout; +import android.support.design.widget.CoordinatorLayout; +import android.util.AttributeSet; +import android.view.View; +import android.widget.LinearLayout; + +/** + * Behaviour of the bottom Toolbar + * WARNING: This class is NOT UNUSED + */ +@SuppressWarnings("unused") +public class BottomBarBehavior extends CoordinatorLayout.Behavior { + + private int defaultDependencyTop = -1; + + public BottomBarBehavior(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public boolean layoutDependsOn(CoordinatorLayout parent, LinearLayout child, View dependency) { + return dependency instanceof AppBarLayout; + } + + + @Override + public boolean onDependentViewChanged(CoordinatorLayout parent, LinearLayout child, View dependency) { + if (defaultDependencyTop == -1) { + defaultDependencyTop = dependency.getTop(); + } + if (dependency.getTop() < 0) + child.setTranslationY(-dependency.getTop() + defaultDependencyTop); + else + child.setTranslationY(defaultDependencyTop); + return true; + } + +} \ No newline at end of file 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 new file mode 100644 index 000000000..26552ec17 --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/ui/HtmlTextView.java @@ -0,0 +1,84 @@ +/* + 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; + +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 com.github.dfa.diaspora_android.activity.MainActivity; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * TextView, that renders HTML with highlited and clickable links and hashtags. + * Links are opened in a webbrowser. + * 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 AppCompatTextView { + + public HtmlTextView(Context context) { + super(context); + init(); + } + + public HtmlTextView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public HtmlTextView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + /** + * Linkify, format markdown and escape the displayed message. + */ + private void init() { + formatHtmlAndCustomTags(); + } + + public void setTextFormatted(String text) { + setText(text); + formatHtmlAndCustomTags(); + } + + private void formatHtmlAndCustomTags() { + setText(new SpannableString(Html.fromHtml(getText().toString()))); + Linkify.TransformFilter filter = new Linkify.TransformFilter() { + public final String transformUrl(final Matcher match, String url) { + return match.group(); + } + }; + + Pattern hashtagPattern = Pattern.compile("[#]+[A-Za-z0-9-_]+\\b"); + String hashtagScheme = MainActivity.CONTENT_HASHTAG; + Linkify.addLinks(this, hashtagPattern, hashtagScheme, null, filter); + + Pattern urlPattern = Patterns.WEB_URL; + Linkify.addLinks(this, urlPattern, null, null, filter); + } +} \ No newline at end of file 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 new file mode 100644 index 000000000..aa8850386 --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/ui/PodSelectionDialog.java @@ -0,0 +1,275 @@ +package com.github.dfa.diaspora_android.ui; + + +import android.annotation.SuppressLint; +import android.app.Dialog; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v7.app.AlertDialog; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.RadioGroup; +import android.widget.Spinner; +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.DiasporaPodList.DiasporaPod; +import com.github.dfa.diaspora_android.data.DiasporaPodList.DiasporaPod.DiasporaPodUrl; +import com.github.dfa.diaspora_android.ui.theme.ThemeHelper; +import com.github.dfa.diaspora_android.ui.theme.ThemedAppCompatDialogFragment; +import com.github.dfa.diaspora_android.util.AppSettings; +import com.github.dfa.diaspora_android.web.ProxyHandler; + +import org.json.JSONException; + +import java.util.List; + +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; +import butterknife.OnItemSelected; + +/** + * Dialog that helps the user configure a pod + * Created by gsantner (http://gsantner.net) on 06.10.16. + */ +public class PodSelectionDialog extends ThemedAppCompatDialogFragment { + public static final String TAG = "com.github.dfa.diaspora_android.ui.PodSelectionDialog"; + + public interface PodSelectionDialogResultListener { + void onPodSelectionDialogResult(DiasporaPod pod, boolean accepted); + } + + public static PodSelectionDialog newInstance(PodSelectionDialogResultListener resultListener) { + return newInstance(new DiasporaPod(), resultListener); + } + + public static PodSelectionDialog newInstance(DiasporaPod pod, PodSelectionDialogResultListener resultListener) { + PodSelectionDialog dialog = new PodSelectionDialog(); + dialog.setPod(pod); + dialog.setResultListener(resultListener); + return dialog; + } + + /* + // ██████╗ ██╗ █████╗ ██╗ ██████╗ ██████╗ + // ██╔══██╗██║██╔══██╗██║ ██╔═══██╗██╔════╝ + // ██║ ██║██║███████║██║ ██║ ██║██║ ███╗ + // ██║ ██║██║██╔══██║██║ ██║ ██║██║ ██║ + // ██████╔╝██║██║ ██║███████╗╚██████╔╝╚██████╔╝ + // ╚═════╝ ╚═╝╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═════╝ + */ + + @BindView(R.id.podselection__dialog__edit_podaddress) + EditText editPodAddress; + + @BindView(R.id.podselection__dialog__edit_pod_name) + EditText editPodName; + + @BindView(R.id.podselection__dialog__radiogroup_protocol) + RadioGroup radiogrpProtocol; + + @BindView(R.id.podselection__dialog__text_profile) + TextView textProfile; + + @BindView(R.id.podselection__dialog__spinner_profile) + Spinner spinnerProfile; + + @BindView(R.id.podselection__dialog__check_torpreset) + CheckBox checkboxTorPreset; + + @BindView(R.id.podselection__dialog__text_torpreset) + TextView textTorPreset; + + @BindView(R.id.podselection__dialog__text_pod_name) + TextView textPodName; + + @BindView(R.id.podselection__dialog__text_pod_address) + TextView textPodAddress; + + @BindView(R.id.podselection__dialog__text_protocol) + TextView textProtocol; + + @BindView(R.id.podselection__dialog__btn_ok) + Button btnOk; + + @BindView(R.id.podselection__dialog__btn_cancel) + Button btnCancel; + + private PodSelectionDialogResultListener resultListener; + private View root; + private DiasporaPod pod = new DiasporaPod(); + private App app; + + @NonNull + @Override + @SuppressLint("InflateParams") + public Dialog onCreateDialog(Bundle savedInstanceState) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + LayoutInflater inflater = getActivity().getLayoutInflater(); + app = (App) getActivity().getApplication(); + + // Bind UI + root = inflater.inflate(R.layout.podselection__dialog, null); + ButterKnife.bind(this, root); + editPodName.setText(pod.getName()); + List podUrls = pod.getPodUrls(); + if (podUrls.size() > 0) { + uiLoadDiasporaUrl(0); + } + if (podUrls.size() > 1) { + textProfile.setVisibility(View.VISIBLE); + spinnerProfile.setVisibility(View.VISIBLE); + String[] podUrlss = new String[podUrls.size()]; + for (int i = 0; i < podUrls.size(); podUrlss[i] = podUrls.get(i++).getBaseUrl()) ; + ArrayAdapter spinnerAdapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_item, podUrlss); + spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + spinnerProfile.setAdapter(spinnerAdapter); + } + applyColorsToViews(); + builder.setView(root); + return builder.create(); + } + + protected void applyColorsToViews() { + ThemeHelper.getInstance(app.getSettings()); + + textPodAddress.setTextColor(ThemeHelper.getAccentColor()); + textPodName.setTextColor(ThemeHelper.getAccentColor()); + textProfile.setTextColor(ThemeHelper.getAccentColor()); + textProtocol.setTextColor(ThemeHelper.getAccentColor()); + textTorPreset.setTextColor(ThemeHelper.getAccentColor()); + btnOk.setTextColor(ThemeHelper.getAccentColor()); + btnCancel.setTextColor(ThemeHelper.getAccentColor()); + + ThemeHelper.updateEditTextColor(editPodAddress); + ThemeHelper.updateEditTextColor(editPodName); + ThemeHelper.updateCheckBoxColor(checkboxTorPreset); + ThemeHelper.updateRadioGroupColor(radiogrpProtocol); + } + + @Override + protected AppSettings getAppSettings() { + if (isAdded()) { + return ((App) getActivity().getApplication()).getSettings(); + } else { + return AppSettings.get(); + } + } + + @OnItemSelected(R.id.podselection__dialog__spinner_profile) + public void spinnerItemSelected(Spinner spinner, int position) { + uiLoadDiasporaUrl(position); + } + + public void uiLoadDiasporaUrl(int wantedPodUrlPos) { + List podUrls = pod.getPodUrls(); + if (podUrls.size() == 0) { + return; + } + wantedPodUrlPos = wantedPodUrlPos < podUrls.size() ? wantedPodUrlPos : 0; + + DiasporaPodUrl url1 = podUrls.get(wantedPodUrlPos); + editPodAddress.setText(url1.getHost()); + radiogrpProtocol.check(url1.getProtocol().equals("https") + ? R.id.podselection__dialog__radio_https : R.id.podselection__dialog__radio_http); + + // Tor + boolean isOnionUrl = url1.getHost().endsWith(".onion"); + setUiVisible(textTorPreset, isOnionUrl); + setUiVisible(checkboxTorPreset, isOnionUrl); + checkboxTorPreset.setChecked(isOnionUrl); + } + + public void setUiVisible(View view, boolean visible) { + if (view != null) { + view.setVisibility(visible ? View.VISIBLE : View.GONE); + } + } + + + @OnClick({R.id.podselection__dialog__btn_ok, R.id.podselection__dialog__btn_cancel}) + public void onResultButtonClicked(View view) { + boolean POSITIVE_PRESSED = view.getId() == R.id.podselection__dialog__btn_ok; + if (POSITIVE_PRESSED) { + if (!checkInputs()) { + return; + } + DiasporaPodUrl podUrl = new DiasporaPodUrl(); + if (radiogrpProtocol.getCheckedRadioButtonId() == R.id.podselection__dialog__radio_https) { + podUrl.setHttpsDefaults(); + } else { + podUrl.setHttpDefaults(); + } + podUrl.setHost(editPodAddress.getText().toString()); + pod.setName(editPodName.getText().toString()); + pod.getPodUrls().clear(); + pod.getPodUrls().add(podUrl); + + // Load Tor preset + if (pod.getPodUrl().getHost().endsWith(".onion") && checkboxTorPreset.isChecked()) { + AppSettings settings = app.getSettings(); + settings.setProxyHttpEnabled(true); + settings.setProxyWasEnabled(false); + settings.setProxyHttpPort(8118); + settings.setProxyHttpHost("127.0.0.1"); + ProxyHandler.getInstance().updateProxySettings(getContext()); + } + + getDialog().dismiss(); + publishResult(true); + } else { + getDialog().cancel(); + publishResult(false); + } + } + + public boolean checkInputs() { + boolean ok = true; + String s = editPodAddress.getText().toString(); + if (TextUtils.isEmpty(s) || s.length() < 3) { + editPodAddress.setError(getString(R.string.missing_value)); + ok = false; + } + s = editPodName.getText().toString(); + if (TextUtils.isEmpty(s) || s.length() < 3) { + editPodName.setError(getString(R.string.missing_value)); + ok = false; + } + return ok; + } + + public void publishResult(boolean accepted) { + if (resultListener != null) { + resultListener.onPodSelectionDialogResult(pod, accepted); + } + } + + /* + * GETTER & SETTER + */ + public PodSelectionDialogResultListener getResultListener() { + return resultListener; + } + + public void setResultListener(PodSelectionDialogResultListener resultListener) { + this.resultListener = resultListener; + } + + public DiasporaPod getPod() { + return pod; + } + + public void setPod(DiasporaPod pod) { + try { + this.pod = new DiasporaPod().fromJson(pod.toJson()); + } catch (JSONException ignored) { + } + } +} diff --git a/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/ColorPalette.java b/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/ColorPalette.java new file mode 100644 index 000000000..d483605e4 --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/ColorPalette.java @@ -0,0 +1,289 @@ +package com.github.dfa.diaspora_android.ui.theme; + +import android.content.Context; +import android.graphics.Color; +import android.support.v4.content.ContextCompat; +import android.support.v4.graphics.ColorUtils; + +import com.github.dfa.diaspora_android.R; + +/** + * Class that handles Colors + * Created by dnld on 24/02/16. + */ +public class ColorPalette { + + public static int[] getAccentColors(Context context) { + return new int[]{ + ContextCompat.getColor(context, R.color.md_red_500), + ContextCompat.getColor(context, R.color.md_purple_500), + ContextCompat.getColor(context, R.color.md_deep_purple_500), + ContextCompat.getColor(context, R.color.md_blue_500), + ContextCompat.getColor(context, R.color.md_light_blue_500), + ContextCompat.getColor(context, R.color.md_cyan_500), + ContextCompat.getColor(context, R.color.md_teal_500), + ContextCompat.getColor(context, R.color.md_green_500), + ContextCompat.getColor(context, R.color.md_yellow_500), + ContextCompat.getColor(context, R.color.md_orange_500), + ContextCompat.getColor(context, R.color.md_deep_orange_500), + ContextCompat.getColor(context, R.color.md_brown_500), + ContextCompat.getColor(context, R.color.md_blue_grey_500), + }; + } + + public static int getObscuredColor(int c) { + float[] hsv = new float[3]; + int color = c; + Color.colorToHSV(color, hsv); + hsv[2] *= 0.85f; // value component + color = Color.HSVToColor(hsv); + return color; + } + + public static int getTransparentColor(int color, int alpha) { + return ColorUtils.setAlphaComponent(color, alpha); + } + + public static int[] getTransparencyShadows(int color) { + int[] shadows = new int[10]; + for (int i = 0; i < 10; i++) + shadows[i] = (ColorPalette.getTransparentColor(color, ((100 - (i * 10)) * 255) / 100)); + return shadows; + } + + public static int[] getBaseColors(Context context) { + return new int[]{ + ContextCompat.getColor(context, R.color.md_red_500), + ContextCompat.getColor(context, R.color.md_pink_500), + ContextCompat.getColor(context, R.color.md_purple_500), + ContextCompat.getColor(context, R.color.md_deep_purple_500), + ContextCompat.getColor(context, R.color.md_indigo_500), + ContextCompat.getColor(context, R.color.md_blue_500), + ContextCompat.getColor(context, R.color.md_light_blue_500), + ContextCompat.getColor(context, R.color.md_cyan_500), + ContextCompat.getColor(context, R.color.md_teal_500), + ContextCompat.getColor(context, R.color.md_green_500), + ContextCompat.getColor(context, R.color.md_light_green_500), + ContextCompat.getColor(context, R.color.md_lime_500), + ContextCompat.getColor(context, R.color.md_yellow_500), + ContextCompat.getColor(context, R.color.md_amber_500), + ContextCompat.getColor(context, R.color.md_orange_500), + ContextCompat.getColor(context, R.color.md_deep_orange_500), + ContextCompat.getColor(context, R.color.md_brown_500), + ContextCompat.getColor(context, R.color.md_blue_grey_500), + ContextCompat.getColor(context, R.color.md_grey_500) + }; + } + + public static int[] getColors(Context context, int c) { + if (c == ContextCompat.getColor(context, R.color.md_red_500)) { + return new int[]{ + ContextCompat.getColor(context, R.color.md_red_200), + ContextCompat.getColor(context, R.color.md_red_300), + ContextCompat.getColor(context, R.color.md_red_400), + ContextCompat.getColor(context, R.color.md_red_500), + ContextCompat.getColor(context, R.color.md_red_600), + ContextCompat.getColor(context, R.color.md_red_700), + ContextCompat.getColor(context, R.color.md_red_800), + ContextCompat.getColor(context, R.color.md_red_900) + }; + } else if (c == ContextCompat.getColor(context, R.color.md_pink_500)) { + return new int[]{ + ContextCompat.getColor(context, R.color.md_pink_200), + ContextCompat.getColor(context, R.color.md_pink_300), + ContextCompat.getColor(context, R.color.md_pink_400), + ContextCompat.getColor(context, R.color.md_pink_500), + ContextCompat.getColor(context, R.color.md_pink_600), + ContextCompat.getColor(context, R.color.md_pink_700), + ContextCompat.getColor(context, R.color.md_pink_800), + ContextCompat.getColor(context, R.color.md_pink_900) + }; + } else if (c == ContextCompat.getColor(context, R.color.md_purple_500)) { + return new int[]{ + ContextCompat.getColor(context, R.color.md_purple_200), + ContextCompat.getColor(context, R.color.md_purple_300), + ContextCompat.getColor(context, R.color.md_purple_400), + ContextCompat.getColor(context, R.color.md_purple_500), + ContextCompat.getColor(context, R.color.md_purple_600), + ContextCompat.getColor(context, R.color.md_purple_700), + ContextCompat.getColor(context, R.color.md_purple_800), + ContextCompat.getColor(context, R.color.md_purple_900) + }; + } else if (c == ContextCompat.getColor(context, R.color.md_deep_purple_500)) { + return new int[]{ + ContextCompat.getColor(context, R.color.md_deep_purple_200), + ContextCompat.getColor(context, R.color.md_deep_purple_300), + ContextCompat.getColor(context, R.color.md_deep_purple_400), + ContextCompat.getColor(context, R.color.md_deep_purple_500), + ContextCompat.getColor(context, R.color.md_deep_purple_600), + ContextCompat.getColor(context, R.color.md_deep_purple_700), + ContextCompat.getColor(context, R.color.md_deep_purple_800), + ContextCompat.getColor(context, R.color.md_deep_purple_900) + }; + } else if (c == ContextCompat.getColor(context, R.color.md_indigo_500)) { + return new int[]{ + ContextCompat.getColor(context, R.color.md_indigo_200), + ContextCompat.getColor(context, R.color.md_indigo_300), + ContextCompat.getColor(context, R.color.md_indigo_400), + ContextCompat.getColor(context, R.color.md_indigo_500), + ContextCompat.getColor(context, R.color.md_indigo_600), + ContextCompat.getColor(context, R.color.md_indigo_700), + ContextCompat.getColor(context, R.color.md_indigo_800), + ContextCompat.getColor(context, R.color.md_indigo_900) + }; + } else if (c == ContextCompat.getColor(context, R.color.md_blue_500)) { + return new int[]{ + ContextCompat.getColor(context, R.color.md_blue_200), + ContextCompat.getColor(context, R.color.md_blue_300), + ContextCompat.getColor(context, R.color.md_blue_400), + ContextCompat.getColor(context, R.color.md_blue_500), + ContextCompat.getColor(context, R.color.md_blue_600), + ContextCompat.getColor(context, R.color.md_blue_650), + ContextCompat.getColor(context, R.color.md_blue_700), + ContextCompat.getColor(context, R.color.md_blue_750), + ContextCompat.getColor(context, R.color.md_blue_800), + ContextCompat.getColor(context, R.color.md_blue_900) + }; + } else if (c == ContextCompat.getColor(context, R.color.md_light_blue_500)) { + return new int[]{ + ContextCompat.getColor(context, R.color.md_light_blue_200), + ContextCompat.getColor(context, R.color.md_light_blue_300), + ContextCompat.getColor(context, R.color.md_light_blue_400), + ContextCompat.getColor(context, R.color.md_light_blue_500), + ContextCompat.getColor(context, R.color.md_light_blue_600), + ContextCompat.getColor(context, R.color.md_light_blue_700), + ContextCompat.getColor(context, R.color.md_light_blue_800), + ContextCompat.getColor(context, R.color.md_light_blue_900) + }; + } else if (c == ContextCompat.getColor(context, R.color.md_cyan_500)) { + return new int[]{ + ContextCompat.getColor(context, R.color.md_cyan_200), + ContextCompat.getColor(context, R.color.md_cyan_300), + ContextCompat.getColor(context, R.color.md_cyan_400), + ContextCompat.getColor(context, R.color.md_cyan_500), + ContextCompat.getColor(context, R.color.md_cyan_600), + ContextCompat.getColor(context, R.color.md_cyan_700), + ContextCompat.getColor(context, R.color.md_cyan_800), + ContextCompat.getColor(context, R.color.md_cyan_900) + }; + } else if (c == ContextCompat.getColor(context, R.color.md_teal_500)) { + return new int[]{ + ContextCompat.getColor(context, R.color.md_teal_200), + ContextCompat.getColor(context, R.color.md_teal_300), + ContextCompat.getColor(context, R.color.md_teal_400), + ContextCompat.getColor(context, R.color.md_teal_500), + ContextCompat.getColor(context, R.color.md_teal_600), + ContextCompat.getColor(context, R.color.md_teal_700), + ContextCompat.getColor(context, R.color.md_teal_800), + ContextCompat.getColor(context, R.color.md_teal_900) + }; + } else if (c == ContextCompat.getColor(context, R.color.md_green_500)) { + return new int[]{ + ContextCompat.getColor(context, R.color.md_green_200), + ContextCompat.getColor(context, R.color.md_green_300), + ContextCompat.getColor(context, R.color.md_green_400), + ContextCompat.getColor(context, R.color.md_green_500), + ContextCompat.getColor(context, R.color.md_green_600), + ContextCompat.getColor(context, R.color.md_green_700), + ContextCompat.getColor(context, R.color.md_green_800), + ContextCompat.getColor(context, R.color.md_green_900) + }; + } else if (c == ContextCompat.getColor(context, R.color.md_light_green_500)) { + return new int[]{ + ContextCompat.getColor(context, R.color.md_light_green_200), + ContextCompat.getColor(context, R.color.md_light_green_300), + ContextCompat.getColor(context, R.color.md_light_green_400), + ContextCompat.getColor(context, R.color.md_light_green_500), + ContextCompat.getColor(context, R.color.md_light_green_600), + ContextCompat.getColor(context, R.color.md_light_green_700), + ContextCompat.getColor(context, R.color.md_light_green_800), + ContextCompat.getColor(context, R.color.md_light_green_900) + }; + } else if (c == ContextCompat.getColor(context, R.color.md_lime_500)) { + return new int[]{ + ContextCompat.getColor(context, R.color.md_lime_200), + ContextCompat.getColor(context, R.color.md_lime_300), + ContextCompat.getColor(context, R.color.md_lime_400), + ContextCompat.getColor(context, R.color.md_lime_500), + ContextCompat.getColor(context, R.color.md_lime_600), + ContextCompat.getColor(context, R.color.md_lime_700), + ContextCompat.getColor(context, R.color.md_lime_800), + ContextCompat.getColor(context, R.color.md_lime_900) + }; + } else if (c == ContextCompat.getColor(context, R.color.md_yellow_500)) { + return new int[]{ + ContextCompat.getColor(context, R.color.md_yellow_400), + ContextCompat.getColor(context, R.color.md_yellow_500), + ContextCompat.getColor(context, R.color.md_yellow_600), + ContextCompat.getColor(context, R.color.md_yellow_700), + ContextCompat.getColor(context, R.color.md_yellow_800), + ContextCompat.getColor(context, R.color.md_yellow_900) + }; + } else if (c == ContextCompat.getColor(context, R.color.md_amber_500)) { + return new int[]{ + ContextCompat.getColor(context, R.color.md_amber_200), + ContextCompat.getColor(context, R.color.md_amber_300), + ContextCompat.getColor(context, R.color.md_amber_400), + ContextCompat.getColor(context, R.color.md_amber_500), + ContextCompat.getColor(context, R.color.md_amber_600), + ContextCompat.getColor(context, R.color.md_amber_700), + ContextCompat.getColor(context, R.color.md_amber_800), + ContextCompat.getColor(context, R.color.md_amber_900) + }; + } else if (c == ContextCompat.getColor(context, R.color.md_orange_500)) { + return new int[]{ + ContextCompat.getColor(context, R.color.md_orange_200), + ContextCompat.getColor(context, R.color.md_orange_300), + ContextCompat.getColor(context, R.color.md_orange_400), + ContextCompat.getColor(context, R.color.md_orange_500), + ContextCompat.getColor(context, R.color.md_orange_600), + ContextCompat.getColor(context, R.color.md_orange_700), + ContextCompat.getColor(context, R.color.md_orange_800), + ContextCompat.getColor(context, R.color.md_orange_900) + }; + } else if (c == ContextCompat.getColor(context, R.color.md_deep_orange_500)) { + return new int[]{ + ContextCompat.getColor(context, R.color.md_deep_orange_200), + ContextCompat.getColor(context, R.color.md_deep_orange_300), + ContextCompat.getColor(context, R.color.md_deep_orange_400), + ContextCompat.getColor(context, R.color.md_deep_orange_500), + ContextCompat.getColor(context, R.color.md_deep_orange_600), + ContextCompat.getColor(context, R.color.md_deep_orange_650), + ContextCompat.getColor(context, R.color.md_deep_orange_700), + ContextCompat.getColor(context, R.color.md_deep_orange_800), + ContextCompat.getColor(context, R.color.md_deep_orange_900) + }; + } else if (c == ContextCompat.getColor(context, R.color.md_brown_500)) { + return new int[]{ + ContextCompat.getColor(context, R.color.md_brown_200), + ContextCompat.getColor(context, R.color.md_brown_300), + ContextCompat.getColor(context, R.color.md_brown_400), + ContextCompat.getColor(context, R.color.md_brown_500), + ContextCompat.getColor(context, R.color.md_brown_600), + ContextCompat.getColor(context, R.color.md_brown_700), + ContextCompat.getColor(context, R.color.md_brown_800), + ContextCompat.getColor(context, R.color.md_brown_900) + }; + } else if (c == ContextCompat.getColor(context, R.color.md_grey_500)) { + return new int[]{ + ContextCompat.getColor(context, R.color.md_grey_400), + ContextCompat.getColor(context, R.color.md_grey_500), + ContextCompat.getColor(context, R.color.md_grey_600), + ContextCompat.getColor(context, R.color.md_grey_700), + ContextCompat.getColor(context, R.color.md_grey_800), + ContextCompat.getColor(context, R.color.md_grey_900), + Color.parseColor("#000000") + }; + } else { + return new int[]{ + ContextCompat.getColor(context, R.color.md_blue_grey_300), + ContextCompat.getColor(context, R.color.md_blue_grey_400), + ContextCompat.getColor(context, R.color.md_blue_grey_500), + ContextCompat.getColor(context, R.color.md_blue_grey_600), + ContextCompat.getColor(context, R.color.md_blue_grey_700), + ContextCompat.getColor(context, R.color.md_blue_grey_800), + ContextCompat.getColor(context, R.color.md_blue_grey_900) + }; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/ThemeHelper.java b/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/ThemeHelper.java new file mode 100644 index 000000000..510bb5668 --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/ThemeHelper.java @@ -0,0 +1,179 @@ +/* + 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 . + This class is inspired by org.horasapps.LeafPic + */ +package com.github.dfa.diaspora_android.ui.theme; + +import android.content.DialogInterface; +import android.content.res.ColorStateList; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.os.Build; +import android.support.design.widget.TabLayout; +import android.support.v4.content.ContextCompat; +import android.support.v4.widget.CompoundButtonCompat; +import android.support.v7.app.AlertDialog; +import android.support.v7.widget.ActionMenuView; +import android.support.v7.widget.Toolbar; +import android.view.View; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.ProgressBar; +import android.widget.RadioButton; +import android.widget.RadioGroup; +import android.widget.TextView; + +import com.github.dfa.diaspora_android.R; +import com.github.dfa.diaspora_android.util.AppSettings; + +/** + * Singleton that can be used to color views + * Created by vanitas on 06.10.16. + */ + +public class ThemeHelper { + private AppSettings appSettings; + private static ThemeHelper instance; + + private ThemeHelper(AppSettings appSettings) { + this.appSettings = appSettings; + } + + public static ThemeHelper getInstance(AppSettings appSettings) { + if (instance == null) { + instance = new ThemeHelper(appSettings); + } + return instance; + } + + public static ThemeHelper getInstance() { + if (instance == null) + throw new IllegalStateException("ThemeHelper must be initialized using getInstance(AppSettingsBase) before it can be used!"); + return instance; + } + + public static void updateEditTextColor(EditText editText) { + if (editText != null) { + editText.setHighlightColor(getInstance().appSettings.getAccentColor()); + if (Build.VERSION.SDK_INT >= 21) { + editText.getBackground().mutate().setColorFilter(getAccentColor(), PorterDuff.Mode.SRC_ATOP); + } + } + } + + public static void updateCheckBoxColor(CheckBox checkBox) { + if (checkBox != null) { + int states[][] = {{android.R.attr.state_checked}, {}}; + int colors[] = {ThemeHelper.getAccentColor(), getNeutralGreyColor()}; + CompoundButtonCompat.setButtonTintList(checkBox, new ColorStateList(states, colors)); + } + } + + public static void updateTabLayoutColor(TabLayout tabLayout) { + if (tabLayout != null) { + tabLayout.setBackgroundColor(getInstance().appSettings.getPrimaryColor()); + tabLayout.setSelectedTabIndicatorColor(getInstance().appSettings.getAccentColor()); + } + } + + public static void updateTextViewLinkColor(TextView textView) { + if (textView != null) { + textView.setHighlightColor(getInstance().appSettings.getAccentColor()); + textView.setLinkTextColor(getInstance().appSettings.getAccentColor()); + } + } + + public static void updateTextViewTextColor(TextView textView) { + if (textView != null) { + textView.setTextColor(getInstance().appSettings.getAccentColor()); + } + } + + public static void updateToolbarColor(Toolbar toolbar) { + if (toolbar != null) { + toolbar.setBackgroundColor(getInstance().appSettings.getPrimaryColor()); + } + } + + public static void updateActionMenuViewColor(ActionMenuView actionMenuView) { + if (actionMenuView != null) { + actionMenuView.setBackgroundColor(getInstance().appSettings.getPrimaryColor()); + } + } + + public static int getPrimaryColor() { + return getInstance().appSettings.getPrimaryColor(); + } + + public static int getAccentColor() { + return getInstance().appSettings.getAccentColor(); + } + + public static void setPrimaryColorAsBackground(View view) { + if (view != null) { + view.setBackgroundColor(getPrimaryColor()); + } + } + + public static int getPrimaryDarkColor() { + return ColorPalette.getObscuredColor(getPrimaryColor()); + } + + public static void updateProgressBarColor(ProgressBar progressBar) { + if (progressBar != null && progressBar.getProgressDrawable() != null) { + progressBar.getProgressDrawable().setColorFilter(getAccentColor(), PorterDuff.Mode.SRC_IN); + } + } + + public static void updateRadioGroupColor(RadioGroup radioGroup) { + if (radioGroup != null && Build.VERSION.SDK_INT >= 21) { + for (int i = 0; i < radioGroup.getChildCount(); ++i) { + RadioButton btn = ((RadioButton) radioGroup.getChildAt(i)); + btn.setButtonTintList(new ColorStateList( + new int[][]{new int[]{-android.R.attr.state_enabled}, new int[]{android.R.attr.state_enabled}}, + new int[]{Color.BLACK, ThemeHelper.getAccentColor()})); + btn.invalidate(); + } + } + } + + public static int getNeutralGreyColor() { + return ContextCompat.getColor(getInstance().appSettings.getContext(), R.color.md_grey_800); + } + + public static void updateAlertDialogColor(AlertDialog alertDialog) { + if (alertDialog != null) { + for (int i : new int[]{ + DialogInterface.BUTTON_POSITIVE, + DialogInterface.BUTTON_NEUTRAL, + DialogInterface.BUTTON_NEGATIVE}) { + Button b = alertDialog.getButton(i); + if (b != null) { + b.setTextColor(getAccentColor()); + } + } + } + } + + public static void updateButtonTextColor(Button button) { + if (button != null) { + button.setTextColor(getAccentColor()); + } + } +} diff --git a/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/Themeable.java b/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/Themeable.java new file mode 100644 index 000000000..61ab17fa2 --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/Themeable.java @@ -0,0 +1,10 @@ +package com.github.dfa.diaspora_android.ui.theme; + +/** + * Interface that allows setting Theme colors + * Created by vanitas on 24.10.16. + */ + +public interface Themeable { + void setColors(); +} 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 new file mode 100644 index 000000000..d8651bb5a --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/ThemedActivity.java @@ -0,0 +1,91 @@ +/* + 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.annotation.TargetApi; +import android.content.pm.ActivityInfo; +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.ContextUtils; + +/** + * Activity that supports color schemes + * Created by vanitas on 06.10.16. + */ + +public abstract class ThemedActivity extends AppCompatActivity { + + protected AppSettings getAppSettings() { + return ((App) getApplication()).getSettings(); + } + + @Override + protected void onResume() { + super.onResume(); + ThemeHelper.getInstance(getAppSettings()); + updateLanguage(); + updateStatusBarColor(); + updateRecentAppColor(); + applyColorToViews(); + updateScreenRotation(); + } + + protected abstract void applyColorToViews(); + + /** + * Update color of the status bar + */ + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private void updateStatusBarColor() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + getWindow().setStatusBarColor(ThemeHelper.getPrimaryDarkColor()); + } + } + + /** + * Update primary color in recent apps overview + */ + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private void updateRecentAppColor() { + + } + + protected void updateScreenRotation() { + String setting = getAppSettings().getScreenRotation(); + int rotation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; // Default (system settings) + + if (setting.equals(getString(R.string.rotation_val_sensor))) { + rotation = ActivityInfo.SCREEN_ORIENTATION_SENSOR; + } else if (setting.equals(getString(R.string.rotation_val_portrait))) { + rotation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT; + } else if (setting.equals(getString(R.string.rotation_val_landscape))) { + rotation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE; + } + setRequestedOrientation(rotation); + } + + public void updateLanguage() { + AppSettings appSettings = getAppSettings(); + ContextUtils.get().setAppLanguage(appSettings.getLanguage()); + } +} diff --git a/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/ThemedAlertDialogBuilder.java b/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/ThemedAlertDialogBuilder.java new file mode 100644 index 000000000..c5a3cde7b --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/ThemedAlertDialogBuilder.java @@ -0,0 +1,45 @@ +package com.github.dfa.diaspora_android.ui.theme; + +import android.content.Context; +import android.content.DialogInterface; +import android.support.annotation.NonNull; +import android.support.annotation.StyleRes; +import android.support.v7.app.AlertDialog; + +import com.github.dfa.diaspora_android.util.AppSettings; + +/** + * AlertDialog Builder that colors its buttons + * Created by vanitas on 06.11.16. + */ + +public class ThemedAlertDialogBuilder extends AlertDialog.Builder { + protected AppSettings appSettings; + + public ThemedAlertDialogBuilder(@NonNull Context context, AppSettings appSettings) { + super(context); + this.appSettings = appSettings; + } + + public ThemedAlertDialogBuilder(@NonNull Context context, @StyleRes int themeResId, AppSettings appSettings) { + super(context, themeResId); + this.appSettings = appSettings; + } + + @Override + public AlertDialog create() { + final AlertDialog dialog = super.create(); + dialog.setOnShowListener(new DialogInterface.OnShowListener() { + @Override + public void onShow(DialogInterface dialogInterface) { + applyColors(dialog); + } + }); + return dialog; + } + + private void applyColors(AlertDialog alertDialog) { + ThemeHelper.getInstance(appSettings); + ThemeHelper.updateAlertDialogColor(alertDialog); + } +} diff --git a/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/ThemedAppCompatDialogFragment.java b/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/ThemedAppCompatDialogFragment.java new file mode 100644 index 000000000..11e6f707e --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/ThemedAppCompatDialogFragment.java @@ -0,0 +1,28 @@ +package com.github.dfa.diaspora_android.ui.theme; + +import android.app.Dialog; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v7.app.AppCompatDialogFragment; + +import com.github.dfa.diaspora_android.util.AppSettings; + +/** + * Themed DialogFragment + * Created by vanitas on 22.10.16. + */ + +public abstract class ThemedAppCompatDialogFragment extends AppCompatDialogFragment { + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + Dialog dialog = super.onCreateDialog(savedInstanceState); + ThemeHelper.getInstance(getAppSettings()); + return dialog; + } + + protected abstract void applyColorsToViews(); + + protected abstract AppSettings getAppSettings(); +} 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 new file mode 100644 index 000000000..0c829342e --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/ThemedCheckBoxPreference.java @@ -0,0 +1,48 @@ +package com.github.dfa.diaspora_android.ui.theme; + +import android.content.Context; +import android.preference.CheckBoxPreference; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; + +import com.github.dfa.diaspora_android.util.AppSettings; + +/** + * CheckboxPreference that colors its checkbox with accent color + * Created by vanitas on 24.10.16. + */ + +public class ThemedCheckBoxPreference extends CheckBoxPreference implements Themeable { + protected View rootLayout; + + @SuppressWarnings("unused") + public ThemedCheckBoxPreference(Context context) { + super(context); + } + + @SuppressWarnings("unused") + public ThemedCheckBoxPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @SuppressWarnings("unused") + public ThemedCheckBoxPreference(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected View onCreateView(ViewGroup parent) { + rootLayout = super.onCreateView(parent); + setColors(); + return rootLayout; + } + + @Override + public void setColors() { + CheckBox checkBox = rootLayout.findViewById(android.R.id.checkbox); + ThemeHelper.getInstance(AppSettings.get()); + ThemeHelper.updateCheckBoxColor(checkBox); + } +} \ No newline at end of file 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 new file mode 100644 index 000000000..c335d9290 --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/ThemedColorPickerPreference.java @@ -0,0 +1,64 @@ +package com.github.dfa.diaspora_android.ui.theme; + +import android.content.Context; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; +import android.preference.Preference; +import android.util.AttributeSet; +import android.view.View; +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.ContextUtils; + +/** + * Preference that shows selected Color in a circle + * Created by vanitas on 25.10.16. + */ + +public class ThemedColorPickerPreference extends Preference implements Themeable { + protected ImageView colorPreview; + + @SuppressWarnings("unused") + public ThemedColorPickerPreference(Context context) { + super(context); + } + + @SuppressWarnings("unused") + public ThemedColorPickerPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @SuppressWarnings("unused") + public ThemedColorPickerPreference(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void onBindView(View view) { + super.onBindView(view); + colorPreview = view.findViewById(android.R.id.icon); + setColors(); + } + + @Override + public void setColors() { + Drawable circle; + if (colorPreview != null && (circle = colorPreview.getDrawable()) != null) { + Context c = getContext(); + AppSettings appSettings = AppSettings.get(); + String key = getKey(); + + 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(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 new file mode 100644 index 000000000..6158a4a9a --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/ThemedFragment.java @@ -0,0 +1,49 @@ +/* + 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 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 GsFragmentBase { + protected AppSettings getAppSettings() { + return ((App) getActivity().getApplication()).getSettings(); + } + + protected abstract void applyColorToViews(); + + @Override + public void onResume() { + super.onResume(); + 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 new file mode 100644 index 000000000..5df13fda9 --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/ThemedPreferenceCategory.java @@ -0,0 +1,51 @@ +package com.github.dfa.diaspora_android.ui.theme; + +import android.content.Context; +import android.preference.PreferenceCategory; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.github.dfa.diaspora_android.util.AppSettings; + +/** + * PreferenceCategory with a colored title + * Created by vanitas on 24.10.16. + */ + +public class ThemedPreferenceCategory extends PreferenceCategory implements Themeable { + protected TextView titleTextView; + + @SuppressWarnings("unused") + public ThemedPreferenceCategory(Context context) { + super(context); + } + + @SuppressWarnings("unused") + public ThemedPreferenceCategory(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @SuppressWarnings("unused") + public ThemedPreferenceCategory(Context context, AttributeSet attrs, + int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected View onCreateView(ViewGroup parent) { + View rootLayout = super.onCreateView(parent); + this.titleTextView = rootLayout.findViewById(android.R.id.title); + setColors(); + return rootLayout; + } + + @Override + public void setColors() { + if (titleTextView != null) { + ThemeHelper.getInstance(AppSettings.get()); + ThemeHelper.updateTextViewTextColor(titleTextView); + } + } +} diff --git a/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/ThemedPreferenceFragment.java b/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/ThemedPreferenceFragment.java new file mode 100644 index 000000000..a3a96c5ab --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/ThemedPreferenceFragment.java @@ -0,0 +1,45 @@ +package com.github.dfa.diaspora_android.ui.theme; + +import android.os.Build; +import android.preference.Preference; +import android.preference.PreferenceFragment; +import android.preference.PreferenceScreen; +import android.view.Window; + +import com.github.dfa.diaspora_android.App; +import com.github.dfa.diaspora_android.util.AppSettings; + +/** + * PreferenceFragment with a colored status bar + * Created by vanitas on 24.10.16. + */ + +public abstract class ThemedPreferenceFragment extends PreferenceFragment { + public abstract void updateViewColors(); + + @Override + public void onResume() { + super.onResume(); + updateViewColors(); + } + + @Override + public boolean onPreferenceTreeClick(PreferenceScreen screen, Preference preference) { + if (isAdded()) { + App app = ((App) getActivity().getApplication()); + AppSettings appSettings = app.getSettings(); + if (Build.VERSION.SDK_INT >= 21) { + if (preference instanceof PreferenceScreen && ((PreferenceScreen) preference).getDialog() != null) { + Window window = ((PreferenceScreen) preference).getDialog().getWindow(); + if (window != null) { + ThemeHelper.getInstance(appSettings); + window.setStatusBarColor(ThemeHelper.getPrimaryDarkColor()); + } + } + } + } + return super.onPreferenceTreeClick(screen, preference); + } + + public abstract String getFragmentTag(); +} 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 new file mode 100644 index 000000000..6bd8c1865 --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/ui/theme/ThemedVisibilityPreference.java @@ -0,0 +1,35 @@ +package com.github.dfa.diaspora_android.ui.theme; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.CheckBox; + +import com.github.dfa.diaspora_android.R; +import com.github.dfa.diaspora_android.util.AppSettings; + +/** + * ThemedCheckBoxPreference with visibility icons instead of checkbox. TODO: Make more flexible? + * Created by vanitas on 25.10.16. + */ + +public class ThemedVisibilityPreference extends ThemedCheckBoxPreference { + public ThemedVisibilityPreference(Context context) { + super(context); + } + + public ThemedVisibilityPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public ThemedVisibilityPreference(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + public void setColors() { + 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/ActivityUtils.java b/app/src/main/java/com/github/dfa/diaspora_android/util/ActivityUtils.java new file mode 100644 index 000000000..817b8e103 --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/util/ActivityUtils.java @@ -0,0 +1,86 @@ +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.BuildConfig; +import com.github.dfa.diaspora_android.R; +import com.github.dfa.diaspora_android.web.WebHelper; + +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 ActivityUtils extends net.gsantner.opoc.util.ActivityUtils { + public ActivityUtils(Activity activity) { + super(activity); + } + + + 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(ActivityUtils.class, Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getAbsolutePath()); + File storageDir = Environment.getExternalStoragePublicDirectory( + Environment.DIRECTORY_PICTURES); + return new File( + imageFileName + /* prefix */ + ".jpg", /* suffix */ + storageDir.getAbsolutePath() /* directory */ + ); + } + + /** + * Show Information if user is offline, returns true if is not connected to internet + * + * @param anchor A view anchor + */ + public boolean showInfoIfUserNotConnectedToInternet(View anchor) { + boolean isOnline = WebHelper.isOnline(_context); + if (!isOnline) { + showSnackBar(R.string.no_internet, true); + } + return !isOnline; + } + + 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()); + } + } + } + } + } + + /** + * 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 000000000..98017bde7 --- /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/AppLog.java b/app/src/main/java/com/github/dfa/diaspora_android/util/AppLog.java new file mode 100644 index 000000000..2f0f38c80 --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/util/AppLog.java @@ -0,0 +1,244 @@ +/* + 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.util; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.Observable; +import java.util.Observer; + +/** + * Class that logs debug messages to Androids Log and to additional targets (like textviews) + * Created by gregor on 18.09.16. + */ +public class AppLog { + private final static String APP_TAG = "d*"; + private static boolean loggingEnabled = true; + private static boolean loggingSpamEnabled = false; + + public static boolean isLoggingEnabled() { + return loggingEnabled; + } + + public static void setLoggingEnabled(boolean loggingEnabled) { + AppLog.loggingEnabled = loggingEnabled; + } + + public static boolean isLoggingSpamEnabled() { + return loggingSpamEnabled; + } + + public static void setLoggingSpamEnabled(boolean loggingSpamEnabled) { + AppLog.loggingSpamEnabled = loggingSpamEnabled; + } + + private static String getLogPrefix(Object source) { + return APP_TAG + "-" + source.getClass().getCanonicalName(); + } + + /* + * + * LOGGER METHODS + * + */ + public static void v(Object source, String _text) { + if (isLoggingEnabled()) { + if (source != null) { + Log.v(getLogPrefix(source), _text); + } else { + Log.v("null", _text); + } + } + } + + public static void i(Object source, String _text) { + if (isLoggingEnabled()) { + if (source != null) { + Log.i(getLogPrefix(source), _text); + } else { + Log.i("null", _text); + } + } + } + + public static void d(Object source, String _text) { + if (isLoggingEnabled()) { + if (source != null) { + Log.d(getLogPrefix(source), _text); + } else { + Log.d("null", _text); + } + } + } + + public static void e(Object source, String _text) { + if (isLoggingEnabled()) { + if (source != null) { + Log.e(getLogPrefix(source), _text); + } else { + Log.e("null", _text); + } + } + } + + public static void w(Object source, String _text) { + if (isLoggingEnabled()) { + if (source != null) { + Log.w(getLogPrefix(source), _text); + } else { + Log.w("null", _text); + } + } + } + + public static void spam(Object source, String _text) { + if (isLoggingEnabled() && isLoggingSpamEnabled()) { + if (source != null) { + Log.v(getLogPrefix(source), _text); + } else { + Log.v("null", _text); + } + } + } + + + /** + * Class that saves logs eg. for later debugging. + * TODO: Differentiate log types (error/debug/info...) + */ + public static class Log extends Observable { + public static final int MAX_BUFFER_SIZE = 100; + + public static Log instance; + private AppSettings appSettings; + private final DateFormat dateFormat; + private final ArrayList logBuffer; + private final ArrayList observers; + + private Log() { + this(null); + } + + private Log(AppSettings appSettings) { + if (appSettings != null) { + //TODO: Store/Restore logBuffer between app starts + logBuffer = new ArrayList<>(); + } else { + logBuffer = new ArrayList<>(); + } + SimpleDateFormat.getTimeInstance(); + dateFormat = SimpleDateFormat.getDateInstance(); + observers = new ArrayList<>(); + } + + public static Log getInstance() { + if (instance == null) instance = new Log(); + return instance; + } + + public static Log getInstance(AppSettings appSettings) { + if (instance == null) instance = new Log(appSettings); + return instance; + } + + private static String time() { + return getInstance().dateFormat.format(new Date()) + ": "; + } + + public static void d(String tag, String msg) { + Log l = getInstance(); + android.util.Log.d(tag, msg); + l.addLogEntry(msg); + l.notifyLogBufferChanged(); + } + + public static void e(String tag, String msg) { + Log l = getInstance(); + android.util.Log.e(tag, msg); + l.addLogEntry(msg); + l.notifyLogBufferChanged(); + } + + public static void i(String tag, String msg) { + Log l = getInstance(); + android.util.Log.i(tag, msg); + l.addLogEntry(msg); + l.notifyLogBufferChanged(); + } + + public static void v(String tag, String msg) { + Log l = getInstance(); + android.util.Log.v(tag, msg); + l.addLogEntry(msg); + l.notifyLogBufferChanged(); + } + + public static void w(String tag, String msg) { + Log l = getInstance(); + android.util.Log.w(tag, msg); + l.addLogEntry(msg); + l.notifyLogBufferChanged(); + } + + public static void wtf(String tag, String msg) { + Log l = getInstance(); + android.util.Log.wtf(tag, msg); + l.addLogEntry(msg); + l.notifyLogBufferChanged(); + } + + public synchronized static ArrayList getLogBufferArray() { + return getInstance().logBuffer; + } + + public synchronized static String getLogBuffer() { + String out = ""; + for (String s : getInstance().logBuffer) { + out = out + s + "\n"; + } + return out; + } + + private void notifyLogBufferChanged() { + if (observers == null) return; + for (Observer o : observers) { + if (o != null) { + o.update(this, null); + } + } + } + + private synchronized void addLogEntry(String msg) { + logBuffer.add(time() + msg); + while (logBuffer.size() > MAX_BUFFER_SIZE) { + logBuffer.remove(0); + } + } + + public static void addLogObserver(Observer observer) { + getInstance().observers.add(observer); + } + + public static void removeLogObserver(Observer o) { + getInstance().observers.remove(o); + } + } +} 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 new file mode 100644 index 000000000..79953d7af --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/util/AppSettings.java @@ -0,0 +1,468 @@ +/* + 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.util; + +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; +import com.github.dfa.diaspora_android.R; +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; + +/** + * Settings + * Created by gsantner (http://gsantner.net/) on 20.03.16. Part of dandelion*. + */ +@SuppressWarnings("ConstantConditions") +public class AppSettings extends SharedPreferencesPropertyBackend { + private final SharedPreferences _prefPod; + private DiasporaPod currentPod0Cached; + + public static AppSettings get() { + return new AppSettings(App.get()); + } + + public AppSettings(Context context) { + super(context); + _prefPod = _context.getSharedPreferences("pod0", Context.MODE_PRIVATE); + } + + /** + * 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); + } + + /** + * 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); + } + + //################################# + //## Getter & Setter for settings + //################################# + public String getProfileId() { + return getString(R.string.pref_key__podprofile_id, "", _prefPod); + } + + public void setProfileId(String profileId) { + setString(R.string.pref_key__podprofile_id, profileId, _prefPod); + } + + public boolean isLoadImages() { + return getBool(R.string.pref_key__load_images, true); + } + + public int getMinimumFontSize() { + switch (getString(R.string.pref_key__font_size, "")) { + case "huge": + return 20; + case "large": + return 16; + case "normal": + return 8; + default: + setString(R.string.pref_key__font_size, "normal"); + return 8; + } + } + + public String getAvatarUrl() { + return getString(R.string.pref_key__podprofile_avatar_url, "", _prefPod); + } + + public void setAvatarUrl(String avatarUrl) { + setString(R.string.pref_key__podprofile_avatar_url, avatarUrl, _prefPod); + } + + public String getName() { + return getString(R.string.pref_key__podprofile_name, "", _prefPod); + } + + public void setName(String name) { + setString(R.string.pref_key__podprofile_name, name, _prefPod); + } + + public DiasporaPod getPod() { + if (currentPod0Cached == null) { + String pref = getString(R.string.pref_key__current_pod_0, "", _prefPod); + + try { + currentPod0Cached = new DiasporaPod().fromJson(new JSONObject(pref)); + } catch (JSONException e) { + currentPod0Cached = null; + } + } + return currentPod0Cached; + } + + public void setPod(DiasporaPod pod) { + try { + 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(R.string.pref_key__current_pod_0, "", _prefPod).equals(""); + } + + public void setPodAspects(DiasporaAspect[] 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(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]); + } + return aspects; + } + + public String[] getFollowedTags() { + return getStringArray(R.string.pref_key__podprofile_followed_tags, _prefPod); + } + + public void setFollowedTags(String[] values) { + setStringArray(R.string.pref_key__podprofile_followed_tags, values, _prefPod); + } + + public String[] getFollowedTagsFavs() { + return getStringArray(R.string.pref_key__podprofile_followed_tags_favs, _prefPod); + } + + public void setFollowedTagsFavs(List values) { + setStringList(R.string.pref_key__podprofile_followed_tags_favs, values, _prefPod); + } + + public String[] getAspectFavs() { + return getStringArray(R.string.pref_key__podprofile_aspects_favs, _prefPod); + } + + public void setAspectFavs(List values) { + setStringList(R.string.pref_key__podprofile_aspects_favs, values, _prefPod); + } + + public int getUnreadMessageCount() { + return getInt(R.string.pref_key__podprofile_unread_message_count, 0, _prefPod); + } + + public void setUnreadMessageCount(int unreadMessageCount) { + setInt(R.string.pref_key__podprofile_unread_message_count, unreadMessageCount, _prefPod); + } + + public int getNotificationCount() { + return getInt(R.string.pref_key__podprofile_notification_count, 0, _prefPod); + } + + public void setNotificationCount(int notificationCount) { + setInt(R.string.pref_key__podprofile_notification_count, notificationCount, _prefPod); + } + + public boolean isAppendSharedViaApp() { + 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(rstr(R.string.pref_key__http_proxy_enabled), enabled).commit(); + } + + /** + * Default return value: false + * + * @return whether proxy is enabled or not + */ + public boolean isProxyHttpEnabled() { + try { + return getBool(R.string.pref_key__http_proxy_enabled, false); + } catch (ClassCastException e) { + setProxyHttpEnabled(false); + return false; + } + } + + public boolean wasProxyEnabled() { + return getBool(R.string.pref_key__proxy_was_enabled, false); + } + + /** + * Needed in order to determine, whether the proxy has just been disabled (trigger app restart) + * or if proxy was disabled before (do not restart app) + * + * @param b new value + */ + @SuppressLint("CommitPrefEdits") + public void setProxyWasEnabled(boolean b) { + _prefApp.edit().putBoolean(rstr(R.string.pref_key__proxy_was_enabled), b).commit(); + } + + /** + * Default value: "" + * + * @return proxy host + */ + public String getProxyHttpHost() { + return getString(R.string.pref_key__http_proxy_host, ""); + } + + public void setProxyHttpHost(String value) { + setString(R.string.pref_key__http_proxy_host, value); + } + + /** + * Default value: 0 + * + * @return proxy port + */ + public int getProxyHttpPort() { + try { + String str = getString(R.string.pref_key__http_proxy_port, "0"); + return Integer.parseInt(str); + } catch (ClassCastException e) { + int port = getInt(R.string.pref_key__http_proxy_port, 0); + setProxyHttpPort(port); + return port; + } + } + + public void setProxyHttpPort(int value) { + setString(R.string.pref_key__http_proxy_port, Integer.toString(value)); + } + + public ProxyHandler.ProxySettings getProxySettings() { + return new ProxyHandler.ProxySettings(isProxyHttpEnabled(), getProxyHttpHost(), getProxyHttpPort()); + } + + public boolean isIntellihideToolbars() { + return getBool(R.string.pref_key__intellihide_toolbars, false); + } + + public boolean isChromeCustomTabsEnabled() { + return getBool(R.string.pref_key__chrome_custom_tabs_enabled, true); + } + + public boolean isLoggingEnabled() { + return getBool(R.string.pref_key__logging_enabled, false); + } + + public boolean isLoggingSpamEnabled() { + return getBool(R.string.pref_key__logging_spam_enabled, false); + } + + public boolean isVisibleInNavExit() { + return getBool(R.string.pref_key__visibility_nav__exit, true); + } + + public boolean isVisibleInNavHelp_license() { + return getBool(R.string.pref_key__visibility_nav__help_license, true); + } + + public boolean isVisibleInNavPublic_activities() { + return getBool(R.string.pref_key__visibility_nav__public_activities, false); + } + + public boolean isVisibleInNavMentions() { + return getBool(R.string.pref_key__visibility_nav__mentions, false); + } + + public boolean isVisibleInNavCommented() { + return getBool(R.string.pref_key__visibility_nav__commented, true); + } + + public boolean isVisibleInNavLiked() { + return getBool(R.string.pref_key__visibility_nav__liked, true); + } + + public boolean isVisibleInNavActivities() { + return getBool(R.string.pref_key__visibility_nav__activities, true); + } + + public boolean isVisibleInNavAspects() { + return getBool(R.string.pref_key__visibility_nav__aspects, true); + } + + public boolean isVisibleInNavFollowed_tags() { + return getBool(R.string.pref_key__visibility_nav__followed_tags, true); + } + + public boolean isVisibleInNavProfile() { + return getBool(R.string.pref_key__visibility_nav__profile, true); + } + + public boolean isVisibleInNavContacts() { + return getBool(R.string.pref_key__visibility_nav__contacts, false); + } + + public boolean isVisibleInNavStatistics() { + return getBool(R.string.pref_key__visibility_nav__statistics, false); + } + + public boolean isVisibleInNavReports() { + return getBool(R.string.pref_key__visibility_nav__reports, false); + } + + public boolean isVisibleInNavDandelionAccount() { + return getBool(R.string.pref_key__visibility_nav__dandelion_account, false); + } + + public boolean isVisibleInNavToggleMobileDesktop() { + return getBool(R.string.pref_key__visibility_nav__toggle_mobile_desktop, false); + } + + public boolean isTopbarStreamShortcutEnabled() { + return getBool(R.string.pref_key__topbar_stream_shortcut, false); + } + + public String getScreenRotation() { + return getString(R.string.pref_key__screen_rotation, R.string.rotation_val_system); + } + + public boolean isAppFirstStart() { + 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(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(R.string.pref_key__podprofile_last_stream_position, -1, _prefPod); + } + + public void setLastVisitedPositionInStream(long timestamp) { + setLong(R.string.pref_key__podprofile_last_stream_position, timestamp, _prefPod); + } + + public void setLanguage(String value) { + setString(R.string.pref_key__language, value); + } + + public String getLanguage() { + return getString(R.string.pref_key__language, ""); + } + + public void setPrimaryColorSettings(int base, int 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(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)) + }; + } + + @SuppressWarnings("ConstantConditions") + public int getPrimaryColor() { + if (isAmoledColorMode()) { + return Color.BLACK; + } else { + 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(R.string.pref_key__accent_color_base, base); + setInt(R.string.pref_key__accent_color_shade, shade); + } + + public int[] getAccentColorSettings() { + return new int[]{ + 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(R.string.pref_key__accent_color_shade, rcolor(R.color.accent)); + } + + public boolean isExtendedNotificationsActivated() { + return getBool(R.string.pref_key__extended_notifications, false); + } + + public boolean isAmoledColorMode() { + return getBool(R.string.pref_key__primary_color__amoled_mode, false); + } + + public boolean isAdBlockEnabled() { + return getBool(R.string.pref_key__adblock_enable, true); + } + + 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 000000000..d2132cc45 --- /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 new file mode 100644 index 000000000..a4bf65c60 --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/util/DiasporaUrlHelper.java @@ -0,0 +1,347 @@ +/* + 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.util; + +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.data.DiasporaPodList.DiasporaPod; + +/** + * Helper class that provides easy access to specific urls related to diaspora + * Created by vanitasvitae on 10.08.16. + */ +@SuppressWarnings({"unused", "SpellCheckingInspection", "SameParameterValue", "WeakerAccess"}) +public class DiasporaUrlHelper { + private final AppSettings settings; + + public static final String URL_BLANK = "about:blank"; + public static final String SUBURL_NOTIFICATIONS = "/notifications"; + public static final String SUBURL_POSTS = "/posts/"; + public static final String SUBURL_STREAM = "/stream"; + public static final String SUBURL_STREAM_WITH_TIMESTAMP = SUBURL_STREAM + "?max_time="; + public static final String SUBURL_CONVERSATIONS = "/conversations"; + public static final String SUBURL_NEW_POST = "/status_messages/new"; + public static final String SUBURL_PEOPLE = "/people/"; + public static final String SUBURL_ACTIVITY = "/activity"; + public static final String SUBURL_LIKED = "/liked"; + public static final String SUBURL_COMMENTED = "/commented"; + public static final String SUBURL_MENTIONS = "/mentions"; + public static final String SUBURL_PUBLIC = "/public"; + public static final String SUBURL_ASPECT = "/aspects?a_ids[]="; + public static final String SUBURL_TOGGLE_MOBILE = "/mobile/toggle"; + public static final String SUBURL_SEARCH_TAGS = "/tags/"; + public static final String SUBURL_SEARCH_PEOPLE = "/people.mobile?q="; + public static final String SUBURL_FOLOWED_TAGS = "/followed_tags"; + public static final String SUBURL_ASPECTS = "/aspects"; + public static final String SUBURL_STATISTICS = "/statistics"; + public static final String SUBURL_PERSONAL_SETTINGS = "/user/edit"; + public static final String SUBURL_MANAGE_TAGS = "/tag_followings/manage"; + public static final String SUBURL_SIGN_IN = "/users/sign_in"; + public static final String SUBURL_CONTACTS = "/contacts"; + public static final String SUBURL_REPORTS = "/reports"; + public static final String SUBURL_NOTIFICATIONS_ALSO_COMMENTED = "/notifications?type=also_commented"; + public static final String SUBURL_NOTIFICATIONS_COMMENT_ON_POST = "/notifications?type=comment_on_post"; + public static final String SUBURL_NOTIFICATIONS_LIKED = "/notifications?type=liked"; + 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 DiasporaUrlHelper(AppSettings settings) { + this.settings = settings; + } + + /** + * Return a url of the pod set in AppSettingsBase. + * Eg. https://pod.geraspora.de + * + * @return https://(pod-domain.tld) + */ + public String getPodUrl() { + DiasporaPod pod = settings.getPod(); + if (pod != null) { + return pod.getPodUrl().getBaseUrl(); + } + return "http://127.0.0.1"; + + } + + /** + * Return a url that points to the stream of the configured diaspora account + * + * @return https://(pod-domain.tld)/stream + */ + public String getStreamUrl() { + return getPodUrl() + SUBURL_STREAM; + } + + /** + * Return a url that points to the stream of the configured diaspora account on a timestamp + * + * @return https://(pod-domain.tld)/stream?max_time=1482057867 + */ + public String getStreamWithTimestampUrl(long timestamp) { + return getPodUrl() + SUBURL_STREAM_WITH_TIMESTAMP + timestamp; + } + + /** + * Return a url that points to the notifications feed of the configured diaspora account + * + * @return https://(pod-domain.tld)/notifications + */ + public String getNotificationsUrl() { + return getPodUrl() + SUBURL_NOTIFICATIONS; + } + + /** + * Returns a url that points to the post with the id postId + * + * @return https://(pod-domain.tld)/posts/(postId) + */ + public String getPostsUrl(long postId) { + return getPodUrl() + SUBURL_POSTS + postId; + } + + /** + * Return a url that points to the conversations overview of the registered diaspora account + * + * @return https://(pod-domain.tld)/conversations + */ + public String getConversationsUrl() { + return getPodUrl() + SUBURL_CONVERSATIONS; + } + + /** + * Return a url that points to the new-post form that lets the user create a new post + * + * @return https://(pod-domain.tld)/status_messages/new + */ + public String getNewPostUrl() { + return getPodUrl() + SUBURL_NEW_POST; + } + + /** + * Return a url that shows the profile of the currently registered diaspora account + * + * @return https://(pod-domain.tld)/people/(profileId) + */ + public String getProfileUrl() { + return getPodUrl() + SUBURL_PEOPLE + settings.getProfileId(); + } + + /** + * Return a url that shows the profile of the user with user id profileId + * + * @param profileId Id of the profile to be shown + * @return https://(pod-domain.tld)/people/(profileId) + */ + public String getProfileUrl(String profileId) { + return getPodUrl() + SUBURL_PEOPLE + profileId; + } + + /** + * Return a url that queries posts from the given aspect + * + * @param aspectId ID of the aspect + * @return https://(pod-domain.tld)//aspects?a_ids[]=aspectId + */ + public String getAspectUrl(String aspectId) { + return getPodUrl() + SUBURL_ASPECT + aspectId; + } + + /** + * Return a url that points to the activities feed of the currently registered diaspora account + * + * @return https://(pod-domain.tld)/activity + */ + public String getActivityUrl() { + return getPodUrl() + SUBURL_ACTIVITY; + } + + /** + * Return a url that points to the feed of posts that were liked by the currently registered diaspora account + * + * @return https://(pod-domain.tld)/liked + */ + public String getLikedPostsUrl() { + return getPodUrl() + SUBURL_LIKED; + } + + /** + * Return a url that points to the stream of posts that were commented by the currently registered diaspora account + * + * @return https://(pod-domain.tld)/commented + */ + public String getCommentedUrl() { + return getPodUrl() + SUBURL_COMMENTED; + } + + /** + * Return a url that points to the stream of posts in which the currently registered diaspora account has been mentioned in + * + * @return https://(pod-domain.tld)/mentions + */ + public String getMentionsUrl() { + return getPodUrl() + SUBURL_MENTIONS; + } + + /** + * Return a url that points to the stream of public posts + * + * @return https://(pod-domain.tld)/public + */ + public String getPublicUrl() { + return getPodUrl() + SUBURL_PUBLIC; + } + + /** + * Return a url that toggles between mobile and desktop view when opened + * + * @return https://(pod-domain.tld)/mobile/toggle + */ + public String getToggleMobileUrl() { + return getPodUrl() + SUBURL_TOGGLE_MOBILE; + } + + /** + * Return a url that queries posts for the given hashtag query + * + * @param query hashtag to be searched + * @return https://(pod-domain.tld)/tags/query + */ + public String getSearchTagsUrl(String query) { + return getPodUrl() + SUBURL_SEARCH_TAGS + query; + } + + /** + * Return a url that queries user accounts for query + * + * @param query search term + * @return https://(pod-domain.tld)/people.mobile?q=(query) + */ + public String getSearchPeopleUrl(String query) { + return getPodUrl() + SUBURL_SEARCH_PEOPLE + query; + } + + /** + * Return a url that points to the statistics page of the pod. + * + * @return https://(pod-domain.tld)/statistics + */ + public String getStatisticsUrl() { + return getPodUrl() + SUBURL_STATISTICS; + } + + /** + * Return an Url that points to the reports page of a the configured pod. + * Note: This url is only useful/visible for podmins and moderators. + * + * @return https://(pod-domain.tld)/reports + */ + public String getReportsUrl() { + return getPodUrl() + SUBURL_REPORTS; + } + + /** + * Return a url that points to the sign in page of the pod. + * + * @return https://(pod-domain.tld)/users/sign_in + */ + public String getSignInUrl() { + return getPodUrl() + SUBURL_SIGN_IN; + } + + /** + * Return a url that points to the personal settings page of the pod. + * + * @return https://(pod-domain.tld)/user/edit + */ + public String getPersonalSettingsUrl() { + return getPodUrl() + SUBURL_PERSONAL_SETTINGS; + } + + /** + * Return a url that points to the manage tags page of the pod. + * + * @return https://(pod-domain.tld)/tag_followings/manage + */ + public String getManageTagsUrl() { + return getPodUrl() + SUBURL_MANAGE_TAGS; + } + + /** + * Return a url that points to the manage tags page of the pod. + * + * @return https://(pod-domain.tld)/contacts + */ + public String getContactsUrl() { + return getPodUrl() + SUBURL_CONTACTS; + } + + public String getSuburlNotificationsAlsoCommentedUrl() { + return getPodUrl() + SUBURL_NOTIFICATIONS_ALSO_COMMENTED; + } + + public String getSuburlNotificationsCommentOnPostUrl() { + return getPodUrl() + SUBURL_NOTIFICATIONS_COMMENT_ON_POST; + } + + public String getSuburlNotificationsLikedUrl() { + return getPodUrl() + SUBURL_NOTIFICATIONS_LIKED; + } + + public String getSuburlNotificationsMentionedUrl() { + return getPodUrl() + SUBURL_NOTIFICATIONS_MENTIONED; + } + + public String getSuburlNotificationsResharedUrl() { + return getPodUrl() + SUBURL_NOTIFICATIONS_RESHARED; + } + + public String getSuburlNotificationsStartedSharingUrl() { + return getPodUrl() + SUBURL_NOTIFICATIONS_STARTED_SHARING; + } + + /** + * Returns the url of the blank WebView + * + * @return about:blank + */ + public String getBlankUrl() { + return URL_BLANK; + } + + public boolean isAspectUrl(String url) { + return url.startsWith(getPodUrl() + "/aspects?a_ids[]="); + } + + public String getAspectNameFromUrl(String url, App app) { + url = url.replace(getPodUrl() + "/aspects?a_ids[]=", "").split(",")[0]; + try { + int id = Integer.parseInt(url); + for (DiasporaAspect aspect : app.getDiasporaUserProfile().getAspects()) { + if (aspect.id == id) { + return aspect.name; + } + } + } catch (Exception ignored) { + } + return app.getString(R.string.aspects); + } +} 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 new file mode 100644 index 000000000..28db279bd --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/web/BrowserFragment.java @@ -0,0 +1,209 @@ +/* + 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.web; + +import android.content.Context; +import android.content.MutableContextWrapper; +import android.os.Bundle; +import android.view.View; +import android.view.ViewGroup; +import android.webkit.WebSettings; +import android.webkit.WebView; +import android.widget.ProgressBar; + +import com.github.dfa.diaspora_android.App; +import com.github.dfa.diaspora_android.R; +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; + +/** + * Fragment with a webView and a ProgressBar. + * This Fragment retains its instance. + * Created by vanitas on 26.09.16. + */ + +public class BrowserFragment extends ThemedFragment { + public static final String TAG = "com.github.dfa.diaspora_android.BrowserFragment"; + + protected ContextMenuWebView webView; + protected ProgressBar progressBar; + protected AppSettings appSettings; + protected CustomWebViewClient webViewClient; + protected WebSettings webSettings; + + protected String pendingUrl; + + @Override + protected int getLayoutResId() { + return R.layout.browser__fragment; + } + + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + AppLog.d(this, "onViewCreated()"); + super.onViewCreated(view, savedInstanceState); + + if (this.appSettings == null) { + this.appSettings = ((App) getActivity().getApplication()).getSettings(); + } + + if (this.webView == null) { + this.webView = view.findViewById(R.id.webView); + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + BrowserFragment.this.applyWebViewSettings(); + } + }); + + ProxyHandler.getInstance().addWebView(webView); + } + + if (this.progressBar == null) { + this.progressBar = view.findViewById(R.id.progressBar); + } + + if (pendingUrl != null) { + loadUrl(pendingUrl); + pendingUrl = null; + } + + webView.setParentActivity(getActivity()); + + this.setRetainInstance(true); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + + if (getRetainInstance() && getView() != null && getView().getParent() instanceof ViewGroup) { + ((ViewGroup) getView().getParent()).removeView(getView()); + } + } + + private void applyWebViewSettings() { + this.webSettings = webView.getSettings(); + webSettings.setAllowFileAccess(false); + webSettings.setUseWideViewPort(true); + webSettings.setLoadWithOverviewMode(true); + webSettings.setUserAgentString("Mozilla/5.0 (Linux; U; Android 4.4.4; Nexus 5 Build/KTU84P) AppleWebkit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30"); + webSettings.setDomStorageEnabled(true); + webSettings.setMinimumFontSize(appSettings.getMinimumFontSize()); + webSettings.setLoadsImagesAutomatically(appSettings.isLoadImages()); + webSettings.setAppCacheEnabled(true); + + if (android.os.Build.VERSION.SDK_INT >= 21) { + WebView.enableSlowWholeDocumentDraw(); + webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW); + } + + this.registerForContextMenu(webView); + //webView.setParentActivity(this); + webView.setOverScrollMode(WebView.OVER_SCROLL_ALWAYS); + + this.webViewClient = new CustomWebViewClient((App) getActivity().getApplication(), webView); + webView.setWebViewClient(webViewClient); + webView.setWebChromeClient(new ProgressBarWebChromeClient(webView, progressBar)); + } + + @Override + public void onResume() { + super.onResume(); + if (webView != null) { + webSettings.setMinimumFontSize(appSettings.getMinimumFontSize()); + webSettings.setLoadsImagesAutomatically(appSettings.isLoadImages()); + } + } + + @Override + public String getFragmentTag() { + return TAG; + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (webView != null && webView.getContext() instanceof MutableContextWrapper) { + ((MutableContextWrapper) webView.getContext()).setBaseContext(context); + } + } + + public boolean onBackPressed() { + if (webView.canGoBack()) { + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + webView.goBack(); + } + }); + return true; + } + return false; + } + + public void loadUrl(final String url) { + if (getWebView() != null) { + AppLog.v(this, "loadUrl(): load " + url); + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + getWebView().loadUrlNew(url); + } + }); + + } else { + AppLog.v(this, "loadUrl(): WebView null: Set pending url to " + url); + pendingUrl = url; + } + } + + public String getUrl() { + if (getWebView() != null) { + return getWebView().getUrl(); + } else { + return pendingUrl; + } + } + + public void reloadUrl() { + AppLog.v(this, "reloadUrl()"); + if (getWebView() != null) { + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + getWebView().reload(); + } + }); + + } + } + + public ContextMenuWebView getWebView() { + return this.webView; + } + + @Override + protected void applyColorToViews() { + ThemeHelper.updateProgressBarColor(progressBar); + } +} 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 new file mode 100644 index 000000000..6cb85c8f7 --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/web/ContextMenuWebView.java @@ -0,0 +1,187 @@ +/* + 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.web; + +import android.app.Activity; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +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.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 + * or open with another browser + */ +@SuppressWarnings("deprecation") +public class ContextMenuWebView extends NestedWebView { + + public static final int ID_SAVE_IMAGE = 10; + public static final int ID_IMAGE_EXTERNAL_BROWSER = 11; + public static final int ID_COPY_IMAGE_LINK = 15; + public static final int ID_COPY_LINK = 12; + public static final int ID_SHARE_LINK = 13; + public static final int ID_SHARE_IMAGE = 14; + + private final Context context; + private Activity parentActivity; + private String lastLoadUrl = ""; + + public ContextMenuWebView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + this.context = context; + } + + public ContextMenuWebView(Context context, AttributeSet attrs) { + super(context, attrs); + this.context = context; + } + + @Override + protected void onCreateContextMenu(ContextMenu menu) { + super.onCreateContextMenu(menu); + + HitTestResult result = getHitTestResult(); + + MenuItem.OnMenuItemClickListener handler = new MenuItem.OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + HitTestResult result = getHitTestResult(); + String url = result.getExtra(); + final ShareUtil shu = new ShareUtil(context).setFileProviderAuthority(BuildConfig.APPLICATION_ID); + final PermissionChecker permc = new PermissionChecker(parentActivity); + final AppSettings appSettings = new AppSettings(context); + + switch (item.getItemId()) { + //Save image to external memory + case ID_SAVE_IMAGE: { + if (permc.doIfExtStoragePermissionGranted(context.getString(R.string.permissions_image))) { + 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); + 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.share__toast_saved_image_to_location) + " " + dlfile.getName(), Toast.LENGTH_LONG).show(); + } + }).execute(url); + } + } + break; + } + + case ID_SHARE_IMAGE: { + if (permc.doIfExtStoragePermissionGranted(context.getString(R.string.permissions_image))) { + File fileSaveDirectory = appSettings.getAppSaveDirectory(); + if (permc.mkdirIfStoragePermissionGranted(fileSaveDirectory)) { + String filename = ".dandelion-shared" + url.substring(url.lastIndexOf(".")); + new DownloadTask(new File(fileSaveDirectory, filename), (ok, dlfile) -> { + if (ok) { + Toast.makeText(context, context.getText(R.string.share__toast_saved_image_to_location) + " " + dlfile.getName(), Toast.LENGTH_LONG).show(); + shu.shareStream(dlfile, "image/" + dlfile.getAbsolutePath().lastIndexOf(".") + 1); + } + }).execute(url); + } + } + break; + } + + case ID_IMAGE_EXTERNAL_BROWSER: + if (url != null) { + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + context.startActivity(intent); + } + break; + + //Copy url to clipboard + case ID_COPY_IMAGE_LINK: + case ID_COPY_LINK: + 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(); + } + break; + + //Try to share link to other apps + case ID_SHARE_LINK: + if (url != null) { + Intent sendIntent = new Intent(); + sendIntent.setAction(Intent.ACTION_SEND); + sendIntent.putExtra(Intent.EXTRA_TEXT, url); + sendIntent.setType("text/plain"); + context.startActivity(Intent.createChooser(sendIntent, getResources() + .getText(R.string.context_menu_share_link))); + } + break; + } + return true; + } + }; + + //Build context menu + if (result.getType() == HitTestResult.IMAGE_TYPE || + 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); + } else if (result.getType() == HitTestResult.ANCHOR_TYPE || + result.getType() == HitTestResult.SRC_ANCHOR_TYPE) { + // Menu options for a hyperlink. + menu.setHeaderTitle(result.getExtra()); + menu.add(0, ID_COPY_LINK, 0, context.getString(R.string.copy_link_to_clipboard)).setOnMenuItemClickListener(handler); + menu.add(0, ID_SHARE_LINK, 0, context.getString(R.string.context_menu_share_link)).setOnMenuItemClickListener(handler); + } + } + + public void loadUrlNew(String url) { + stopLoading(); + loadUrl(url); + } + + @Override + public void loadUrl(String url) { + super.loadUrl(url); + WebHelper.sendUpdateTitleByUrlIntent(url, getContext()); + } + + public void setParentActivity(Activity activity) { + this.parentActivity = activity; + } +} 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 new file mode 100644 index 000000000..c6af6f854 --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/web/CustomWebViewClient.java @@ -0,0 +1,98 @@ +/* + 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.web; + +import android.annotation.TargetApi; +import android.content.Intent; +import android.os.Build; +import android.support.v4.content.LocalBroadcastManager; +import android.webkit.CookieManager; +import android.webkit.WebResourceResponse; +import android.webkit.WebView; +import android.webkit.WebViewClient; + +import com.github.dfa.diaspora_android.App; +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 net.gsantner.opoc.util.AdBlock; + +public class CustomWebViewClient extends WebViewClient { + private final App app; + private String lastLoadUrl = ""; + private boolean isAdBlockEnabled = false; + + public CustomWebViewClient(App app, WebView webView) { + this.app = app; + isAdBlockEnabled = AppSettings.get().isAdBlockEnabled(); + } + + //Open non-diaspora links in customtab/external browser + public boolean shouldOverrideUrlLoading(WebView view, String url) { + DiasporaPodList.DiasporaPod pod = AppSettings.get().getPod(); + String host = null; + if (pod != null && pod.getPodUrl() != null && pod.getPodUrl().getHost() != null) { + host = pod.getPodUrl().getHost(); + } + + if (url.startsWith("https://dia.so/") + || (host != null && (url.startsWith("https://" + host) + || url.startsWith("http://" + host)))) { + return false; + } else { + Intent i = new Intent(MainActivity.ACTION_OPEN_EXTERNAL_URL); + i.putExtra(MainActivity.EXTRA_URL, url); + LocalBroadcastManager.getInstance(app.getApplicationContext()).sendBroadcast(i); + return true; + } + } + + public void onPageFinished(WebView view, String url) { + super.onPageFinished(view, url); + + final CookieManager cookieManager = app.getCookieManager(); + String cookies = cookieManager.getCookie(url); + DiasporaPodList.DiasporaPod pod = AppSettings.get().getPod(); + + if (cookies != null) { + cookieManager.setCookie(url, cookies); + if (pod != null && pod.getPodUrl() != null) { + cookieManager.setCookie(pod.getPodUrl().getBaseUrl(), cookies); + cookieManager.setCookie(".dia.so", "pod=" + pod.getPodUrl().getHost()); + } + for (String c : cookies.split(";")) { + //AppLog.d(this, "Cookie: " + c.split("=")[0] + " Value:" + c.split("=")[1]); + } + //new ProfileFetchTask(app).execute(); + } + } + + + @SuppressWarnings("deprecation") + @TargetApi(Build.VERSION_CODES.KITKAT_WATCH) + @Override + public WebResourceResponse shouldInterceptRequest(WebView view, String url) { + if (isAdBlockEnabled && AdBlock.getInstance().isAdHost(url)) { + return AdBlock.createEmptyResponse(); + } + return super.shouldInterceptRequest(view, 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 new file mode 100644 index 000000000..1dc21836c --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/web/DiasporaStreamWebChromeClient.java @@ -0,0 +1,82 @@ +/* + 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.web; + +import android.webkit.JsResult; +import android.webkit.WebView; +import android.widget.ProgressBar; + +import com.github.dfa.diaspora_android.R; +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; + +/** + * WebChromeClient that handles sharing text to diaspora* + * Created by vanitas on 26.09.16. + */ + +public class DiasporaStreamWebChromeClient extends FileUploadWebChromeClient { + protected SharedTextCallback sharedTextCallback; + + public DiasporaStreamWebChromeClient(WebView webView, ProgressBar progressBar, FileUploadCallback fileUploadCallback, SharedTextCallback callback) { + super(webView, progressBar, fileUploadCallback); + this.sharedTextCallback = callback; + } + + @Override + public void onProgressChanged(WebView wv, int progress) { + super.onProgressChanged(wv, progress); + WebHelper.optimizeMobileSiteLayout(wv); + WebHelper.sendUpdateTitleByUrlIntent(wv.getUrl(), wv.getContext()); + + if (progress > 0 && progress <= 85) { + WebHelper.getUserProfile(wv); + } + + if (progress > 60) { + String textToBeShared = sharedTextCallback.getSharedText(); + if (textToBeShared != null) { + AppLog.d(this, "Share text into webView"); + WebHelper.shareTextIntoWebView(wv, textToBeShared); + } + } + } + + @Override + public boolean onJsConfirm(WebView view, String url, String message, final JsResult result) { + ThemedAlertDialogBuilder builder = new ThemedAlertDialogBuilder(view.getContext(), AppSettings.get()); + builder.setTitle(view.getContext().getString(R.string.confirmation)) + .setMessage(message) + .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; + } + + public interface SharedTextCallback { + String getSharedText(); + + void setSharedText(String shared); + } +} diff --git a/app/src/main/java/com/github/dfa/diaspora_android/web/FileUploadWebChromeClient.java b/app/src/main/java/com/github/dfa/diaspora_android/web/FileUploadWebChromeClient.java new file mode 100644 index 000000000..8197d6ff0 --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/web/FileUploadWebChromeClient.java @@ -0,0 +1,55 @@ +/* + 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.web; + +import android.net.Uri; +import android.webkit.ValueCallback; +import android.webkit.WebView; +import android.widget.ProgressBar; + +/** + * WebChromeClient that allows uploading images + * Created by vanitas on 26.09.16. + */ + +public class FileUploadWebChromeClient extends ProgressBarWebChromeClient { + protected FileUploadCallback fileUploadCallback; + + public FileUploadWebChromeClient(WebView webView, ProgressBar progressBar, FileUploadCallback fileUploadCallback) { + super(webView, progressBar); + this.fileUploadCallback = fileUploadCallback; + } + + //For Android 4.1/4.2 only. DO NOT REMOVE! + @SuppressWarnings("unused") + protected void openFileChooser(ValueCallback uploadMsg, String acceptType, String capture) { + fileUploadCallback.legacyImageUpload(uploadMsg, acceptType, capture); + } + + @Override + public boolean onShowFileChooser(WebView webView, ValueCallback filePathCallback, FileChooserParams fileChooserParams) { + return fileUploadCallback.imageUpload(webView, filePathCallback, fileChooserParams); + } + + public interface FileUploadCallback { + boolean imageUpload(WebView webView, ValueCallback filePathCallback, FileChooserParams fileChooserParams); + + void legacyImageUpload(ValueCallback uploadMsg, String acceptType, String capture); + } +} diff --git a/app/src/main/java/com/github/dfa/diaspora_android/web/NestedWebView.java b/app/src/main/java/com/github/dfa/diaspora_android/web/NestedWebView.java new file mode 100644 index 000000000..2ee37f253 --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/web/NestedWebView.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2015 takahirom + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.dfa.diaspora_android.web; + +import android.content.Context; +import android.support.v4.view.MotionEventCompat; +import android.support.v4.view.NestedScrollingChild; +import android.support.v4.view.NestedScrollingChildHelper; +import android.support.v4.view.ViewCompat; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.webkit.WebView; + +public class NestedWebView extends WebView implements NestedScrollingChild { + private int mLastY; + private final int[] mScrollOffset = new int[2]; + private final int[] mScrollConsumed = new int[2]; + private int mNestedOffsetY; + private NestedScrollingChildHelper mChildHelper; + + public NestedWebView(Context context) { + this(context, null); + } + + public NestedWebView(Context context, AttributeSet attrs) { + this(context, attrs, android.R.attr.webViewStyle); + } + + public NestedWebView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + mChildHelper = new NestedScrollingChildHelper(this); + setNestedScrollingEnabled(true); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + boolean returnValue = false; + + MotionEvent event = MotionEvent.obtain(ev); + final int action = MotionEventCompat.getActionMasked(event); + if (action == MotionEvent.ACTION_DOWN) { + mNestedOffsetY = 0; + } + int eventY = (int) event.getY(); + event.offsetLocation(0, mNestedOffsetY); + switch (action) { + case MotionEvent.ACTION_MOVE: + int deltaY = mLastY - eventY; + // NestedPreScroll + if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) { + deltaY -= mScrollConsumed[1]; + mLastY = eventY - mScrollOffset[1]; + event.offsetLocation(0, -mScrollOffset[1]); + mNestedOffsetY += mScrollOffset[1]; + } + returnValue = super.onTouchEvent(event); + + // NestedScroll + if (dispatchNestedScroll(0, mScrollOffset[1], 0, deltaY, mScrollOffset)) { + event.offsetLocation(0, mScrollOffset[1]); + mNestedOffsetY += mScrollOffset[1]; + mLastY -= mScrollOffset[1]; + } + break; + case MotionEvent.ACTION_DOWN: + returnValue = super.onTouchEvent(event); + mLastY = eventY; + // start NestedScroll + startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL); + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + returnValue = super.onTouchEvent(event); + // end NestedScroll + stopNestedScroll(); + break; + } + if (event != null) { + event.recycle(); + } + return returnValue; + } + + // Nested Scroll implements + @Override + public void setNestedScrollingEnabled(boolean enabled) { + mChildHelper.setNestedScrollingEnabled(enabled); + } + + @Override + public boolean isNestedScrollingEnabled() { + return mChildHelper.isNestedScrollingEnabled(); + } + + @Override + public boolean startNestedScroll(int axes) { + return mChildHelper.startNestedScroll(axes); + } + + @Override + public void stopNestedScroll() { + mChildHelper.stopNestedScroll(); + } + + @Override + public boolean hasNestedScrollingParent() { + return mChildHelper.hasNestedScrollingParent(); + } + + @Override + public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, + int[] offsetInWindow) { + return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow); + } + + @Override + public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { + return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow); + } + + @Override + public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { + return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed); + } + + @Override + public boolean dispatchNestedPreFling(float velocityX, float velocityY) { + return mChildHelper.dispatchNestedPreFling(velocityX, velocityY); + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/github/dfa/diaspora_android/web/ProgressBarWebChromeClient.java b/app/src/main/java/com/github/dfa/diaspora_android/web/ProgressBarWebChromeClient.java new file mode 100644 index 000000000..f1f27931b --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/web/ProgressBarWebChromeClient.java @@ -0,0 +1,44 @@ +/* + 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.web; + +import android.view.View; +import android.webkit.WebChromeClient; +import android.webkit.WebView; +import android.widget.ProgressBar; + +/** + * WebChromeClient that connects the ProgressBar and the WebView and updates the progress of the progressBar. + * Created by vanitas on 26.09.16. + */ + +public class ProgressBarWebChromeClient extends WebChromeClient { + protected final ProgressBar progressBar; + protected final WebView webView; + + public ProgressBarWebChromeClient(WebView webView, ProgressBar progressBar) { + this.webView = webView; + this.progressBar = progressBar; + } + + public void onProgressChanged(WebView wv, int progress) { + progressBar.setProgress(progress); + progressBar.setVisibility(progress == 100 ? View.GONE : View.VISIBLE); + } +} diff --git a/app/src/main/java/com/github/dfa/diaspora_android/web/ProxyHandler.java b/app/src/main/java/com/github/dfa/diaspora_android/web/ProxyHandler.java new file mode 100644 index 000000000..3b1f8d7b8 --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/web/ProxyHandler.java @@ -0,0 +1,136 @@ +/* + 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.web; + +import android.content.Context; +import android.os.StrictMode; +import android.webkit.WebView; + +import com.github.dfa.diaspora_android.activity.MainActivity; +import com.github.dfa.diaspora_android.util.AppLog; +import com.github.dfa.diaspora_android.util.AppSettings; + +import java.util.ArrayList; + +import info.guardianproject.netcipher.NetCipher; +import info.guardianproject.netcipher.webkit.WebkitProxy; + +/** + * Proxy Handler that applies proxy settings of the app to webviews etc. + * Created by vanitas on 10.10.16. + */ + +public class ProxyHandler { + private static ProxyHandler instance; + private ArrayList webViews; + + + private ProxyHandler() { + /* Singleton, yo? */ + this.webViews = new ArrayList<>(); + } + + public static ProxyHandler getInstance() { + if (instance == null) { + instance = new ProxyHandler(); + } + return instance; + } + + public void updateProxySettings(Context context) { + AppLog.d(this, "UpdateProxySettings()"); + AppSettings appSettings = AppSettings.get(); + StrictMode.ThreadPolicy old = StrictMode.getThreadPolicy(); + StrictMode.ThreadPolicy tmp = new StrictMode.ThreadPolicy.Builder().permitAll().build(); + StrictMode.setThreadPolicy(tmp); + if (appSettings.isProxyHttpEnabled()) { + //Update NetCipher + NetCipher.setProxy(appSettings.getProxyHttpHost(), appSettings.getProxyHttpPort()); + //Update webviews + for (WebView wv : webViews) { + if (wv != null) { + try { + WebkitProxy.setProxy(MainActivity.class.getName(), context.getApplicationContext(), wv, appSettings.getProxyHttpHost(), appSettings.getProxyHttpPort()); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + } + StrictMode.setThreadPolicy(old); + } + + public void addWebView(WebView wv) { + AppLog.d(this, "AddWebView"); + if (wv != null && !webViews.contains(wv)) { + webViews.add(wv); + updateWebViewProxySettings(wv, wv.getContext()); + } + } + + private void updateWebViewProxySettings(WebView wv, Context context) { + AppLog.d(this, "UpdateWebViewProxySettings()"); + AppSettings appSettings = AppSettings.get(); + StrictMode.ThreadPolicy old = StrictMode.getThreadPolicy(); + StrictMode.ThreadPolicy tmp = new StrictMode.ThreadPolicy.Builder().permitAll().build(); + StrictMode.setThreadPolicy(tmp); + if (appSettings.isProxyHttpEnabled()) { + if (wv != null) { + try { + WebkitProxy.setProxy(MainActivity.class.getName(), context.getApplicationContext(), wv, appSettings.getProxyHttpHost(), appSettings.getProxyHttpPort()); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + StrictMode.setThreadPolicy(old); + } + + public static class ProxySettings { + private final boolean enabled; + private final String host; + private final int port; + + public ProxySettings(boolean enabled, String host, int port) { + this.enabled = enabled; + this.host = host; + this.port = port; + } + + public boolean isEnabled() { + return enabled; + } + + public String getHost() { + return host; + } + + public int getPort() { + return port; + } + + @Override + public boolean equals(Object other) { + return (other instanceof ProxySettings) && + enabled == ((ProxySettings) other).isEnabled() && + host.equals(((ProxySettings) other).getHost()) && + port == ((ProxySettings) other).getPort(); + } + } +} 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 new file mode 100644 index 000000000..44cf8ad30 --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/web/WebHelper.java @@ -0,0 +1,126 @@ +/* + 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.web; + +import android.content.Context; +import android.content.Intent; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.support.v4.content.LocalBroadcastManager; +import android.text.Html; +import android.webkit.URLUtil; +import android.webkit.WebView; + +import com.github.dfa.diaspora_android.activity.MainActivity; + +/** + * Created by Gregor Santner on 07.08.16. + * http://gsantner.net + */ +public class WebHelper { + + public static boolean isOnline(Context context) { + ConnectivityManager cnm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo ni = cnm.getActiveNetworkInfo(); + return ni != null && ni.isConnectedOrConnecting(); + } + + public static String replaceUrlWithMarkdown(String url) { + if (url != null && URLUtil.isHttpUrl(url) || URLUtil.isHttpsUrl(url)) { + return "<" + url + ">"; + } + return url; + } + + public static String escapeHtmlText(String text) { + text = Html.escapeHtml(text); + text = text.replace("\n", " "); + return text; + } + + public static void optimizeMobileSiteLayout(final WebView wv) { + wv.loadUrl("javascript: ( function() {" + + " if (document.documentElement == null || document.documentElement.style == null) { return; }" + + " document.documentElement.style.paddingBottom = '50px';" + + " document.getElementById('main').style.paddingTop = '5px';" + + " if(document.getElementById('main_nav')) {" + + " document.getElementById('main_nav').parentNode.removeChild(" + + " document.getElementById('main_nav'));" + + " } else if (document.getElementById('main-nav')) {" + + " document.getElementById('main-nav').parentNode.removeChild(" + + " document.getElementById('main-nav'));" + + " }" + + "})();"); + } + + public static void getUserProfile(final WebView wv) { + // aspects":[{"id":124934,"name":"Friends","selected":true},{"id":124937,"name":"Liked me","selected":false},{"id":124938,"name":"Follow","selected":false},{"id":128327,"name":"Nur ich","selected":false}] + wv.loadUrl("javascript: ( function() {" + + " if (typeof gon !== 'undefined' && typeof gon.user !== 'undefined') {" + + " var followed_tags = document.getElementById(\"followed_tags\");" + + " if(followed_tags != null) {" + + " try {" + + " var links = followed_tags.nextElementSibling.children[0].children;" + + " var tags = [];" + + " for(var i = 0; i < links.length - 1; i++) {" + // the last element is "Manage followed tags" link + " tags.push(links[i].innerText.replace('#',''));" + + " }" + + " gon.user[\"android_app.followed_tags\"] = tags;" + + " } catch(e) {}" + + " }" + + " var userProfile = JSON.stringify(gon.user);" + + " AndroidBridge.setUserProfile(userProfile.toString());" + + " } " + + "})();"); + } + + public static void shareTextIntoWebView(final WebView webView, String sharedText) { + sharedText = sharedText.replace("'", "'").replace("\"", """); + webView.loadUrl("javascript:(function() { " + + " document.documentElement.style.paddingBottom = '500px';" + + " if (typeof window.hasBeenSharedTo !== 'undefined') { AndroidBridge.contentHasBeenShared(); return; }" + + " var textbox = document.getElementsByTagName('textarea')[0];" + + " var textToBeShared = '" + sharedText + "';" + + " if (textbox) { " + + " textbox.style.height='210px'; " + + " textbox.innerHTML = textToBeShared; " + + " window.hasBeenSharedTo = true;" + + " window.lastShared = textToBeShared;" + + " }" + + "})();"); + } + + private static String lastUpdateTitleByUrl = ""; + + public static synchronized void sendUpdateTitleByUrlIntent(String url, Context context) { + // Ignore javascript stuff + if (url != null && url.startsWith("javascript:")) { + return; + } + + // Don't spam intents + if (lastUpdateTitleByUrl != null && !lastUpdateTitleByUrl.equals(url) && url != null) { + Intent updateActivityTitleIntent = new Intent(MainActivity.ACTION_UPDATE_TITLE_FROM_URL); + updateActivityTitleIntent.putExtra(MainActivity.EXTRA_URL, url); + LocalBroadcastManager.getInstance(context).sendBroadcast(updateActivityTitleIntent); + } + lastUpdateTitleByUrl = url; + } +} diff --git a/app/src/main/java/com/github/dfa/diaspora_android/web/custom_tab/BrowserFallback.java b/app/src/main/java/com/github/dfa/diaspora_android/web/custom_tab/BrowserFallback.java new file mode 100644 index 000000000..af10c11ae --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/web/custom_tab/BrowserFallback.java @@ -0,0 +1,36 @@ +/* + 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.web.custom_tab; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; + +/** + * Adapted from https://medium.com/ribot-labs/exploring-chrome-customs-tabs-on-android-ef427effe2f4 + */ + +public class BrowserFallback implements CustomTabActivityHelper.CustomTabFallback { + @Override + public void openUri(Activity activity, Uri uri) { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(uri); + activity.startActivity(intent); + } +} diff --git a/app/src/main/java/com/github/dfa/diaspora_android/web/custom_tab/CustomTabActivityHelper.java b/app/src/main/java/com/github/dfa/diaspora_android/web/custom_tab/CustomTabActivityHelper.java new file mode 100644 index 000000000..fce060624 --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/web/custom_tab/CustomTabActivityHelper.java @@ -0,0 +1,172 @@ +/* + 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.web.custom_tab; + +import android.app.Activity; +import android.content.ComponentName; +import android.net.Uri; +import android.os.Bundle; +import android.support.customtabs.CustomTabsClient; +import android.support.customtabs.CustomTabsIntent; +import android.support.customtabs.CustomTabsServiceConnection; +import android.support.customtabs.CustomTabsSession; + +import java.util.List; + +/** + * Adapted from https://medium.com/ribot-labs/exploring-chrome-customs-tabs-on-android-ef427effe2f4 + */ + +public class CustomTabActivityHelper { + private CustomTabsSession mCustomTabsSession; + private CustomTabsClient mClient; + private CustomTabsServiceConnection mConnection; + private ConnectionCallback mConnectionCallback; + + /** + * Opens the URL on a Custom Tab if possible. Otherwise fallsback to opening it on a WebView + * + * @param activity The host activity + * @param customTabsIntent a CustomTabsIntent to be used if Custom Tabs is available + * @param uri the Uri to be opened + * @param fallback a CustomTabFallback to be used if Custom Tabs is not available + */ + public static void openCustomTab(Activity activity, + CustomTabsIntent customTabsIntent, + Uri uri, + CustomTabFallback fallback) { + String packageName = CustomTabsHelper.getPackageNameToUse(activity); + + //If we cant find a package name, it means there's no browser that supports + //Chrome Custom Tabs installed. So, we fallback to the ui__webview + if (packageName == null) { + if (fallback != null) { + fallback.openUri(activity, uri); + } + } else { + customTabsIntent.intent.setPackage(packageName); + customTabsIntent.launchUrl(activity, uri); + } + } + + /** + * Unbinds the Activity from the Custom Tabs Service + * + * @param activity the activity that is connected to the service + */ + public void unbindCustomTabsService(Activity activity) { + if (mConnection == null) return; + activity.unbindService(mConnection); + mClient = null; + mCustomTabsSession = null; + } + + /** + * Creates or retrieves an exiting CustomTabsSession + * + * @return a CustomTabsSession + */ + public CustomTabsSession getSession() { + if (mClient == null) { + mCustomTabsSession = null; + } else if (mCustomTabsSession == null) { + mCustomTabsSession = mClient.newSession(null); + } + return mCustomTabsSession; + } + + /** + * Register a Callback to be called when connected or disconnected from the Custom Tabs Service + * + * @param connectionCallback callback + */ + public void setConnectionCallback(ConnectionCallback connectionCallback) { + this.mConnectionCallback = connectionCallback; + } + + /** + * Binds the Activity to the Custom Tabs Service + * + * @param activity the activity to be binded to the service + */ + public void bindCustomTabsService(Activity activity) { + if (mClient != null) return; + + String packageName = CustomTabsHelper.getPackageNameToUse(activity); + if (packageName == null) return; + mConnection = new CustomTabsServiceConnection() { + @Override + public void onCustomTabsServiceConnected(ComponentName name, CustomTabsClient client) { + mClient = client; + mClient.warmup(0L); + if (mConnectionCallback != null) mConnectionCallback.onCustomTabsConnected(); + //Initialize a session as soon as possible. + getSession(); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + mClient = null; + if (mConnectionCallback != null) mConnectionCallback.onCustomTabsDisconnected(); + } + }; + CustomTabsClient.bindCustomTabsService(activity, packageName, mConnection); + } + + public boolean mayLaunchUrl(Uri uri, Bundle extras, List otherLikelyBundles) { + if (mClient == null) return false; + + CustomTabsSession session = getSession(); + if (session == null) return false; + + return session.mayLaunchUrl(uri, extras, otherLikelyBundles); + } + + public boolean warmup(int flags) { + return mClient.warmup(flags); + } + + /** + * A Callback for when the service is connected or disconnected. Use those callbacks to + * handle UI changes when the service is connected or disconnected + */ + public interface ConnectionCallback { + /** + * Called when the service is connected + */ + void onCustomTabsConnected(); + + /** + * Called when the service is disconnected + */ + void onCustomTabsDisconnected(); + } + + /** + * To be used as a fallback to open the Uri when Custom Tabs is not available + */ + public interface CustomTabFallback { + /** + * @param activity The Activity that wants to open the Uri + * @param uri The uri to be opened by the fallback + */ + void openUri(Activity activity, Uri uri); + } + +} \ No newline at end of file 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 new file mode 100644 index 000000000..aaa89f06e --- /dev/null +++ b/app/src/main/java/com/github/dfa/diaspora_android/web/custom_tab/CustomTabsHelper.java @@ -0,0 +1,151 @@ +/* + 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.web.custom_tab; + +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.support.customtabs.CustomTabsService; +import android.text.TextUtils; + +import com.github.dfa.diaspora_android.util.AppLog; + +import java.util.ArrayList; +import java.util.List; + +/** + * Helper class for Custom Tabs. Adapted from https://medium.com/ribot-labs/exploring-chrome-customs-tabs-on-android-ef427effe2f4 + */ +public class CustomTabsHelper { + private static final String TAG = "CustomTabsHelper"; + static final String STABLE_PACKAGE = "com.android.chrome"; + 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"; + + private static String sPackageNameToUse; + + private CustomTabsHelper() { + } + + /** + * Goes through all apps that handle VIEW intents and have a warmup service. Picks + * the one chosen by the user if there is one, otherwise makes a best effort to return a + * valid package name. + *

+ * This is not threadsafe. + * + * @param context {@link Context} to use for accessing {@link PackageManager}. + * @return The package name recommended to use for connecting to custom tabs related components. + */ + public static String getPackageNameToUse(Context context) { + if (sPackageNameToUse != null) return sPackageNameToUse; + + PackageManager pm = context.getPackageManager(); + // Get default VIEW intent handler. + Intent activityIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.example.com")); + ResolveInfo defaultViewHandlerInfo = pm.resolveActivity(activityIntent, 0); + String defaultViewHandlerPackageName = null; + if (defaultViewHandlerInfo != null) { + defaultViewHandlerPackageName = defaultViewHandlerInfo.activityInfo.packageName; + } + + // Get all apps that can handle VIEW intents. + List resolvedActivityList = pm.queryIntentActivities(activityIntent, 0); + List packagesSupportingCustomTabs = new ArrayList<>(); + for (ResolveInfo info : resolvedActivityList) { + Intent serviceIntent = new Intent(); + serviceIntent.setAction(CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION); + serviceIntent.setPackage(info.activityInfo.packageName); + if (pm.resolveService(serviceIntent, 0) != null) { + packagesSupportingCustomTabs.add(info.activityInfo.packageName); + } + } + + // Now packagesSupportingCustomTabs contains all apps that can handle both VIEW intents + // and service calls. + if (packagesSupportingCustomTabs.isEmpty()) { + sPackageNameToUse = null; + } else if (packagesSupportingCustomTabs.size() == 1) { + sPackageNameToUse = packagesSupportingCustomTabs.get(0); + } else if (!TextUtils.isEmpty(defaultViewHandlerPackageName) + && !hasSpecializedHandlerIntents(context, activityIntent) + && packagesSupportingCustomTabs.contains(defaultViewHandlerPackageName)) { + sPackageNameToUse = defaultViewHandlerPackageName; + } else if (packagesSupportingCustomTabs.contains(STABLE_PACKAGE)) { + sPackageNameToUse = STABLE_PACKAGE; + } else if (packagesSupportingCustomTabs.contains(BETA_PACKAGE)) { + sPackageNameToUse = BETA_PACKAGE; + } else if (packagesSupportingCustomTabs.contains(DEV_PACKAGE)) { + 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; + } + + /** + * Used to check whether there is a specialized handler for a given intent. + * + * @param intent The intent to check with. + * @return Whether there is a specialized handler for the given intent. + */ + private static boolean hasSpecializedHandlerIntents(Context context, Intent intent) { + try { + PackageManager pm = context.getPackageManager(); + List handlers = pm.queryIntentActivities( + intent, + PackageManager.GET_RESOLVED_FILTER); + if (handlers == null || handlers.size() == 0) { + return false; + } + for (ResolveInfo resolveInfo : handlers) { + IntentFilter filter = resolveInfo.filter; + if (filter == null) continue; + if (filter.countDataAuthorities() == 0 || filter.countDataPaths() == 0) continue; + if (resolveInfo.activityInfo == null) continue; + return true; + } + } catch (RuntimeException e) { + AppLog.e(TAG, "Runtime exception while getting specialized handlers"); + } + return false; + } + + /** + * @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, CHROMIUM, FENNEC, KLAR}; + } +} diff --git a/app/src/main/java/de/baumann/diaspora/FloatingActionsMenuBehavior.java b/app/src/main/java/de/baumann/diaspora/FloatingActionsMenuBehavior.java deleted file mode 100644 index 1f36ccce5..000000000 --- a/app/src/main/java/de/baumann/diaspora/FloatingActionsMenuBehavior.java +++ /dev/null @@ -1,31 +0,0 @@ -package de.baumann.diaspora; - -/** - * Created by juergen on 29.02.16. - */ -import android.content.Context; -import android.support.design.widget.CoordinatorLayout; -import android.support.design.widget.Snackbar.SnackbarLayout; -import android.util.AttributeSet; -import android.view.View; - -import com.getbase.floatingactionbutton.FloatingActionButton; -import com.getbase.floatingactionbutton.FloatingActionsMenu; - -public class FloatingActionsMenuBehavior extends CoordinatorLayout.Behavior { - - public FloatingActionsMenuBehavior(Context context, AttributeSet attrs) { - } - - @Override - public boolean layoutDependsOn(CoordinatorLayout parent, FloatingActionsMenu child, View dependency) { - return dependency instanceof SnackbarLayout; - } - - @Override - public boolean onDependentViewChanged(CoordinatorLayout parent, FloatingActionsMenu child, View dependency) { - float translationY = Math.min(0, dependency.getTranslationY() - dependency.getHeight()); - child.setTranslationY(translationY); - return true; - } -} diff --git a/app/src/main/java/de/baumann/diaspora/MainActivity.java b/app/src/main/java/de/baumann/diaspora/MainActivity.java deleted file mode 100644 index cae3c8389..000000000 --- a/app/src/main/java/de/baumann/diaspora/MainActivity.java +++ /dev/null @@ -1,1149 +0,0 @@ -/* - This file is part of the Diaspora Native WebApp. - - Diaspora Native WebApp 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. - - Diaspora Native WebApp 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 Diaspora Native WebApp. - - If not, see . - */ - -package de.baumann.diaspora; - -import android.Manifest; -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.app.Activity; -import android.app.AlertDialog; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Picture; -import android.net.Uri; -import android.os.Bundle; -import android.os.Environment; -import android.os.Handler; -import android.provider.MediaStore; -import android.support.annotation.NonNull; -import android.support.design.widget.NavigationView; -import android.support.design.widget.Snackbar; -import android.support.v4.view.GravityCompat; -import android.support.v4.widget.DrawerLayout; -import android.support.v4.widget.SwipeRefreshLayout; -import android.support.v7.app.ActionBarDrawerToggle; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.Toolbar; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.webkit.JavascriptInterface; -import android.webkit.JsResult; -import android.webkit.ValueCallback; -import android.webkit.WebChromeClient; -import android.webkit.WebSettings; -import android.webkit.WebView; -import android.webkit.WebViewClient; -import android.widget.EditText; -import android.widget.ProgressBar; -import android.widget.RadioButton; -import android.widget.RadioGroup; -import android.widget.TextView; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.text.SimpleDateFormat; -import java.util.Date; - -import de.baumann.diaspora.utils.Helpers; -import de.baumann.diaspora.utils.PrefManager; - -public class MainActivity extends AppCompatActivity - implements NavigationView.OnNavigationItemSelectedListener { - - - private static final String URL_MESSAGE = "URL_MESSAGE"; - final Handler myHandler = new Handler(); - WebView webView; - static final String TAG = "Diaspora Main"; - String podDomain; - Menu menu; - int notificationCount = 0; - int conversationCount = 0; - ValueCallback mFilePathCallback; - String mCameraPhotoPath; - public static final int INPUT_FILE_REQUEST_CODE = 1; - com.getbase.floatingactionbutton.FloatingActionsMenu fab; - TextView txtTitle; - ProgressBar progressBar; - WebSettings wSettings; - PrefManager pm; - private SwipeRefreshLayout swipeView; - final private int REQUEST_CODE_ASK_PERMISSIONS = 123; - - @SuppressLint("SetJavaScriptEnabled") - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - if (android.os.Build.VERSION.SDK_INT >= 21) - WebView.enableSlowWholeDocumentDraw(); - - setContentView(R.layout.activity_main); - Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); - setSupportActionBar(toolbar); - - DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout); - ActionBarDrawerToggle toggle = new ActionBarDrawerToggle( - this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close); - drawer.setDrawerListener(toggle); - toggle.syncState(); - - NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view); - navigationView.setNavigationItemSelectedListener(this); - - progressBar = (ProgressBar)findViewById(R.id.progressBar); - pm = new PrefManager(MainActivity.this); - - SharedPreferences config = getSharedPreferences("PodSettings", MODE_PRIVATE); - podDomain = config.getString("podDomain", null); - - fab = (com.getbase.floatingactionbutton.FloatingActionsMenu) findViewById(R.id.multiple_actions); - fab.setVisibility(View.GONE); - - swipeView = (SwipeRefreshLayout) findViewById(R.id.swipe); - swipeView.setColorSchemeResources(R.color.colorPrimary, - R.color.fab_big); - - webView = (WebView)findViewById(R.id.webView); - webView.setScrollBarStyle(WebView.SCROLLBARS_OUTSIDE_OVERLAY); - webView.addJavascriptInterface(new JavaScriptInterface(), "NotificationCounter"); - - if (savedInstanceState != null) { - webView.restoreState(savedInstanceState); - } - - wSettings = webView.getSettings(); - wSettings.setJavaScriptEnabled(true); - wSettings.setUseWideViewPort(true); - wSettings.setLoadWithOverviewMode(true); - wSettings.setDomStorageEnabled(true); - wSettings.setMinimumFontSize(pm.getMinimumFontSize()); - wSettings.setLoadsImagesAutomatically(pm.getLoadImages()); - - if (android.os.Build.VERSION.SDK_INT >= 21) - wSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW); - - /* - * WebViewClient - */ - webView.setWebViewClient(new WebViewClient() { - public boolean shouldOverrideUrlLoading(WebView view, String url) { - if (!url.contains(podDomain)) { - Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); - startActivity(i); - return true; - } - return false; - } - - public void onPageFinished(WebView view, String url) { - swipeView.setRefreshing(false); - } - }); - - swipeView.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { - @Override - public void onRefresh() { - if (Helpers.isOnline(MainActivity.this)) { - webView.reload(); - } else { - Snackbar.make(swipeView, R.string.no_internet, Snackbar.LENGTH_LONG).show(); - swipeView.setRefreshing(false); - } - } - }); - - /* - * WebChromeClient - */ - webView.setWebChromeClient(new WebChromeClient() { - - public void onProgressChanged(WebView wv, int progress) { - progressBar.setProgress(progress); - - if (progress > 0 && progress <= 60) { - Helpers.getNotificationCount(wv); - } - - if (progress > 60) { - Helpers.hideTopBar(wv); - fab.setVisibility(View.VISIBLE); - } - - if (progress == 100) { - fab.collapse(); - progressBar.setVisibility(View.GONE); - } else { - progressBar.setVisibility(View.VISIBLE); - } - } - - @Override - public boolean onShowFileChooser(WebView webView, ValueCallback filePathCallback, FileChooserParams fileChooserParams) { - if (mFilePathCallback != null) mFilePathCallback.onReceiveValue(null); - - mFilePathCallback = filePathCallback; - - Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); - if (takePictureIntent.resolveActivity(getPackageManager()) != null) { - // Create the File where the photo should go - File photoFile = null; - try { - photoFile = createImageFile(); - takePictureIntent.putExtra("PhotoPath", mCameraPhotoPath); - } catch (IOException ex) { - // Error occurred while creating the File - Snackbar.make(swipeView, R.string.image, Snackbar.LENGTH_LONG).show(); - return false; - } - - // Continue only if the File was successfully created - if (photoFile != null) { - mCameraPhotoPath = "file:" + photoFile.getAbsolutePath(); - takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, - Uri.fromFile(photoFile)); - } else { - takePictureIntent = null; - } - } - - Intent contentSelectionIntent = new Intent(Intent.ACTION_GET_CONTENT); - contentSelectionIntent.addCategory(Intent.CATEGORY_OPENABLE); - contentSelectionIntent.setType("image/*"); - - Intent[] intentArray; - if (takePictureIntent != null) { - intentArray = new Intent[]{takePictureIntent}; - } else { - intentArray = new Intent[0]; - } - - Intent chooserIntent = new Intent(Intent.ACTION_CHOOSER); - chooserIntent.putExtra(Intent.EXTRA_INTENT, contentSelectionIntent); - chooserIntent.putExtra(Intent.EXTRA_TITLE, "Image Chooser"); - chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, intentArray); - - startActivityForResult(chooserIntent, INPUT_FILE_REQUEST_CODE); - - return true; - } - - public boolean onJsAlert(WebView view, String url, String message, JsResult result) { - return super.onJsAlert(view, url, message, result); - } - }); - - - if (savedInstanceState == null) { - if (Helpers.isOnline(MainActivity.this)) { - webView.loadData("", "text/html", null); - webView.loadUrl("https://"+podDomain); - } else { - Snackbar.make(swipeView, R.string.no_internet, Snackbar.LENGTH_LONG).show(); - } - } - - } - - /* - * Fab button events - */ - - public void fab1_click (View v){ - fab.collapse(); - if (Helpers.isOnline(MainActivity.this)) { - webView.loadUrl("https://" + podDomain + "/status_messages/new"); - setTitle(R.string.fab1_title); - } else { - Snackbar.make(swipeView, R.string.no_internet, Snackbar.LENGTH_LONG).show(); - } - } - - public void fab2_click(View v){ - fab.collapse(); - if (Helpers.isOnline(MainActivity.this)) { - final AlertDialog.Builder alert = new AlertDialog.Builder(this); - final EditText input = new EditText(this); - alert.setView(input); - alert.setTitle(R.string.search_alert_title); - alert.setPositiveButton(R.string.search_alert_people, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int whichButton) { - String inputTag = input.getText().toString().trim(); - String cleanTag = inputTag.replaceAll("\\*", ""); - // this validate the input data for tagfind - if (cleanTag == null || cleanTag.equals("")) { - dialog.cancel(); // if user don�t have added a tag - Snackbar.make(swipeView, R.string.search_alert_bypeople_validate_needsomedata, Snackbar.LENGTH_LONG).show(); - } else { // User have added a search tag - webView.loadUrl("https://" + podDomain + "/people.mobile?q=" + cleanTag); - setTitle(R.string.fab2_title_person); - } - } - }).setNegativeButton(R.string.search_alert_tag, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int whichButton) { - String inputTag = input.getText().toString().trim(); - String cleanTag = inputTag.replaceAll("\\#", ""); - // this validate the input data for tagfind - if (cleanTag == null || cleanTag.equals("")) { - dialog.cancel(); // if user hasn't added a tag - Snackbar.make(swipeView, R.string.search_alert_bytags_validate_needsomedata, Snackbar.LENGTH_LONG).show(); - } else { // User have added a search tag - webView.loadUrl("https://" + podDomain + "/tags/" + cleanTag); - setTitle(R.string.fab2_title_tag); - } - } - }); - alert.show(); - } - else { - Snackbar.make(swipeView, R.string.no_internet, Snackbar.LENGTH_LONG).show(); - } - } - - public void fab3_click(View v){ - fab.collapse(); - webView.scrollTo(0,0); - } - - private File createImageFile() throws IOException { - // Create an image file name - String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); - String imageFileName = "JPEG_" + timeStamp + "_"; - File storageDir = Environment.getExternalStoragePublicDirectory( - Environment.DIRECTORY_PICTURES); - return File.createTempFile( - imageFileName, /* prefix */ - ".jpg", /* suffix */ - storageDir /* directory */ - ); - } - - - @Override - public void onActivityResult (int requestCode, int resultCode, Intent data) { - if(requestCode != INPUT_FILE_REQUEST_CODE || mFilePathCallback == null) { - super.onActivityResult(requestCode, resultCode, data); - return; - } - Uri[] results = null; - if(resultCode == Activity.RESULT_OK) { - if(data == null) { - if(mCameraPhotoPath != null) { - results = new Uri[]{Uri.parse(mCameraPhotoPath)}; - } - } else { - String dataString = data.getDataString(); - if (dataString != null) { - results = new Uri[]{Uri.parse(dataString)}; - } - } - } - - mFilePathCallback.onReceiveValue(results); - mFilePathCallback = null; - } - - @Override - protected void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - webView.saveState(outState); - } - - @Override - protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { - super.onRestoreInstanceState(savedInstanceState); - webView.restoreState(savedInstanceState); - } - - @Override - protected void onResume() { - super.onResume(); - registerReceiver(brLoadUrl, new IntentFilter(URL_MESSAGE)); - } - - @Override - public void onBackPressed() { - fab.collapse(); - if (webView.canGoBack()) { - webView.goBack(); - setTitle(R.string.app_name); - Snackbar snackbar = Snackbar - .make(swipeView, R.string.confirm_exit, Snackbar.LENGTH_LONG) - .setAction(R.string.yes, new View.OnClickListener() { - @Override - public void onClick(View view) { - moveTaskToBack(true); - } - }); - snackbar.show(); - } else { - Snackbar snackbar = Snackbar - .make(swipeView, R.string.confirm_exit, Snackbar.LENGTH_LONG) - .setAction(R.string.yes, new View.OnClickListener() { - @Override - public void onClick(View view) { - moveTaskToBack(true); - } - }); - snackbar.show(); - } - } - - private BroadcastReceiver brLoadUrl = new BroadcastReceiver() { - - @Override - public void onReceive(Context context, Intent intent) { - String url = intent.getStringExtra("url"); - txtTitle.setText(R.string.app_name); - webView.loadUrl(url); - } - }; - - @Override - protected void onPause() { - unregisterReceiver(brLoadUrl); - super.onPause(); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.menu_main, menu); - return true; - } - - @Override - public boolean onPrepareOptionsMenu(Menu menu) { - this.menu = menu; - MenuItem itemNotification = menu.findItem(R.id.notifications); - if (itemNotification != null) { - if (notificationCount > 0) { - itemNotification.setIcon(R.drawable.ic_bell_ring_white_24dp); - } else { - itemNotification.setIcon(R.drawable.ic_bell_outline_white_24dp); - } - - MenuItem itemConversation = menu.findItem(R.id.conversations); - if (conversationCount > 0) { - itemConversation.setIcon(R.drawable.ic_message_text_white_24dp); - } else { - itemConversation.setIcon(R.drawable.ic_message_text_outline_white_24dp); - } - } - return super.onPrepareOptionsMenu(menu); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - int id = item.getItemId(); - - if (id == R.id.notifications) { - if (Helpers.isOnline(MainActivity.this)) { - webView.loadUrl("https://" + podDomain + "/notifications"); - setTitle(R.string.jb_notifications); - return true; - } else { - Snackbar.make(swipeView, R.string.no_internet, Snackbar.LENGTH_LONG).show(); - return false; - } - } - - if (id == R.id.conversations) { - if (Helpers.isOnline(MainActivity.this)) { - webView.loadUrl("https://" + podDomain + "/conversations"); - setTitle(R.string.jb_conversations); - return true; - } else { - Snackbar.make(swipeView, R.string.no_internet, Snackbar.LENGTH_LONG).show(); - return false; - } - } - - if (id == R.id.exit) { - Snackbar snackbar = Snackbar - .make(swipeView, R.string.confirm_exit, Snackbar.LENGTH_LONG) - .setAction(R.string.yes, new View.OnClickListener() { - @Override - public void onClick(View view) { - moveTaskToBack(true); - } - }); - snackbar.show(); - } - - if (id == R.id.help_license) { - final CharSequence[] options = { getString(R.string.help_license), getString(R.string.help_help), getString(R.string.help_donate) }; - new AlertDialog.Builder(MainActivity.this) - .setItems(options, new DialogInterface.OnClickListener() { - - @Override - - public void onClick(DialogInterface dialog, int item) { - - if (options[item].equals(getString(R.string.help_license))) - - { - new AlertDialog.Builder(MainActivity.this) - .setMessage(getString(R.string.about_text)) - .setPositiveButton(getString(R.string.about_yes), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/scoute-dich/Diaspora-Native-WebApp")); - startActivity(i); - dialog.cancel(); - } - }) - .setNegativeButton(getString(R.string.about_no), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - dialog.cancel(); - } - }).show(); - } - - if (options[item].equals(getString(R.string.help_help))) - - { - new AlertDialog.Builder(MainActivity.this) - .setMessage(getString(R.string.markdown_text)) - .setNegativeButton(getString(R.string.about_no), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - dialog.cancel(); - } - }).show(); - } - - if (options[item].equals(getString(R.string.help_donate))) - - { - new AlertDialog.Builder(MainActivity.this) - .setMessage(getString(R.string.donate_text)) - .setPositiveButton(getString(R.string.donate_1), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse("http://martinv.tip.me/")); - startActivity(i); - dialog.cancel(); - } - }) - .setNegativeButton(getString(R.string.about_no), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - dialog.cancel(); - } - }).show(); - } - - } - - }).show(); - } - - if (id == R.id.view) { - final CharSequence[] options = { getString(R.string.settings_font), getString(R.string.settings_view),getString(R.string.settings_image) }; - new AlertDialog.Builder(MainActivity.this) - .setItems(options, new DialogInterface.OnClickListener() { - - @Override - - public void onClick(DialogInterface dialog, int item) { - - if (options[item].equals(getString(R.string.settings_font))) - - { - if (Helpers.isOnline(MainActivity.this)) { - alertFormElements(); - } else { - Snackbar.make(swipeView, R.string.no_internet, Snackbar.LENGTH_LONG).show(); - } - } - - if (options[item].equals(getString(R.string.settings_view))) - - { - if (Helpers.isOnline(MainActivity.this)) { - webView.loadUrl("https://" + podDomain + "/mobile/toggle"); - } else { // No Internet connection - Snackbar.make(swipeView, R.string.no_internet, Snackbar.LENGTH_LONG).show(); - } - } - - if (options[item].equals(getString(R.string.settings_image))) - - { - if (Helpers.isOnline(MainActivity.this)) { - wSettings.setLoadsImagesAutomatically(!pm.getLoadImages()); - pm.setLoadImages(!pm.getLoadImages()); - webView.loadUrl(webView.getUrl()); - } else { - Snackbar.make(swipeView, R.string.no_internet, Snackbar.LENGTH_LONG).show(); - } - } - - } - - }).show(); - } - - if (id == R.id.share) { - final CharSequence[] options = { getString(R.string.share_link), getString(R.string.share_screenshot),getString(R.string.take_screenshot) }; - new AlertDialog.Builder(MainActivity.this) - .setItems(options, new DialogInterface.OnClickListener() { - - @Override - - public void onClick(DialogInterface dialog, int item) { - - if (options[item].equals(getString(R.string.share_link))) - - { - Intent sharingIntent = new Intent(Intent.ACTION_SEND); - sharingIntent.setType("image/png"); - sharingIntent.putExtra(Intent.EXTRA_SUBJECT, webView.getTitle()); - sharingIntent.putExtra(Intent.EXTRA_TEXT, webView.getUrl()); - startActivity(Intent.createChooser(sharingIntent, "Share using")); - } - - if (options[item].equals(getString(R.string.share_screenshot))) - - { - if (android.os.Build.VERSION.SDK_INT >= 23) { - int hasWRITE_EXTERNAL_STORAGE = checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE); - if (hasWRITE_EXTERNAL_STORAGE != PackageManager.PERMISSION_GRANTED) { - if (!shouldShowRequestPermissionRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { - new AlertDialog.Builder(MainActivity.this) - .setMessage(R.string.permissions) - .setPositiveButton(getString(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}, - REQUEST_CODE_ASK_PERMISSIONS); - } - }) - .setNegativeButton(getString(R.string.no), null) - .show(); - return; - } - requestPermissions(new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, - REQUEST_CODE_ASK_PERMISSIONS); - return; - } - } - - Snackbar.make(swipeView, R.string.toast_screenshot, Snackbar.LENGTH_LONG).show(); - File directory = new File(Environment.getExternalStorageDirectory() + "/Pictures/Diaspora/"); - if (!directory.exists()) { - directory.mkdirs(); - } - - Date date = new Date(); - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd_HH-mm"); - - Picture picture = webView.capturePicture(); - Bitmap b = Bitmap.createBitmap(picture.getWidth(), picture.getHeight(), Bitmap.Config.ARGB_8888); - Canvas c = new Canvas(b); - - File screen = new File(Environment.getExternalStorageDirectory() + "/Pictures/Diaspora/" - + dateFormat.format(date) + ".jpg"); - if (screen.exists()) - screen.delete(); - - picture.draw(c); - - FileOutputStream fos = null; - try { - - fos = new FileOutputStream(screen); - if (fos != null) { - b.compress(Bitmap.CompressFormat.JPEG, 90, fos); - - fos.close(); - } - } catch (Exception e) { - e.getMessage(); - - } - - Intent sharingIntent = new Intent(Intent.ACTION_SEND); - sharingIntent.setType("image/png"); - sharingIntent.putExtra(Intent.EXTRA_SUBJECT, webView.getTitle()); - sharingIntent.putExtra(Intent.EXTRA_TEXT, webView.getUrl()); - Uri bmpUri = Uri.fromFile(new File(Environment.getExternalStorageDirectory() + "/Pictures/Diaspora/" - + dateFormat.format(date) + ".jpg")); - sharingIntent.putExtra(Intent.EXTRA_STREAM, bmpUri); - startActivity(Intent.createChooser(sharingIntent, "Share using")); - - File file = new File(Environment.getExternalStorageDirectory() + "/Pictures/Diaspora/" - + dateFormat.format(date) + ".jpg"); - Uri uri = Uri.fromFile(file); - Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri); - sendBroadcast(intent); - } - - if (options[item].equals(getString(R.string.take_screenshot))) - - { - if (android.os.Build.VERSION.SDK_INT >= 23) { - int hasWRITE_EXTERNAL_STORAGE = checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE); - if (hasWRITE_EXTERNAL_STORAGE != PackageManager.PERMISSION_GRANTED) { - if (!shouldShowRequestPermissionRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { - new AlertDialog.Builder(MainActivity.this) - .setMessage(R.string.permissions) - .setPositiveButton(getString(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}, - REQUEST_CODE_ASK_PERMISSIONS); - } - }) - .setNegativeButton(getString(R.string.no), null) - .show(); - return; - } - requestPermissions(new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, - REQUEST_CODE_ASK_PERMISSIONS); - return; - } - } - - Snackbar.make(swipeView, R.string.toast_screenshot, Snackbar.LENGTH_LONG).show(); - - File directory = new File(Environment.getExternalStorageDirectory() + "/Pictures/Diaspora/"); - if (!directory.exists()) { - directory.mkdirs(); - } - - Date date = new Date(); - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd_HH-mm"); - - Picture picture = webView.capturePicture(); - Bitmap b = Bitmap.createBitmap(picture.getWidth(), picture.getHeight(), Bitmap.Config.ARGB_8888); - Canvas c = new Canvas(b); - - File screen = new File(Environment.getExternalStorageDirectory() + "/Pictures/Diaspora/" - + dateFormat.format(date) + ".jpg"); - if (screen.exists()) - screen.delete(); - - picture.draw(c); - - FileOutputStream fos = null; - try { - - fos = new FileOutputStream(screen); - if (fos != null) { - b.compress(Bitmap.CompressFormat.JPEG, 90, fos); - - fos.close(); - } - } catch (Exception e) { - e.getMessage(); - - } - - File file = new File(Environment.getExternalStorageDirectory() + "/Pictures/Diaspora/" - + dateFormat.format(date) + ".jpg"); - Uri uri = Uri.fromFile(file); - Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri); - sendBroadcast(intent); - } - - } - - }).show(); - } - return super.onOptionsItemSelected(item); - } - - - - public void alertFormElements() { - - /* - * Inflate the XML view. activity_main is in - * res/layout/form_elements.xml - */ - LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); - final View formElementsView = inflater.inflate(R.layout.font_size_chooser, - null, false); - - final RadioGroup rgFontSize = (RadioGroup) formElementsView - .findViewById(R.id.genderRadioGroup); - - // the alert dialog - new AlertDialog.Builder(MainActivity.this).setView(formElementsView) - .setTitle("Set Font Size") - .setNegativeButton("OK", new DialogInterface.OnClickListener() { - @TargetApi(11) - public void onClick(DialogInterface dialog, int id) { - - int selectedId = rgFontSize - .getCheckedRadioButtonId(); - - // find the radiobutton by returned id - RadioButton selectedRadioButton = (RadioButton) formElementsView - .findViewById(selectedId); - - if (selectedRadioButton.getId() == R.id.radNormal) { - pm.setMinimumFontSize(8); - } else if (selectedRadioButton.getId() == R.id.radLarge) { - pm.setMinimumFontSize(16); - } else if (selectedRadioButton.getId() == R.id.radLarger) { - pm.setMinimumFontSize(20); - } - - wSettings.setMinimumFontSize(pm.getMinimumFontSize()); - - if (Helpers.isOnline(MainActivity.this)) { - webView.loadUrl(webView.getUrl()); - } else { - Snackbar.make(swipeView, R.string.no_internet, Snackbar.LENGTH_LONG).show(); - } - dialog.cancel(); - } - }).show(); - } - - public class JavaScriptInterface { - @JavascriptInterface - public void setNotificationCount(final String webMessage){ - myHandler.post(new Runnable() { - @Override - public void run() { - notificationCount = Integer.valueOf(webMessage); - - MenuItem item = menu.findItem(R.id.notifications); - - if (item != null) { - if (notificationCount > 0) { - item.setIcon(R.drawable.ic_bell_ring_white_24dp); - } else { - item.setIcon(R.drawable.ic_bell_outline_white_24dp); - } - } - - - } - }); - } - - @JavascriptInterface - public void setConversationCount(final String webMessage){ - myHandler.post(new Runnable() { - @Override - public void run() { - conversationCount = Integer.valueOf(webMessage); - - MenuItem item = menu.findItem(R.id.conversations); - - if (item != null) { - if (conversationCount > 0) { - item.setIcon(R.drawable.ic_message_text_white_24dp); - } else { - item.setIcon(R.drawable.ic_message_text_outline_white_24dp); - } - } - - } - }); - } - - } - - @SuppressWarnings("StatementWithEmptyBody") - @Override - public boolean onNavigationItemSelected(MenuItem item) { - // Handle navigation view item clicks here. - int id = item.getItemId(); - - if (id == R.id.jb_stream) { - if (Helpers.isOnline(MainActivity.this)) { - webView.loadUrl("https://" + podDomain + "/stream"); - setTitle(R.string.jb_stream); - } else { - Snackbar.make(swipeView, R.string.no_internet, Snackbar.LENGTH_LONG).show(); - } - - } else if (id == R.id.jb_followed_tags) { - if (Helpers.isOnline(MainActivity.this)) { - webView.loadUrl("https://" + podDomain + "/followed_tags"); - setTitle(R.string.jb_followed_tags); - } else { - Snackbar.make(swipeView, R.string.no_internet, Snackbar.LENGTH_LONG).show(); - } - - } else if (id == R.id.jb_aspects) { - if (Helpers.isOnline(MainActivity.this)) { - webView.loadUrl("https://" + podDomain + "/aspects"); - setTitle(R.string.jb_aspects); - } else { - Snackbar.make(swipeView, R.string.no_internet, Snackbar.LENGTH_LONG).show(); - } - - } else if (id == R.id.jb_activities) { - if (Helpers.isOnline(MainActivity.this)) { - webView.loadUrl("https://" + podDomain + "/activity"); - setTitle(R.string.jb_activities); - } else { - Snackbar.make(swipeView, R.string.no_internet, Snackbar.LENGTH_LONG).show(); - } - - } else if (id == R.id.jb_liked) { - if (Helpers.isOnline(MainActivity.this)) { - webView.loadUrl("https://" + podDomain + "/liked"); - setTitle(R.string.jb_liked); - } else { - Snackbar.make(swipeView, R.string.no_internet, Snackbar.LENGTH_LONG).show(); - } - - } else if (id == R.id.jb_commented) { - if (Helpers.isOnline(MainActivity.this)) { - webView.loadUrl("https://" + podDomain + "/commented"); - setTitle(R.string.jb_commented); - } else { - Snackbar.make(swipeView, R.string.no_internet, Snackbar.LENGTH_LONG).show(); - } - - } else if (id == R.id.jb_mentions) { - if (Helpers.isOnline(MainActivity.this)) { - webView.loadUrl("https://" + podDomain + "/mentions"); - setTitle(R.string.jb_mentions); - } else { - Snackbar.make(swipeView, R.string.no_internet, Snackbar.LENGTH_LONG).show(); - } - - } else if (id == R.id.jb_public) { - if (Helpers.isOnline(MainActivity.this)) { - webView.loadUrl("https://" + podDomain + "/public"); - setTitle(R.string.jb_public); - } else { - Snackbar.make(swipeView, R.string.no_internet, Snackbar.LENGTH_LONG).show(); - } - - } else if (id == R.id.jb_settings_view) { - final CharSequence[] options = { getString(R.string.settings_font), getString(R.string.settings_view),getString(R.string.settings_image) }; - new AlertDialog.Builder(MainActivity.this) - .setItems(options, new DialogInterface.OnClickListener() { - - @Override - - public void onClick(DialogInterface dialog, int item) { - - if (options[item].equals(getString(R.string.settings_font))) - - { - if (Helpers.isOnline(MainActivity.this)) { - alertFormElements(); - } else { - Snackbar.make(swipeView, R.string.no_internet, Snackbar.LENGTH_LONG).show(); - } - } - - if (options[item].equals(getString(R.string.settings_view))) - - { - if (Helpers.isOnline(MainActivity.this)) { - webView.loadUrl("https://" + podDomain + "/mobile/toggle"); - } else { // No Internet connection - Snackbar.make(swipeView, R.string.no_internet, Snackbar.LENGTH_LONG).show(); - } - } - - if (options[item].equals(getString(R.string.settings_image))) - - { - if (Helpers.isOnline(MainActivity.this)) { - wSettings.setLoadsImagesAutomatically(!pm.getLoadImages()); - pm.setLoadImages(!pm.getLoadImages()); - webView.loadUrl(webView.getUrl()); - } else { - Snackbar.make(swipeView, R.string.no_internet, Snackbar.LENGTH_LONG).show(); - } - } - - } - - }).show(); - - } else if (id == R.id.jb_settings_diaspora) { - final CharSequence[] options2 = { getString(R.string.jb_settings), getString(R.string.jb_manage_tags), - getString(R.string.jb_contacts), getString(R.string.jb_pod) }; - new AlertDialog.Builder(MainActivity.this) - .setItems(options2, new DialogInterface.OnClickListener() { - - @Override - - public void onClick(DialogInterface dialog, int item) { - - if (options2[item].equals(getString(R.string.jb_settings))) - - { - setTitle(R.string.jb_settings); - if (Helpers.isOnline(MainActivity.this)) { - webView.loadUrl("https://" + podDomain + "/user/edit"); - } else { // No Internet connection - Snackbar.make(swipeView, R.string.no_internet, Snackbar.LENGTH_LONG).show(); - } - } - - if (options2[item].equals(getString(R.string.jb_manage_tags))) - - { - setTitle(R.string.jb_manage_tags); - if (Helpers.isOnline(MainActivity.this)) { - webView.loadUrl("https://" + podDomain + "/tag_followings/manage"); - } else { // No Internet connection - Snackbar.make(swipeView, R.string.no_internet, Snackbar.LENGTH_LONG).show(); - } - } - - if (options2[item].equals(getString(R.string.jb_contacts))) - - { - setTitle(R.string.jb_contacts); - if (Helpers.isOnline(MainActivity.this)) { - webView.loadUrl("https://" + podDomain + "/contacts"); - } else { // No Internet connection - Snackbar.make(swipeView, R.string.no_internet, Snackbar.LENGTH_LONG).show(); - } - } - - if (options2[item].equals(getString(R.string.jb_pod))) - - { - if (Helpers.isOnline(MainActivity.this)) { - new AlertDialog.Builder(MainActivity.this) - .setTitle(getString(R.string.confirmation)) - .setMessage(getString(R.string.change_pod_warning)) - .setPositiveButton(getString(R.string.yes), - new DialogInterface.OnClickListener() { - @TargetApi(11) - public void onClick(DialogInterface dialog, int id) { - webView.clearCache(true); - dialog.cancel(); - Intent i = new Intent(MainActivity.this, PodsActivity.class); - startActivity(i); - finish(); - } - }) - .setNegativeButton(getString(R.string.no), new DialogInterface.OnClickListener() { - @TargetApi(11) - public void onClick(DialogInterface dialog, int id) { - dialog.cancel(); - } - }).show(); - } else { - Snackbar.make(swipeView, R.string.no_internet, Snackbar.LENGTH_LONG).show(); - } - } - - } - - }).show(); - - } else if (id == R.id.jb_license_help) { - final CharSequence[] options = { getString(R.string.help_license), getString(R.string.help_help), getString(R.string.help_donate) }; - new AlertDialog.Builder(MainActivity.this) - .setItems(options, new DialogInterface.OnClickListener() { - - @Override - - public void onClick(DialogInterface dialog, int item) { - - if (options[item].equals(getString(R.string.help_license))) - - { - new AlertDialog.Builder(MainActivity.this) - .setTitle(R.string.about_title) - .setMessage(getString(R.string.about_text)) - .setPositiveButton(getString(R.string.about_yes), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/scoute-dich/Diaspora-Native-WebApp")); - startActivity(i); - dialog.cancel(); - } - }) - .setNegativeButton(getString(R.string.about_no), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - dialog.cancel(); - } - }).show(); - } - - if (options[item].equals(getString(R.string.help_help))) - - { - new AlertDialog.Builder(MainActivity.this) - .setMessage(getString(R.string.markdown_text)) - .setNegativeButton(getString(R.string.about_no), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - dialog.cancel(); - } - }).show(); - } - - if (options[item].equals(getString(R.string.help_donate))) - - { - new AlertDialog.Builder(MainActivity.this) - .setMessage(getString(R.string.donate_text)) - .setPositiveButton(getString(R.string.donate_1), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse("http://martinv.tip.me/")); - startActivity(i); - dialog.cancel(); - } - }) - .setNegativeButton(getString(R.string.about_no), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - dialog.cancel(); - } - }).show(); - } - - } - - }).show(); - } - - DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout); - drawer.closeDrawer(GravityCompat.START); - return true; - } - -} \ No newline at end of file diff --git a/app/src/main/java/de/baumann/diaspora/PodsActivity.java b/app/src/main/java/de/baumann/diaspora/PodsActivity.java deleted file mode 100644 index 15340cffc..000000000 --- a/app/src/main/java/de/baumann/diaspora/PodsActivity.java +++ /dev/null @@ -1,262 +0,0 @@ -/* - This file is part of the Diaspora Native WebApp. - - Diaspora Native WebApp 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. - - Diaspora Native WebApp 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 Diaspora Native WebApp. - - If not, see . - */ - -package de.baumann.diaspora; - -import android.annotation.TargetApi; -import android.app.AlertDialog; -import android.app.ProgressDialog; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.SharedPreferences; -import android.os.Build; -import android.os.Bundle; -import android.support.design.widget.Snackbar; -import android.support.v7.app.ActionBarActivity; -import android.text.Editable; -import android.text.TextWatcher; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.webkit.CookieManager; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.EditText; -import android.widget.ImageView; -import android.widget.ListView; -import android.widget.TextView; - -import java.util.ArrayList; -import java.util.Collections; - -import de.baumann.diaspora.services.GetPodsService; -import de.baumann.diaspora.utils.Helpers; - - -public class PodsActivity extends ActionBarActivity { - - BroadcastReceiver podListReceiver; - EditText filter; - ListView lv; - ImageView imgSelectPod; - ProgressDialog progressDialog; - private static final String TAG = "Diaspora Pods"; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_pods); - - filter = (EditText) findViewById(R.id.edtFilter); - lv = (ListView) findViewById(R.id.lstPods); - lv.setTextFilterEnabled(true); - - imgSelectPod = (ImageView) findViewById(R.id.imgSelectPod); - imgSelectPod.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - if (filter.getText().length() > 4 && filter.getText().toString().contains(".")) - askConfirmation(filter.getText().toString()); - else - Snackbar.make(lv, R.string.valid_pod, Snackbar.LENGTH_LONG).show(); - } - }); - - podListReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (intent.hasExtra("pods")) { - Bundle extras = intent.getExtras(); - String[] pods = extras.getStringArray("pods"); - - if (progressDialog != null) - progressDialog.dismiss(); - - if (pods != null && pods.length>0) - updateListview(pods); - else { - Snackbar.make(lv, R.string.podlist_error, Snackbar.LENGTH_LONG).show(); - } - } else { - // List of pods empty - } - } - }; - - registerReceiver(podListReceiver, new IntentFilter(GetPodsService.MESSAGE)); - - progressDialog = new ProgressDialog(PodsActivity.this); - progressDialog.setCancelable(false); - progressDialog.setIndeterminate(true); - progressDialog.setMessage(getString(R.string.loading_podlist)); - - if (Helpers.isOnline(PodsActivity.this)) { - progressDialog.show(); - } else { - Snackbar.make(lv, R.string.no_internet, Snackbar.LENGTH_LONG).show(); - } - - } - - @Override - protected void onResume() { - super.onResume(); - Intent i= new Intent(PodsActivity.this, GetPodsService.class); - startService(i); - } - - - private void updateListview(String[] source) { - final ArrayList podList = new ArrayList<>(); - - for (int i = 0 ; i < source.length ; i++) { - podList.add(source[i].toLowerCase()); - } - Collections.sort(podList); - - final ArrayAdapter adapter = new ArrayAdapter( - PodsActivity.this, - android.R.layout.simple_list_item_1, - podList); - lv.setAdapter(adapter); - lv.setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - askConfirmation(((TextView) view).getText().toString()); - } - }); - - adapter.getFilter().filter(filter.getText()); - filter.addTextChangedListener(new TextWatcher() { - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - (adapter).getFilter().filter(s.toString()); - } - - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - } - - @Override - public void afterTextChanged(Editable s) { - } - }); - - } - - public void askConfirmation(final String podDomain) { - if (Helpers.isOnline(PodsActivity.this)) { - new AlertDialog.Builder(PodsActivity.this) - .setTitle(getString(R.string.confirmation)) - .setMessage(getString(R.string.confirm_pod)+podDomain+"?") - .setPositiveButton("YES", - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - - SharedPreferences sp = getSharedPreferences("PodSettings", MODE_PRIVATE); - SharedPreferences.Editor editor = sp.edit(); - editor.putString("podDomain", podDomain); - editor.apply(); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - try { - CookieManager.getInstance().removeAllCookies(null); - CookieManager.getInstance().removeSessionCookies(null); - } catch (Exception e) { - e.printStackTrace(); - } - } else { - try { - CookieManager.getInstance().removeAllCookie(); - CookieManager.getInstance().removeSessionCookie(); - } catch (Exception e) { - e.printStackTrace(); - } - } - - Intent i = new Intent(PodsActivity.this, MainActivity.class); - dialog.cancel(); - startActivity(i); - finish(); - } - }) - .setNegativeButton("NO", new DialogInterface.OnClickListener() { - @TargetApi(11) - public void onClick(DialogInterface dialog, int id) { - dialog.cancel(); - } - }).show(); - - } else { - Snackbar.make(lv, R.string.no_internet, Snackbar.LENGTH_LONG).show(); - } - } - - - @Override - public void onBackPressed() { - Snackbar snackbar = Snackbar - .make(lv, R.string.confirm_exit, Snackbar.LENGTH_LONG) - .setAction(R.string.yes, new View.OnClickListener() { - @Override - public void onClick(View view) { - moveTaskToBack(true); - } - }); - snackbar.show(); - } - - @Override - protected void onDestroy() { - unregisterReceiver(podListReceiver); - super.onDestroy(); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.menu_pods, menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - int id = item.getItemId(); - - if (id == R.id.reload) { - if (Helpers.isOnline(PodsActivity.this)) { - progressDialog.show(); - Intent i= new Intent(PodsActivity.this, GetPodsService.class); - startService(i); - return true; - } else { - Snackbar.make(lv, R.string.no_internet, Snackbar.LENGTH_LONG).show(); - return false; - } - } - - return super.onOptionsItemSelected(item); - } - - -} - - diff --git a/app/src/main/java/de/baumann/diaspora/ShareActivity.java b/app/src/main/java/de/baumann/diaspora/ShareActivity.java deleted file mode 100644 index 553f2502d..000000000 --- a/app/src/main/java/de/baumann/diaspora/ShareActivity.java +++ /dev/null @@ -1,307 +0,0 @@ -/* - This file is part of the Diaspora Native WebApp. - - Diaspora Native WebApp 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. - - Diaspora Native WebApp 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 Diaspora Native WebApp. - - If not, see . - */ - -package de.baumann.diaspora; - -import android.annotation.SuppressLint; -import android.app.AlertDialog; -import android.content.Intent; -import android.content.SharedPreferences; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.Environment; -import android.provider.MediaStore; -import android.support.design.widget.Snackbar; -import android.util.Log; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.webkit.JsResult; -import android.webkit.ValueCallback; -import android.webkit.WebChromeClient; -import android.webkit.WebSettings; -import android.webkit.WebView; -import android.webkit.WebViewClient; -import android.widget.ProgressBar; -import android.widget.TextView; - -import java.io.File; -import java.io.IOException; -import java.text.SimpleDateFormat; -import java.util.Date; - -import de.baumann.diaspora.utils.Helpers; - - -public class ShareActivity extends MainActivity { - - private WebView webView; - private static final String TAG = "Diaspora Share"; - private String podDomain; - private ValueCallback mFilePathCallback; - private String mCameraPhotoPath; - private com.getbase.floatingactionbutton.FloatingActionsMenu fab; - private TextView txtTitle; - private ProgressBar progressBar; - - @SuppressLint("SetJavaScriptEnabled") - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - - progressBar = (ProgressBar)findViewById(R.id.progressBar); - - txtTitle.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - if (Helpers.isOnline(ShareActivity.this)) { - txtTitle.setText(R.string.jb_stream); - Intent i = new Intent(ShareActivity.this, MainActivity.class); - i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(i); - finish(); - } else { - Snackbar.make(v, R.string.no_internet, Snackbar.LENGTH_SHORT).show(); - } - } - }); - - - SharedPreferences config = getSharedPreferences("PodSettings", MODE_PRIVATE); - podDomain = config.getString("podDomain", null); - - fab = (com.getbase.floatingactionbutton.FloatingActionsMenu) findViewById(R.id.multiple_actions); - fab.setVisibility(View.GONE); - - webView = (WebView)findViewById(R.id.webView); - webView.setScrollBarStyle(WebView.SCROLLBARS_OUTSIDE_OVERLAY); - - WebSettings wSettings = webView.getSettings(); - wSettings.setJavaScriptEnabled(true); - wSettings.setBuiltInZoomControls(true); - - if (Build.VERSION.SDK_INT >= 21) - wSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW); - - /* - * WebViewClient - */ - webView.setWebViewClient(new WebViewClient() { - public boolean shouldOverrideUrlLoading(WebView view, String url) { - Log.d(TAG, url); - if (!url.contains(podDomain)) { - Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); - startActivity(i); - return true; - } - return false; - - } - - public void onPageFinished(WebView view, String url) { - Log.i(TAG, "Finished loading URL: " + url); - } - - public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { - Log.e(TAG, "Error: " + description); - - new AlertDialog.Builder(ShareActivity.this) - .setIcon(android.R.drawable.ic_dialog_alert) - .setMessage(description) - .setPositiveButton("CLOSE", null) - .show(); - } - }); - - - /* - * WebChromeClient - */ - webView.setWebChromeClient(new WebChromeClient() { - - public void onProgressChanged(WebView wv, int progress) { - progressBar.setProgress(progress); - - if (progress > 0 && progress <= 60) { - Helpers.getNotificationCount(wv); - } - - if (progress > 60) { - Helpers.hideTopBar(wv); - } - - if (progress == 100) { - progressBar.setVisibility(View.GONE); - } else { - progressBar.setVisibility(View.VISIBLE); - } - } - - @Override - public boolean onShowFileChooser(WebView webView, ValueCallback filePathCallback, FileChooserParams fileChooserParams) { - if (mFilePathCallback != null) mFilePathCallback.onReceiveValue(null); - - mFilePathCallback = filePathCallback; - - Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); - if (takePictureIntent.resolveActivity(getPackageManager()) != null) { - // Create the File where the photo should go - File photoFile = null; - try { - photoFile = createImageFile(); - takePictureIntent.putExtra("PhotoPath", mCameraPhotoPath); - } catch (IOException ex) { - // Error occurred while creating the File - Snackbar.make(getWindow().findViewById(R.id.drawer_layout), "Unable to get image", Snackbar.LENGTH_SHORT).show(); - } - - // Continue only if the File was successfully created - if (photoFile != null) { - mCameraPhotoPath = "file:" + photoFile.getAbsolutePath(); - takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, - Uri.fromFile(photoFile)); - } else { - takePictureIntent = null; - } - } - - Intent contentSelectionIntent = new Intent(Intent.ACTION_GET_CONTENT); - contentSelectionIntent.addCategory(Intent.CATEGORY_OPENABLE); - contentSelectionIntent.setType("image/*"); - - Intent[] intentArray; - if (takePictureIntent != null) { - intentArray = new Intent[]{takePictureIntent}; - } else { - intentArray = new Intent[0]; - } - - Intent chooserIntent = new Intent(Intent.ACTION_CHOOSER); - chooserIntent.putExtra(Intent.EXTRA_INTENT, contentSelectionIntent); - chooserIntent.putExtra(Intent.EXTRA_TITLE, "Image Chooser"); - chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, intentArray); - - startActivityForResult(chooserIntent, INPUT_FILE_REQUEST_CODE); - - return true; - } - - public boolean onJsAlert(WebView view, String url, String message, JsResult result) { - return super.onJsAlert(view, url, message, result); - } - }); - - - - - Intent intent = getIntent(); - final Bundle extras = intent.getExtras(); - String action = intent.getAction(); - - if (Intent.ACTION_SEND.equals(action)) { - webView.setWebViewClient(new WebViewClient() { - - public void onPageFinished(WebView view, String url) { - - if (extras.containsKey(Intent.EXTRA_TEXT) && extras.containsKey(Intent.EXTRA_SUBJECT)) { - final String extraText = (String) extras.get(Intent.EXTRA_TEXT); - final String extraSubject = (String) extras.get(Intent.EXTRA_SUBJECT); - - webView.setWebViewClient(new WebViewClient() { - @Override - public boolean shouldOverrideUrlLoading(WebView view, String url) { - - finish(); - - Snackbar.make(webView, R.string.please_reload, Snackbar.LENGTH_LONG).show(); - - Intent i = new Intent(ShareActivity.this, MainActivity.class); - i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(i); - - return false; - } - }); - - webView.loadUrl("javascript:(function() { " + - "document.getElementsByTagName('textarea')[0].style.height='110px'; " + - "document.getElementsByTagName('textarea')[0].innerHTML = '[" + extraSubject + "](" + extraText + ") #ViaDiasporaNativeWebApp'; " + - " if(document.getElementById(\"main_nav\")) {" + - " document.getElementById(\"main_nav\").parentNode.removeChild(" + - " document.getElementById(\"main_nav\"));" + - " } else if (document.getElementById(\"main-nav\")) {" + - " document.getElementById(\"main-nav\").parentNode.removeChild(" + - " document.getElementById(\"main-nav\"));" + - " }" + - "})();"); - - } - } - }); - } - - if (savedInstanceState == null) { - if (Helpers.isOnline(ShareActivity.this)) { - webView.loadUrl("https://"+podDomain+"/status_messages/new"); - } else { - Snackbar.make(getWindow().findViewById(R.id.drawer_layout), R.string.no_internet, Snackbar.LENGTH_SHORT).show(); - } - } - - } - - private File createImageFile() throws IOException { - String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); - String imageFileName = "JPEG_" + timeStamp + "_"; - File storageDir = Environment.getExternalStoragePublicDirectory( - Environment.DIRECTORY_PICTURES); - return File.createTempFile( - imageFileName, /* prefix */ - ".jpg", /* suffix */ - storageDir /* directory */ - ); - } - - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.menu_compose, menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - int id = item.getItemId(); - - if (id == R.id.reload) { - if (Helpers.isOnline(ShareActivity.this)) { - webView.reload(); - return true; - } else { - Snackbar.make(getWindow().findViewById(R.id.drawer_layout), R.string.no_internet, Snackbar.LENGTH_SHORT).show(); - return false; - } - } - - return super.onOptionsItemSelected(item); - } - -} diff --git a/app/src/main/java/de/baumann/diaspora/SplashActivity.java b/app/src/main/java/de/baumann/diaspora/SplashActivity.java deleted file mode 100644 index 7bb03d096..000000000 --- a/app/src/main/java/de/baumann/diaspora/SplashActivity.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - This file is part of the Diaspora Native WebApp. - - Diaspora Native WebApp 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. - - Diaspora Native WebApp 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 Diaspora Native WebApp. - - If not, see . - */ - -package de.baumann.diaspora; - -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.res.TypedArray; -import android.os.Bundle; -import android.support.v7.app.AppCompatActivity; -import android.view.WindowManager; -import android.widget.ImageView; - -import java.util.Timer; -import java.util.TimerTask; - - -public class SplashActivity extends AppCompatActivity { - - ImageView imgSplash; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, - WindowManager.LayoutParams.FLAG_FULLSCREEN); - setContentView(R.layout.activity_splash); - - imgSplash = (ImageView) findViewById(R.id.imgSplash); - - TypedArray images = getResources().obtainTypedArray(R.array.splash_images); - int choice = (int) (Math.random() * images.length()); - imgSplash.setImageResource(images.getResourceId(choice, R.drawable.splashscreen1)); - images.recycle(); - - final SharedPreferences config = getSharedPreferences("PodSettings", MODE_PRIVATE); - - Timer timer = new Timer(); - timer.schedule(new TimerTask() { - @Override - public void run() { - Intent i; - if (config.getString("podDomain", null) != null) { - i = new Intent(SplashActivity.this, MainActivity.class); - } else { - i = new Intent(SplashActivity.this, PodsActivity.class); - } - startActivity(i); - finish(); - } - }, 2000); - - } - -} diff --git a/app/src/main/java/de/baumann/diaspora/services/GetPodsService.java b/app/src/main/java/de/baumann/diaspora/services/GetPodsService.java deleted file mode 100644 index 6a0ab86c0..000000000 --- a/app/src/main/java/de/baumann/diaspora/services/GetPodsService.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - This file is part of the Diaspora Native WebApp. - - Diaspora Native WebApp 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. - - Diaspora Native WebApp 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 Diaspora Native WebApp. - - If not, see . - */ - -package de.baumann.diaspora.services; - -import android.app.Service; -import android.content.Intent; -import android.os.AsyncTask; -import android.os.IBinder; -import android.util.Log; - -import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; -import org.apache.http.StatusLine; -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.impl.client.DefaultHttpClient; -import org.json.JSONArray; -import org.json.JSONObject; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.util.ArrayList; -import java.util.List; - -public class GetPodsService extends Service { - public static final String MESSAGE = "de.baumann.diaspora.podsreceived"; - - private static final String TAG = "Diaspora Pod Service"; - - public GetPodsService() { } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - getPods(); - return super.onStartCommand(intent, flags, startId); - } - - private void getPods() { - /* - * Most of the code in this AsyncTask is from the file getPodlistTask.java - * from the app "Diaspora Webclient". - * A few modifications and adaptations were made by me. - * Source: - * https://github.com/voidcode/Diaspora-Webclient/blob/master/src/com/voidcode/diasporawebclient/getPodlistTask.java - * Thanks to Terkel Sørensen - */ - AsyncTask getPodsAsync = new AsyncTask() { - @Override - protected String[] doInBackground(Void... params) { - - // TODO: Update deprecated code - - StringBuilder builder = new StringBuilder(); - HttpClient client = new DefaultHttpClient(); - List list = null; - try { - HttpGet httpGet = new HttpGet("http://podupti.me/api.php?key=4r45tg&format=json"); - HttpResponse response = client.execute(httpGet); - StatusLine statusLine = response.getStatusLine(); - int statusCode = statusLine.getStatusCode(); - if (statusCode == 200) { - HttpEntity entity = response.getEntity(); - InputStream content = entity.getContent(); - BufferedReader reader = new BufferedReader( - new InputStreamReader(content)); - String line; - while ((line = reader.readLine()) != null) { - builder.append(line); - } - } else { - //TODO Notify User about failure - Log.e(TAG, "Failed to download list of pods"); - } - } catch (IOException e) { - //TODO handle json buggy feed - e.printStackTrace(); - } - //Parse the JSON Data - try { - JSONObject j = new JSONObject(builder.toString()); - JSONArray jr = j.getJSONArray("pods"); - Log.d(TAG, "Number of entries " + jr.length()); - list = new ArrayList(); - for (int i = 0; i < jr.length(); i++) { - JSONObject jo = jr.getJSONObject(i); - Log.d(TAG, jo.getString("domain")); - String secure = jo.getString("secure"); - if (secure.equals("true")) - list.add(jo.getString("domain")); - } - - } catch (Exception e) { - //TODO Handle Parsing errors here - e.printStackTrace(); - } - if (list != null) - return list.toArray(new String[list.size()]); - else - return null; - } - - @Override - protected void onPostExecute(String[] strings) { - Intent broadcastIntent = new Intent(MESSAGE); - if (strings != null) - broadcastIntent.putExtra("pods", strings); - sendBroadcast(broadcastIntent); - stopSelf(); - } - }; - getPodsAsync.execute(); - } - - @Override - public IBinder onBind(Intent intent) { - // TODO: Return the communication channel to the service. - throw new UnsupportedOperationException("Not yet implemented"); - } - -} diff --git a/app/src/main/java/de/baumann/diaspora/utils/Helpers.java b/app/src/main/java/de/baumann/diaspora/utils/Helpers.java deleted file mode 100644 index 489cefc32..000000000 --- a/app/src/main/java/de/baumann/diaspora/utils/Helpers.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - This file is part of the Diaspora Native WebApp. - - Diaspora Native WebApp 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. - - Diaspora Native WebApp 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 Diaspora Native WebApp. - - If not, see . - */ - -package de.baumann.diaspora.utils; - - -import android.content.Context; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.webkit.WebView; - -public class Helpers { - - public static boolean isOnline(Context context){ - ConnectivityManager cnm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo ni = cnm.getActiveNetworkInfo(); - return ni != null && ni.isConnectedOrConnecting(); - } - - public static void hideTopBar(WebView wv) { - wv.loadUrl("javascript: ( function() {" + - " if(document.getElementById('main_nav')) {" + - " document.getElementById('main_nav').parentNode.removeChild(" + - " document.getElementById('main_nav'));" + - " } else if (document.getElementById('main-nav')) {" + - " document.getElementById('main-nav').parentNode.removeChild(" + - " document.getElementById('main-nav'));" + - " }" + - "})();"); - } - - public static void getNotificationCount(WebView wv) { - wv.loadUrl("javascript: ( function() {" + - " if (document.getElementById('notification')) {" + - " var count = document.getElementById('notification').innerHTML;" + - " NotificationCounter.setNotificationCount(count.replace(/(\\r\\n|\\n|\\r)/gm, \"\"));" + - " } else {" + - " NotificationCounter.setNotificationCount('0');" + - " }" + - " if (document.getElementById('conversation')) {" + - " var count = document.getElementById('conversation').innerHTML;" + - " NotificationCounter.setConversationCount(count.replace(/(\\r\\n|\\n|\\r)/gm, \"\"));" + - " } else {" + - " NotificationCounter.setConversationCount('0');" + - " }" + - "})();"); - } -} diff --git a/app/src/main/java/de/baumann/diaspora/utils/PrefManager.java b/app/src/main/java/de/baumann/diaspora/utils/PrefManager.java deleted file mode 100644 index 9cceb3e38..000000000 --- a/app/src/main/java/de/baumann/diaspora/utils/PrefManager.java +++ /dev/null @@ -1,52 +0,0 @@ -package de.baumann.diaspora.utils; - -import android.content.Context; -import android.content.SharedPreferences; -import android.content.SharedPreferences.Editor; -import android.preference.PreferenceManager; - -public class PrefManager { - - private final Context context; - private boolean loadImages = true; - private int minimumFontSize = 0; - - public PrefManager(Context ctx) { - SharedPreferences sp = null; - this.context = ctx; - try { - sp = PreferenceManager.getDefaultSharedPreferences(context); - } catch (Exception e) { - e.printStackTrace(); - } - - if (sp != null) { - loadImages = sp.getBoolean("loadImages", true); - minimumFontSize = sp.getInt("minimumFontSize", 8); - } - } - - public boolean getLoadImages() { - return loadImages; - } - - public void setLoadImages(boolean loadImages) { - this.loadImages = loadImages; - SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); - Editor edit = sp.edit(); - edit.putBoolean("loadImages", loadImages); - edit.commit(); - } - - public int getMinimumFontSize() { - return minimumFontSize; - } - - public void setMinimumFontSize(int minimumFontSize) { - this.minimumFontSize = minimumFontSize; - SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); - Editor edit = sp.edit(); - edit.putInt("minimumFontSize", minimumFontSize); - edit.commit(); - } -} 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 000000000..93019ce93 --- /dev/null +++ b/app/src/main/java/net/gsantner/opoc/activity/GsFragmentBase.java @@ -0,0 +1,129 @@ +/*####################################################### + * + * Maintained by Gregor Santner, 2017- + * https://gsantner.net/ + * + * 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.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +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; + + @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; + 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(); + } + } + } +} 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 000000000..adaafa3b1 --- /dev/null +++ b/app/src/main/java/net/gsantner/opoc/format/markdown/SimpleMarkdownParser.java @@ -0,0 +1,228 @@ +/*####################################################### + * + * Maintained by Gregor Santner, 2016- + * https://gsantner.net/ + * + * 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)^([-*] )(.*)$", " $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) + ; + return text; + } + }; + + public final static SmpFilter FILTER_CHANGELOG = new SmpFilter() { + @Override + public String filter(String text) { + text = text + .replace("New:", "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; + } + }; + + //######################## + //## 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 000000000..c27d9bce9 --- /dev/null +++ b/app/src/main/java/net/gsantner/opoc/preference/PropertyBackend.java @@ -0,0 +1,49 @@ +/*####################################################### + * + * Maintained by Gregor Santner, 2018- + * https://gsantner.net/ + * + * 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 000000000..275d88a59 --- /dev/null +++ b/app/src/main/java/net/gsantner/opoc/preference/SharedPreferencesPropertyBackend.java @@ -0,0 +1,499 @@ +/*####################################################### + * + * Maintained by Gregor Santner, 2016- + * https://gsantner.net/ + * + * 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.util.ArrayList; +import java.util.Arrays; +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"; + + // + // 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); + } + + // + // 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 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) { + return gp(pref).getString(key, defaultValue); + } + + public String getString(@StringRes int keyResourceId, String defaultValue, @StringRes int keyResourceIdDefaultValue, final SharedPreferences... pref) { + return gp(pref).getString(rstr(keyResourceId), rstr(keyResourceIdDefaultValue)); + } + + 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 = pref + .getString(key, ARRAY_SEPARATOR) + .replace(ARRAY_SEPARATOR_SUBSTITUTE, ARRAY_SEPARATOR); + if (value.equals(ARRAY_SEPARATOR)) { + 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 gp(pref).getInt(rstr(keyResourceId), defaultValue); + } + + public int getInt(String key, int defaultValue, final SharedPreferences... pref) { + return gp(pref).getInt(key, 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 = pref.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 gp(pref).getLong(rstr(keyResourceId), defaultValue); + } + + public long getLong(String key, long defaultValue, final SharedPreferences... pref) { + return gp(pref).getLong(key, 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 gp(pref).getFloat(rstr(keyResourceId), defaultValue); + } + + public float getFloat(String key, float defaultValue, final SharedPreferences... pref) { + return gp(pref).getFloat(key, 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 gp(pref).getBoolean(rstr(keyResourceId), defaultValue); + } + + public boolean getBool(String key, boolean defaultValue, final SharedPreferences... pref) { + return gp(pref).getBoolean(key, defaultValue); + } + + // + // Getter & Setter for Color + // + public int getColor(String key, @ColorRes int defaultColor, final SharedPreferences... pref) { + return gp(pref).getInt(key, rcolor(defaultColor)); + } + + public int getColor(@StringRes int keyResourceId, @ColorRes int defaultColor, final SharedPreferences... pref) { + return gp(pref).getInt(rstr(keyResourceId), rcolor(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; + } +} 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 000000000..76284a11f --- /dev/null +++ b/app/src/main/java/net/gsantner/opoc/preference/nonsupport/LanguagePreference.java @@ -0,0 +1,188 @@ +/*####################################################### + * + * Maintained by Gregor Santner, 2017- + * https://gsantner.net/ + * + * 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/util/ActivityUtils.java b/app/src/main/java/net/gsantner/opoc/util/ActivityUtils.java new file mode 100644 index 000000000..53a86cc1d --- /dev/null +++ b/app/src/main/java/net/gsantner/opoc/util/ActivityUtils.java @@ -0,0 +1,166 @@ +/*####################################################### + * + * Maintained by Gregor Santner, 2016- + * https://gsantner.net/ + * + * License: Apache 2.0 + * https://github.com/gsantner/opoc/#licensing + * https://www.apache.org/licenses/LICENSE-2.0 + * +#########################################################*/ +package net.gsantner.opoc.util; + +import android.app.Activity; +import android.content.ActivityNotFoundException; +import android.content.DialogInterface; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +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.LinkMovementMethod; +import android.util.TypedValue; +import android.view.View; +import android.view.WindowManager; +import android.view.inputmethod.InputMethodManager; +import android.webkit.WebView; + + +@SuppressWarnings({"WeakerAccess", "unused", "SameParameterValue", "SpellCheckingInspection"}) +public class ActivityUtils extends net.gsantner.opoc.util.ContextUtils { + //######################## + //## Members, Constructors + //######################## + protected Activity _activity; + + public ActivityUtils(final Activity activity) { + super(activity); + _activity = 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 + * 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 void showSnackBar(@StringRes int stringResId, boolean showLong) { + Snackbar.make(_activity.findViewById(android.R.id.content), stringResId, + showLong ? Snackbar.LENGTH_LONG : Snackbar.LENGTH_SHORT).show(); + } + + public 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 void hideSoftKeyboard() { + InputMethodManager imm = (InputMethodManager) _activity.getSystemService(Activity.INPUT_METHOD_SERVICE); + if (imm != null && _activity.getCurrentFocus() != null && _activity.getCurrentFocus().getWindowToken() != null) { + imm.hideSoftInputFromWindow(_activity.getCurrentFocus().getWindowToken(), 0); + } + } + + public void showSoftKeyboard() { + InputMethodManager imm = (InputMethodManager) _activity.getSystemService(Activity.INPUT_METHOD_SERVICE); + if (imm != null && _activity.getCurrentFocus() != null && _activity.getCurrentFocus().getWindowToken() != null) { + imm.showSoftInput(_activity.getCurrentFocus(), InputMethodManager.SHOW_FORCED); + } + } + + public 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) { + AppCompatTextView textView = new AppCompatTextView(_context); + int padding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16, + _context.getResources().getDisplayMetrics()); + textView.setMovementMethod(new LinkMovementMethod()); + textView.setPadding(padding, 0, padding, 0); + + textView.setText(isHtml ? new SpannableString(Html.fromHtml(text)) : text); + AlertDialog.Builder dialog = new AlertDialog.Builder(_context) + .setPositiveButton(android.R.string.ok, null) + .setOnDismissListener(dismissedListener) + .setTitle(resTitleId) + .setView(textView); + dialog.show(); + } + + 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(); + } + + // Toggle with no param, else set visibility according to first bool + public void 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); + } + + public void 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("http://play.google.com/store/apps/" + pkgId))); + } + } +} diff --git a/app/src/main/java/net/gsantner/opoc/util/AdBlock.java b/app/src/main/java/net/gsantner/opoc/util/AdBlock.java new file mode 100644 index 000000000..304a4b852 --- /dev/null +++ b/app/src/main/java/net/gsantner/opoc/util/AdBlock.java @@ -0,0 +1,175 @@ +/*####################################################### + * + * Maintained by Gregor Santner, 2017- + * https://gsantner.net/ + * + * 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 + * Always blocks both, www and non www + + * Load hosts (call e.g. via Application-Object) +AdBlock.getInstance().loadHostsFromRawAssetsAsync(context); + + * Override inside a WebViewClient: +public WebResourceResponse shouldInterceptRequest(WebView view, String url) { + return AdBlock.getInstance().isAdHost(url) + ? AdBlock.createEmptyResponse() + : super.shouldInterceptRequest(view, url); +} +*/ +package net.gsantner.opoc.util; + +import android.content.Context; +import android.util.Log; +import android.webkit.WebResourceResponse; + +import com.github.dfa.diaspora_android.R; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.lang.reflect.Field; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Simple Host-Based AdBlocker + */ +@SuppressWarnings({"WeakerAccess", "SpellCheckingInspection", "unused"}) +public class AdBlock { + private static final AdBlock instance = new AdBlock(); + + public static AdBlock getInstance() { + return instance; + } + + //######################## + //## + //## Members + //## + //######################## + private final Set _adblockHostsFromRaw = new HashSet<>(); + private final Set _adblockHosts = new HashSet<>(); + private boolean _isLoaded; + + //######################## + //## + //## Methods + //## + //######################## + private AdBlock() { + } + + public boolean isAdHost(String urlS) { + if (urlS != null && !urlS.isEmpty() && urlS.startsWith("http")) { + try { + URI url = new URI(urlS); + String host = url.getHost().trim(); + if (host.startsWith("www.") && host.length() >= 4) { + host = host.substring(4); + } + return _adblockHosts.contains(host) || _adblockHosts.contains("www." + host); + } catch (URISyntaxException e) { + e.printStackTrace(); + } + + } + return false; + } + + public AdBlock reset() { + _adblockHosts.clear(); + _adblockHosts.addAll(_adblockHostsFromRaw); + 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) { + for (String host : hosts) { + if (host != null) { + host = host.trim(); + if (host.startsWith("www.") && host.length() >= 4) { + host = host.substring(4); + } + if (!host.startsWith("#") && !host.startsWith("\"")) { + _adblockHosts.add(host); + } + } + } + + } + + public void loadHostsFromRawAssetsAsync(final Context context) { + new Thread(new Runnable() { + @Override + public void run() { + try { + loadHostsFromRawAssets(context); + _isLoaded = true; + } catch (IOException e) { + e.printStackTrace(); + } + } + }).start(); + } + + private void loadHostsFromRawAssets(Context context) throws IOException { + BufferedReader br = null; + String host; + + _adblockHosts.clear(); + for (int rawId : getAdblockIdsInRaw()) { + try { + br = new BufferedReader(new InputStreamReader(context.getResources().openRawResource(rawId))); + while ((host = br.readLine()) != null) { + addBlockedHosts(host); + } + } catch (Exception e) { + Log.d(AdBlock.class.getName(), "Error: Cannot read adblock res " + rawId); + } finally { + if (br != null) { + try { + br.close(); + } catch (IOException ignored) { + } + } + } + } + _adblockHostsFromRaw.clear(); + _adblockHostsFromRaw.addAll(_adblockHosts); + } + + private List getAdblockIdsInRaw() { + ArrayList adblockResIds = new ArrayList<>(); + Field[] fields = R.raw.class.getFields(); + for (Field field : fields) { + try { + int resId = field.getInt(field); + String resFilename = field.getName(); + if (resFilename.startsWith("adblock_domains__")) { + adblockResIds.add(resId); + } + } catch (IllegalAccessException | IllegalArgumentException ignored) { + } + } + return adblockResIds; + } +} 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 000000000..a08ff682a --- /dev/null +++ b/app/src/main/java/net/gsantner/opoc/util/Callback.java @@ -0,0 +1,34 @@ +/*####################################################### + * + * Maintained by Gregor Santner, 2018- + * https://gsantner.net/ + * + * License: Apache 2.0 + * https://github.com/gsantner/opoc/#licensing + * https://www.apache.org/licenses/LICENSE-2.0 + * +#########################################################*/ +package net.gsantner.opoc.util; + +@SuppressWarnings("unused") +public class 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); + } +} 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 000000000..17d64424e --- /dev/null +++ b/app/src/main/java/net/gsantner/opoc/util/ContextUtils.java @@ -0,0 +1,675 @@ +/*####################################################### + * + * Maintained by Gregor Santner, 2016- + * https://gsantner.net/ + * + * License: Apache 2.0 + * https://github.com/gsantner/opoc/#licensing + * https://www.apache.org/licenses/LICENSE-2.0 + * +#########################################################*/ +package net.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.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.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.content.ContextCompat; +import android.support.v4.graphics.drawable.DrawableCompat; +import android.text.Html; +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.widget.ImageView; +import android.widget.TextView; + +import net.gsantner.opoc.format.markdown.SimpleMarkdownParser; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.lang.reflect.Method; +import java.util.Locale; + +import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; +import static android.graphics.Bitmap.CompressFormat; + +@SuppressWarnings({"WeakerAccess", "unused", "SameParameterValue", "ObsoleteSdkInt", "deprecation", "SpellCheckingInspection"}) +public class ContextUtils { + // + // Members, Constructors + // + protected Context _context; + + public ContextUtils(Context context) { + _context = context; + } + + public Context context() { + return _context; + } + + + // + // 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(ResType resType, final String name) { + return _context.getResources().getIdentifier(name, resType.name().toLowerCase(), _context.getPackageName()); + } + + /** + * Get String by given string ressource id (nuermic) + */ + public String rstr(@StringRes int strResId) { + return _context.getString(strResId); + } + + /** + * Get String by given string ressource identifier (textual) + */ + public String rstr(String strResKey) { + try { + return rstr(getResId(ResType.STRING, strResKey)); + } catch (Resources.NotFoundException e) { + return null; + } + } + + /** + * Get drawable from given ressource identifier + */ + public Drawable rdrawable(@DrawableRes int resId) { + return ContextCompat.getDrawable(_context, resId); + } + + /** + * Get color by given color ressource id + */ + public int rcolor(@ColorRes int resId) { + 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 String colorToHexString(int intColor, boolean... withAlpha) { + boolean a = withAlpha != null && withAlpha.length >= 1 && withAlpha[0]; + return String.format(a ? "#%08X" : "#%06X", (a ? 0xFFFFFFFF : 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 "?"; + } + } + + /** + * Send a {@link Intent#ACTION_VIEW} Intent with given paramter + * If the parameter is an string a browser will get triggered + */ + public void openWebpageInExternalBrowser(final String url) { + Uri uri = Uri.parse(url); + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + intent.addFlags(FLAG_ACTIVITY_NEW_TASK); + try { + _context.startActivity(intent); + } catch (ActivityNotFoundException e) { + e.printStackTrace(); + } + } + + /** + * 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(String fieldName) { + String pkg = rstr("manifest_package_id"); + pkg = (pkg != null ? pkg : _context.getPackageName()) + ".BuildConfig"; + try { + Class c = Class.forName(pkg); + return c.getField(fieldName).get(null); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + /** + * Get a BuildConfig bool value + */ + public Boolean bcbool(String fieldName, Boolean defaultValue) { + Object field = getBuildConfigValue(fieldName); + if (field != null && field instanceof Boolean) { + return (Boolean) field; + } + return defaultValue; + } + + /** + * Get a BuildConfig string value + */ + public String bcstr(String fieldName, String defaultValue) { + Object field = getBuildConfigValue(fieldName); + if (field != null && field instanceof String) { + return (String) 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); + } + + /** + * Request a bitcoin donation with given details. + * All parameters are awaited as string resource ids + */ + public void showDonateBitcoinRequest(@StringRes final int srBitcoinId, @StringRes final int srBitcoinAmount, @StringRes final int srBitcoinMessage, @StringRes final int srAlternativeDonateUrl) { + if (!isGooglePlayBuild()) { + String btcUri = String.format("bitcoin:%s?amount=%s&label=%s&message=%s", + rstr(srBitcoinId), rstr(srBitcoinAmount), + rstr(srBitcoinMessage), rstr(srBitcoinMessage)); + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse(btcUri)); + intent.addFlags(FLAG_ACTIVITY_NEW_TASK); + try { + _context.startActivity(intent); + } catch (ActivityNotFoundException e) { + openWebpageInExternalBrowser(rstr(srAlternativeDonateUrl)); + } + } + } + + public String readTextfileFromRawRes(@RawRes int rawResId, String linePrefix, String linePostfix) { + StringBuilder sb = new StringBuilder(); + BufferedReader br = null; + 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) { + PackageManager pm = _context.getApplicationContext().getPackageManager(); + try { + 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 inte = new Intent(_context, classToStart); + PendingIntent inteP = PendingIntent.getActivity(_context, 555, inte, PendingIntent.FLAG_CANCEL_CURRENT); + AlarmManager mgr = (AlarmManager) _context.getSystemService(Context.ALARM_SERVICE); + if (mgr != null) { + mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 100, inteP); + } else { + inte.addFlags(FLAG_ACTIVITY_NEW_TASK); + _context.startActivity(inte); + } + 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(String androidLC) { + Locale locale = getLocaleByAndroidCode(androidLC); + Configuration config = _context.getResources().getConfiguration(); + config.locale = (locale != null && !androidLC.isEmpty()) + ? locale : Resources.getSystem().getConfiguration().locale; + _context.getResources().updateConfiguration(config, null); + } + + /** + * Try to guess if the color on top of the given {@code colorOnBottomInt} + * should be light or dark. Returns true if top color should be light + */ + public boolean shouldColorOnTopBeLight(@ColorInt int colorOnBottomInt) { + 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(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; + } + + /** + * Request the givens paths to be scanned by MediaScanner + * + * @param files Files and folders to scan + */ + public void mediaScannerScanFile(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 int drawableId) { + return drawableToBitmap(ContextCompat.getDrawable(_context, drawableId)); + } + + /** + * 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(File imagePath, 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(BitmapFactory.Options options, 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(Bitmap bitmap, 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 {@code imageFile}, in {@link CompressFormat#JPEG} format + */ + public boolean writeImageToFileJpeg(File imageFile, Bitmap image) { + return writeImageToFile(imageFile, image, Bitmap.CompressFormat.JPEG, 95); + } + + /** + * Write the given {@link Bitmap} to filesystem + * + * @param targetFile The file to be written in + * @param image The image as android {@link Bitmap} + * @param format One format of {@link CompressFormat}, null will determine based on filename + * @param quality Quality level, defaults to 95 + * @return True if writing was successful + */ + public boolean writeImageToFile(File targetFile, Bitmap image, CompressFormat format, Integer quality) { + File folder = new File(targetFile.getParent()); + if (quality == null || quality < 0 || quality > 100) { + quality = 95; + } + if (format == null) { + format = CompressFormat.JPEG; + String lc = targetFile.getAbsolutePath().toLowerCase(Locale.ROOT); + if (lc.endsWith(".png")) { + format = CompressFormat.PNG; + } + if (lc.endsWith(".webp")) { + format = CompressFormat.WEBP; + } + } + if (folder.exists() || folder.mkdirs()) { + FileOutputStream stream = null; + try { + stream = new FileOutputStream(targetFile); // overwrites this image every time + image.compress(format, quality, stream); + return true; + } catch (FileNotFoundException ignored) { + } finally { + try { + if (stream != null) { + stream.close(); + } + } catch (IOException ignored) { + } + } + } + return false; + } + + /** + * Draw text in the center of the given {@link DrawableRes} + * This may be useful for e.g. badge counts + */ + public Bitmap drawTextOnDrawable(@DrawableRes int drawableRes, String text, int textSize) { + 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(Menu menu, boolean recurse, @ColorInt int iconColor) { + for (int i = 0; i < menu.size(); i++) { + MenuItem item = menu.getItem(i); + tintDrawable(item.getIcon(), iconColor); + if (item.hasSubMenu() && recurse) { + tintMenuItems(item.getSubMenu(), recurse, iconColor); + } + } + } + + /** + * Loads {@link Drawable} by given {@link DrawableRes} and applies a color + */ + public Drawable tintDrawable(@DrawableRes int drawableRes, @ColorInt int color) { + return tintDrawable(rdrawable(drawableRes), color); + } + + /** + * Tint a {@link Drawable} with given {@code color} + */ + public Drawable tintDrawable(@Nullable Drawable drawable, @ColorInt 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(Menu menu, boolean visible) { + 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"); + } + } + } +} 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 000000000..aec5b868e --- /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 000000000..13cb66d5a --- /dev/null +++ b/app/src/main/java/net/gsantner/opoc/util/FileUtils.java @@ -0,0 +1,340 @@ +/*####################################################### + * + * Maintained by Gregor Santner, 2017- + * https://gsantner.net/ + * + * License: Apache 2.0 + * https://github.com/gsantner/opoc/#licensing + * https://www.apache.org/licenses/LICENSE-2.0 + * +#########################################################*/ +package net.gsantner.opoc.util; + + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.UUID; +import java.util.regex.Pattern; + +@SuppressWarnings({"WeakerAccess", "unused", "SameParameterValue", "SpellCheckingInspection", "deprecation"}) +public class FileUtils { + // Used on methods like copyFile(src, dst) + private static final int BUFFER_SIZE = 4096; + + 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; + } + } + + // 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; + } + } +} 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 000000000..5212e5b72 --- /dev/null +++ b/app/src/main/java/net/gsantner/opoc/util/NetworkUtils.java @@ -0,0 +1,223 @@ +/*####################################################### + * + * Maintained by Gregor Santner, 2017- + * https://gsantner.net/ + * + * License: Apache 2.0 + * https://github.com/gsantner/opoc/#licensing + * https://www.apache.org/licenses/LICENSE-2.0 + * +#########################################################*/ +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); + } + + 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(UTF8))); + 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; + } +} 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 000000000..524c30651 --- /dev/null +++ b/app/src/main/java/net/gsantner/opoc/util/PermissionChecker.java @@ -0,0 +1,70 @@ +/*####################################################### + * + * Maintained by Gregor Santner, 2017- + * https://gsantner.net/ + * + * License: Apache 2.0 + * https://github.com/gsantner/opoc/#licensing + * https://www.apache.org/licenses/LICENSE-2.0 + * +#########################################################*/ +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 { + private static final int CODE_PERMISSION_EXTERNAL_STORAGE = 4000; + + private 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 000000000..21932cd6c --- /dev/null +++ b/app/src/main/java/net/gsantner/opoc/util/ShareUtil.java @@ -0,0 +1,453 @@ +/*####################################################### + * + * Maintained by Gregor Santner, 2017- + * https://gsantner.net/ + * + * License: Apache 2.0 + * https://github.com/gsantner/opoc/#licensing + * https://www.apache.org/licenses/LICENSE-2.0 + * +#########################################################*/ +package net.gsantner.opoc.util; + +import android.app.Activity; +import android.content.ClipData; +import android.content.Context; +import android.content.Intent; +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.print.PrintAttributes; +import android.print.PrintDocumentAdapter; +import android.print.PrintJob; +import android.print.PrintManager; +import android.support.annotation.DrawableRes; +import android.support.annotation.Nullable; +import android.support.annotation.RequiresApi; +import android.support.v4.content.FileProvider; +import android.support.v4.content.pm.ShortcutInfoCompat; +import android.support.v4.content.pm.ShortcutManagerCompat; +import android.support.v4.graphics.drawable.IconCompat; +import android.text.TextUtils; +import android.util.Log; +import android.view.View; +import android.webkit.WebView; + +import java.io.File; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Random; + +/** + * A utility class to ease information sharing on Android + * Also allows to parse/fetch information out of shared information + */ +@SuppressWarnings({"UnusedReturnValue", "WeakerAccess", "SameParameterValue", "unused", "deprecation", "ConstantConditions", "ObsoleteSdkInt", "SpellCheckingInspection"}) +public class ShareUtil { + public final static String EXTRA_FILEPATH = "real_file_path_2"; + public final static SimpleDateFormat SDF_RFC3339_ISH = new SimpleDateFormat("yyyy-MM-dd'T'HH-mm", Locale.getDefault()); + public final static SimpleDateFormat SDF_SHORT = new SimpleDateFormat("yyMMdd-HHmm", Locale.getDefault()); + + + protected Context _context; + protected String _fileProviderAuthority; + protected String _chooserTitle; + + public ShareUtil(Context context) { + _context = context; + _chooserTitle = "➥"; + } + + public String getFileProviderAuthority() { + if (TextUtils.isEmpty(_fileProviderAuthority)) { + throw new RuntimeException("Error at ShareUtil.getFileProviderAuthority(): No FileProvider authority provided"); + } + return _fileProviderAuthority; + } + + public ShareUtil setFileProviderAuthority(String fileProviderAuthority) { + _fileProviderAuthority = fileProviderAuthority; + return this; + } + + + public ShareUtil setChooserTitle(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(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(Intent intent, String chooserText) { + _context.startActivity(Intent.createChooser(intent, + chooserText != null ? chooserText : _chooserTitle)); + } + + /** + * 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(Intent intent, @DrawableRes int iconRes, 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(Intent intent, @DrawableRes int iconRes, 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(String text, @Nullable String mimeType) { + Intent intent = new Intent(Intent.ACTION_SEND); + intent.putExtra(Intent.EXTRA_TEXT, text); + intent.setType(mimeType != null ? mimeType : "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 void shareStream(File file, String mimeType) { + Uri fileUri = FileProvider.getUriForFile(_context, getFileProviderAuthority(), file); + Intent intent = new Intent(Intent.ACTION_SEND); + intent.putExtra(Intent.EXTRA_STREAM, fileUri); + intent.putExtra(EXTRA_FILEPATH, file.getAbsolutePath()); + intent.setType(mimeType); + showChooser(intent, null); + } + + /** + * Share the given bitmap with given format + * + * @param bitmap Image + * @param format A {@link Bitmap.CompressFormat}, supporting JPEG,PNG,WEBP + * @return if success, true + */ + public boolean shareImage(Bitmap bitmap, Bitmap.CompressFormat format) { + return shareImage(bitmap, format, 95, "SharedImage"); + } + + /** + * 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(Bitmap bitmap, Bitmap.CompressFormat format, int quality, String imageName) { + try { + String ext = format.name().toLowerCase(); + File file = File.createTempFile(imageName, "." + ext.replace("jpeg", "jpg"), _context.getExternalCacheDir()); + if (bitmap != null && new ContextUtils(_context).writeImageToFile(file, bitmap, format, quality)) { + shareStream(file, "image/" + ext); + return true; + } + } catch (IOException e) { + e.printStackTrace(); + } + return false; + } + + /** + * 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) + @SuppressWarnings("deprecation") + public PrintJob print(WebView webview, String jobName) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + PrintDocumentAdapter printAdapter; + PrintManager printManager = (PrintManager) webview.getContext().getSystemService(Context.PRINT_SERVICE); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + printAdapter = webview.createPrintDocumentAdapter(jobName); + } else { + printAdapter = webview.createPrintDocumentAdapter(); + } + if (printManager != null) { + return printManager.print(jobName, printAdapter, new PrintAttributes.Builder().build()); + } + } 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) + @SuppressWarnings("deprecation") + public PrintJob createPdf(WebView webview, 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(WebView webView) { + try { + //Measure WebView's content + int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); + int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); + webView.measure(widthMeasureSpec, heightMeasureSpec); + webView.layout(0, 0, webView.getMeasuredWidth(), webView.getMeasuredHeight()); + + //Build drawing cache and store its size + webView.buildDrawingCache(); + int measuredWidth = webView.getMeasuredWidth(); + int measuredHeight = webView.getMeasuredHeight(); + + //Creates the bitmap and draw WebView's content on in + Bitmap bitmap = Bitmap.createBitmap(measuredWidth, measuredHeight, Bitmap.Config.ARGB_8888); + 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(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); + cm.setPrimaryClip(clip); + 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, 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(String subject, String body, 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(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))) { + // 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"}) { + if (fileStr.startsWith(prefix)) { + fileStr = fileStr.substring(prefix.length()); + } + } + // 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()))); + } + // 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; + } + } + } + } + } + return null; + } +} 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 000000000..bbd3e0212 --- /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 000000000..bbd3e0212 --- /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-anydpi-v26/ic_launcher_test.xml b/app/src/main/res/drawable-anydpi-v26/ic_launcher_test.xml new file mode 100644 index 000000000..f1b3aaa2a --- /dev/null +++ b/app/src/main/res/drawable-anydpi-v26/ic_launcher_test.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-anydpi-v26/ic_launcher_test_round.xml b/app/src/main/res/drawable-anydpi-v26/ic_launcher_test_round.xml new file mode 100644 index 000000000..f1b3aaa2a --- /dev/null +++ b/app/src/main/res/drawable-anydpi-v26/ic_launcher_test_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi/chrome_custom_tab__back.png b/app/src/main/res/drawable-hdpi/chrome_custom_tab__back.png new file mode 100644 index 000000000..cd1972677 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/chrome_custom_tab__back.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_bell_outline_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_bell_outline_white_24dp.png deleted file mode 100644 index b53dde90e..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_bell_outline_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_bell_ring_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_bell_ring_white_24dp.png deleted file mode 100644 index 9cc1f069e..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_bell_ring_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_launcher.png b/app/src/main/res/drawable-hdpi/ic_launcher.png index d212a834c..6f647c49b 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 000000000..6f647c49b 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 new file mode 100644 index 000000000..c30acbbc1 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_launcher_test.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_launcher_test_round.png b/app/src/main/res/drawable-hdpi/ic_launcher_test_round.png new file mode 100644 index 000000000..c30acbbc1 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_launcher_test_round.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_message_text_outline_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_message_text_outline_white_24dp.png deleted file mode 100644 index 610aa1305..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_message_text_outline_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_message_text_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_message_text_white_24dp.png deleted file mode 100644 index a086bcbb4..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_message_text_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_sync_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_sync_white_24dp.png deleted file mode 100644 index 013d1c2a2..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_sync_white_24dp.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 new file mode 100644 index 000000000..8e89a6a91 Binary files /dev/null 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 new file mode 100644 index 000000000..a4c4ac0b2 Binary files /dev/null and b/app/src/main/res/drawable-ldpi/ic_launcher_test.png differ diff --git a/app/src/main/res/drawable-mdpi/chrome_custom_tab__back.png b/app/src/main/res/drawable-mdpi/chrome_custom_tab__back.png new file mode 100644 index 000000000..4ef72eec9 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/chrome_custom_tab__back.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_bell_outline_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_bell_outline_white_24dp.png deleted file mode 100644 index 0d4b0a1d3..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_bell_outline_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_bell_ring_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_bell_ring_white_24dp.png deleted file mode 100644 index 56001f7b9..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_bell_ring_white_24dp.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 d212a834c..a9db7c164 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 000000000..a9db7c164 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 new file mode 100644 index 000000000..f23d1091a Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_launcher_test.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_launcher_test_round.png b/app/src/main/res/drawable-mdpi/ic_launcher_test_round.png new file mode 100644 index 000000000..f23d1091a Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_launcher_test_round.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_message_text_outline_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_message_text_outline_white_24dp.png deleted file mode 100644 index 576b52747..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_message_text_outline_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_message_text_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_message_text_white_24dp.png deleted file mode 100644 index aee3a1ce4..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_message_text_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_sync_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_sync_white_24dp.png deleted file mode 100644 index 91770671c..000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_sync_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/chrome_custom_tab__back.png b/app/src/main/res/drawable-xhdpi/chrome_custom_tab__back.png new file mode 100644 index 000000000..832f5a361 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/chrome_custom_tab__back.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_bell_outline_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_bell_outline_white_24dp.png deleted file mode 100644 index 657e69d81..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_bell_outline_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_bell_ring_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_bell_ring_white_24dp.png deleted file mode 100644 index be2a70e04..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_bell_ring_white_24dp.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 d212a834c..c97e5b53f 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 000000000..c97e5b53f 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 new file mode 100644 index 000000000..43025584e Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_launcher_test.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_launcher_test_round.png b/app/src/main/res/drawable-xhdpi/ic_launcher_test_round.png new file mode 100644 index 000000000..43025584e Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_launcher_test_round.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_message_text_outline_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_message_text_outline_white_24dp.png deleted file mode 100644 index d66b733af..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_message_text_outline_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_message_text_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_message_text_white_24dp.png deleted file mode 100644 index 5155a77a6..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_message_text_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_sync_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_sync_white_24dp.png deleted file mode 100644 index 01a969d5a..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_sync_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/chrome_custom_tab__back.png b/app/src/main/res/drawable-xxhdpi/chrome_custom_tab__back.png new file mode 100644 index 000000000..32a6d91ce Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/chrome_custom_tab__back.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_bell_outline_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_bell_outline_white_24dp.png deleted file mode 100644 index 069bb3e5a..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_bell_outline_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_bell_ring_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_bell_ring_white_24dp.png deleted file mode 100644 index cdb2c24fe..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_bell_ring_white_24dp.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 d212a834c..11c9bd14e 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 000000000..11c9bd14e 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 new file mode 100644 index 000000000..3993c4e4c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_launcher_test.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_launcher_test_round.png b/app/src/main/res/drawable-xxhdpi/ic_launcher_test_round.png new file mode 100644 index 000000000..3993c4e4c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_launcher_test_round.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_message_text_outline_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_message_text_outline_white_24dp.png deleted file mode 100644 index 9fcb86829..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_message_text_outline_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_message_text_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_message_text_white_24dp.png deleted file mode 100644 index 1d3d19e7b..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_message_text_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_sync_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_sync_white_24dp.png deleted file mode 100644 index 9b0836da8..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_sync_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/chrome_custom_tab__back.png b/app/src/main/res/drawable-xxxhdpi/chrome_custom_tab__back.png new file mode 100644 index 000000000..e27034d67 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/chrome_custom_tab__back.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_bell_outline_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_bell_outline_white_24dp.png deleted file mode 100644 index 2756e2ae5..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_bell_outline_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_bell_ring_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_bell_ring_white_24dp.png deleted file mode 100644 index 7ed1a2f27..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_bell_ring_white_24dp.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 d212a834c..48f09459f 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 000000000..48f09459f 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 new file mode 100644 index 000000000..72aabea6a Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_launcher_test.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_launcher_test_round.png b/app/src/main/res/drawable-xxxhdpi/ic_launcher_test_round.png new file mode 100644 index 000000000..72aabea6a Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_launcher_test_round.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_message_text_outline_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_message_text_outline_white_24dp.png deleted file mode 100644 index 26a104231..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_message_text_outline_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_message_text_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_message_text_white_24dp.png deleted file mode 100644 index 3fd53a5ad..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_message_text_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_sync_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_sync_white_24dp.png deleted file mode 100644 index 73cf22cac..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_sync_white_24dp.png and /dev/null differ diff --git a/app/src/main/res/drawable/circle.xml b/app/src/main/res/drawable/circle.xml new file mode 100644 index 000000000..eeadfaf09 --- /dev/null +++ b/app/src/main/res/drawable/circle.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/fab_compose.png b/app/src/main/res/drawable/fab_compose.png deleted file mode 100644 index 5ef731b4c..000000000 Binary files a/app/src/main/res/drawable/fab_compose.png and /dev/null differ diff --git a/app/src/main/res/drawable/fab_label_background.xml b/app/src/main/res/drawable/fab_label_background.xml deleted file mode 100644 index 83f55c971..000000000 --- a/app/src/main/res/drawable/fab_label_background.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/fab_search.png b/app/src/main/res/drawable/fab_search.png deleted file mode 100644 index 78f32f75c..000000000 Binary files a/app/src/main/res/drawable/fab_search.png and /dev/null differ diff --git a/app/src/main/res/drawable/fab_top.png b/app/src/main/res/drawable/fab_top.png deleted file mode 100644 index 402daf58f..000000000 Binary files a/app/src/main/res/drawable/fab_top.png and /dev/null differ diff --git a/app/src/main/res/drawable/header.jpg b/app/src/main/res/drawable/header.jpg deleted file mode 100644 index 915e324ca..000000000 Binary files a/app/src/main/res/drawable/header.jpg and /dev/null differ diff --git a/app/src/main/res/drawable/ic_arrow_back_black_24px.xml b/app/src/main/res/drawable/ic_arrow_back_black_24px.xml new file mode 100644 index 000000000..9b1cf9759 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_back_black_24px.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_arrow_back_white_24px.xml b/app/src/main/res/drawable/ic_arrow_back_white_24px.xml new file mode 100644 index 000000000..ded32bb2f --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_back_white_24px.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_arrow_upward_white_48px.xml b/app/src/main/res/drawable/ic_arrow_upward_white_48px.xml new file mode 100644 index 000000000..92bfb15df --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_upward_white_48px.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_atsign.xml b/app/src/main/res/drawable/ic_atsign.xml new file mode 100644 index 000000000..d68306184 --- /dev/null +++ b/app/src/main/res/drawable/ic_atsign.xml @@ -0,0 +1,7 @@ + + + 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 new file mode 100644 index 000000000..4d83902b8 --- /dev/null +++ b/app/src/main/res/drawable/ic_bug_report_black_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_cancel_black_48px.xml b/app/src/main/res/drawable/ic_cancel_black_48px.xml new file mode 100644 index 000000000..789bf6f40 --- /dev/null +++ b/app/src/main/res/drawable/ic_cancel_black_48px.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_clear_black_24px.xml b/app/src/main/res/drawable/ic_clear_black_24px.xml new file mode 100644 index 000000000..178a01274 --- /dev/null +++ b/app/src/main/res/drawable/ic_clear_black_24px.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file 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 new file mode 100644 index 000000000..f75e2fbe3 --- /dev/null +++ b/app/src/main/res/drawable/ic_color_lens_black_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_comment_black_48px.xml b/app/src/main/res/drawable/ic_comment_black_48px.xml new file mode 100644 index 000000000..040e3245e --- /dev/null +++ b/app/src/main/res/drawable/ic_comment_black_48px.xml @@ -0,0 +1,14 @@ + + + + + + \ 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 new file mode 100644 index 000000000..674b66b7c --- /dev/null +++ b/app/src/main/res/drawable/ic_contacts_black_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_dashboard_black_48px.xml b/app/src/main/res/drawable/ic_dashboard_black_48px.xml new file mode 100644 index 000000000..e204deb5b --- /dev/null +++ b/app/src/main/res/drawable/ic_dashboard_black_48px.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_favorite_black_48px.xml b/app/src/main/res/drawable/ic_favorite_black_48px.xml new file mode 100644 index 000000000..e3cc36daf --- /dev/null +++ b/app/src/main/res/drawable/ic_favorite_black_48px.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_file_download_black_24px.xml b/app/src/main/res/drawable/ic_file_download_black_24px.xml new file mode 100644 index 000000000..102d49709 --- /dev/null +++ b/app/src/main/res/drawable/ic_file_download_black_24px.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_format_align_left_black_48px.xml b/app/src/main/res/drawable/ic_format_align_left_black_48px.xml new file mode 100644 index 000000000..f1e324c58 --- /dev/null +++ b/app/src/main/res/drawable/ic_format_align_left_black_48px.xml @@ -0,0 +1,4 @@ + + + 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 new file mode 100644 index 000000000..6cb93c699 --- /dev/null +++ b/app/src/main/res/drawable/ic_format_list_bulleted_black_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_format_size_black_24px.xml b/app/src/main/res/drawable/ic_format_size_black_24px.xml new file mode 100644 index 000000000..184d252f4 --- /dev/null +++ b/app/src/main/res/drawable/ic_format_size_black_24px.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_group_black_48px.xml b/app/src/main/res/drawable/ic_group_black_48px.xml new file mode 100644 index 000000000..c023a47c7 --- /dev/null +++ b/app/src/main/res/drawable/ic_group_black_48px.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_history_black_48px.xml b/app/src/main/res/drawable/ic_history_black_48px.xml new file mode 100644 index 000000000..b70c2146c --- /dev/null +++ b/app/src/main/res/drawable/ic_history_black_48px.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_info_black_48px.xml b/app/src/main/res/drawable/ic_info_black_48px.xml new file mode 100644 index 000000000..a002fad8e --- /dev/null +++ b/app/src/main/res/drawable/ic_info_black_48px.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_language_black_48px.xml b/app/src/main/res/drawable/ic_language_black_48px.xml new file mode 100644 index 000000000..295368470 --- /dev/null +++ b/app/src/main/res/drawable/ic_language_black_48px.xml @@ -0,0 +1,4 @@ + + + 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 000000000..10e5177cf --- /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 000000000..2610c60ce --- /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_launcher_test_background.xml b/app/src/main/res/drawable/ic_launcher_test_background.xml new file mode 100644 index 000000000..e7de1de8a --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_test_background.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher_test_foreground.xml b/app/src/main/res/drawable/ic_launcher_test_foreground.xml new file mode 100644 index 000000000..2610c60ce --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_test_foreground.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_local_offer_black_48px.xml b/app/src/main/res/drawable/ic_local_offer_black_48px.xml new file mode 100644 index 000000000..68fdd1b79 --- /dev/null +++ b/app/src/main/res/drawable/ic_local_offer_black_48px.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_mail_white_48px.xml b/app/src/main/res/drawable/ic_mail_white_48px.xml new file mode 100644 index 000000000..5382ce54a --- /dev/null +++ b/app/src/main/res/drawable/ic_mail_white_48px.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_mail_white_48px__layer.xml b/app/src/main/res/drawable/ic_mail_white_48px__layer.xml new file mode 100644 index 000000000..983a98bf8 --- /dev/null +++ b/app/src/main/res/drawable/ic_mail_white_48px__layer.xml @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file 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 000000000..7575f1a57 --- /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_mode_edit_white_48px.xml b/app/src/main/res/drawable/ic_mode_edit_white_48px.xml new file mode 100644 index 000000000..a7b5fa79c --- /dev/null +++ b/app/src/main/res/drawable/ic_mode_edit_white_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 new file mode 100644 index 000000000..7009a6763 --- /dev/null +++ b/app/src/main/res/drawable/ic_notifications_black_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_notifications_white_48px.xml b/app/src/main/res/drawable/ic_notifications_white_48px.xml new file mode 100644 index 000000000..173dea85c --- /dev/null +++ b/app/src/main/res/drawable/ic_notifications_white_48px.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_notifications_white_48px__layer.xml b/app/src/main/res/drawable/ic_notifications_white_48px__layer.xml new file mode 100644 index 000000000..971baea64 --- /dev/null +++ b/app/src/main/res/drawable/ic_notifications_white_48px__layer.xml @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file 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 new file mode 100644 index 000000000..6c4a8cb24 --- /dev/null +++ b/app/src/main/res/drawable/ic_open_in_new_black_24px.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_people_black_24px.xml b/app/src/main/res/drawable/ic_people_black_24px.xml new file mode 100644 index 000000000..4cfd86960 --- /dev/null +++ b/app/src/main/res/drawable/ic_people_black_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_person_add_black_48px.xml b/app/src/main/res/drawable/ic_person_add_black_48px.xml new file mode 100644 index 000000000..5b1bbcc66 --- /dev/null +++ b/app/src/main/res/drawable/ic_person_add_black_48px.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_person_black_24px.xml b/app/src/main/res/drawable/ic_person_black_24px.xml new file mode 100644 index 000000000..ecd54ce83 --- /dev/null +++ b/app/src/main/res/drawable/ic_person_black_24px.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_person_black_48px.xml b/app/src/main/res/drawable/ic_person_black_48px.xml new file mode 100644 index 000000000..223f3f268 --- /dev/null +++ b/app/src/main/res/drawable/ic_person_black_48px.xml @@ -0,0 +1,4 @@ + + + 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 new file mode 100644 index 000000000..62a0a5d61 --- /dev/null +++ b/app/src/main/res/drawable/ic_person_pin_black_48px.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_public_black_48px.xml b/app/src/main/res/drawable/ic_public_black_48px.xml new file mode 100644 index 000000000..97b21143e --- /dev/null +++ b/app/src/main/res/drawable/ic_public_black_48px.xml @@ -0,0 +1,4 @@ + + + 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 new file mode 100644 index 000000000..5216a9ae5 --- /dev/null +++ b/app/src/main/res/drawable/ic_question_answer_black_48px.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_refresh_white_48px.xml b/app/src/main/res/drawable/ic_refresh_white_48px.xml new file mode 100644 index 000000000..a3ff4f671 --- /dev/null +++ b/app/src/main/res/drawable/ic_refresh_white_48px.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_repeat_black_48px.xml b/app/src/main/res/drawable/ic_repeat_black_48px.xml new file mode 100644 index 000000000..b39dca7a0 --- /dev/null +++ b/app/src/main/res/drawable/ic_repeat_black_48px.xml @@ -0,0 +1,13 @@ + + + + + + \ 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 new file mode 100644 index 000000000..586f1aab9 --- /dev/null +++ b/app/src/main/res/drawable/ic_report_black_48px.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file 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 new file mode 100644 index 000000000..b63eaae7a --- /dev/null +++ b/app/src/main/res/drawable/ic_screen_rotation_black_24px.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_search_white_48px.xml b/app/src/main/res/drawable/ic_search_white_48px.xml new file mode 100644 index 000000000..13b7e4841 --- /dev/null +++ b/app/src/main/res/drawable/ic_search_white_48px.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_settings_black_48px.xml b/app/src/main/res/drawable/ic_settings_black_48px.xml new file mode 100644 index 000000000..8fbaa9a2f --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_black_48px.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_share_white_48px.xml b/app/src/main/res/drawable/ic_share_white_48px.xml new file mode 100644 index 000000000..1a53d3d94 --- /dev/null +++ b/app/src/main/res/drawable/ic_share_white_48px.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_star_border_black_48px.xml b/app/src/main/res/drawable/ic_star_border_black_48px.xml new file mode 100644 index 000000000..3da8c75bd --- /dev/null +++ b/app/src/main/res/drawable/ic_star_border_black_48px.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_star_filled_48px.xml b/app/src/main/res/drawable/ic_star_filled_48px.xml new file mode 100644 index 000000000..2b1fe326f --- /dev/null +++ b/app/src/main/res/drawable/ic_star_filled_48px.xml @@ -0,0 +1,6 @@ + + + diff --git a/app/src/main/res/drawable/ic_stream.xml b/app/src/main/res/drawable/ic_stream.xml new file mode 100644 index 000000000..0050d519b --- /dev/null +++ b/app/src/main/res/drawable/ic_stream.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_sync_white_48px.xml b/app/src/main/res/drawable/ic_sync_white_48px.xml new file mode 100644 index 000000000..0868795df --- /dev/null +++ b/app/src/main/res/drawable/ic_sync_white_48px.xml @@ -0,0 +1,4 @@ + + + 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 new file mode 100644 index 000000000..1fca08b39 --- /dev/null +++ b/app/src/main/res/drawable/ic_thumb_up_black_24px.xml @@ -0,0 +1,16 @@ + + + + + + \ 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 new file mode 100644 index 000000000..1fca08b39 --- /dev/null +++ b/app/src/main/res/drawable/ic_thumb_up_black_48px.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_timeline_black_48px.xml b/app/src/main/res/drawable/ic_timeline_black_48px.xml new file mode 100644 index 000000000..9184473cd --- /dev/null +++ b/app/src/main/res/drawable/ic_timeline_black_48px.xml @@ -0,0 +1,4 @@ + + + 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 new file mode 100644 index 000000000..dda5c85cc --- /dev/null +++ b/app/src/main/res/drawable/ic_touch_app_black_24px.xml @@ -0,0 +1,9 @@ + + + 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 000000000..e02f1d191 --- /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 new file mode 100644 index 000000000..b3bb7460e --- /dev/null +++ b/app/src/main/res/drawable/ic_visibility_off_black_24px.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_visibility_on_black_24px.xml b/app/src/main/res/drawable/ic_visibility_on_black_24px.xml new file mode 100644 index 000000000..b4bf03e57 --- /dev/null +++ b/app/src/main/res/drawable/ic_visibility_on_black_24px.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_visibility_selector.xml b/app/src/main/res/drawable/ic_visibility_selector.xml new file mode 100644 index 000000000..891267a1d --- /dev/null +++ b/app/src/main/res/drawable/ic_visibility_selector.xml @@ -0,0 +1,7 @@ + + + + + \ 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 new file mode 100644 index 000000000..266f1578d --- /dev/null +++ b/app/src/main/res/drawable/ic_vpn_lock_black_24px.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_zoom_out_map_black_24px.xml b/app/src/main/res/drawable/ic_zoom_out_map_black_24px.xml new file mode 100644 index 000000000..8fc5ffb14 --- /dev/null +++ b/app/src/main/res/drawable/ic_zoom_out_map_black_24px.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/jb_activities.png b/app/src/main/res/drawable/jb_activities.png deleted file mode 100644 index f4f7d1310..000000000 Binary files a/app/src/main/res/drawable/jb_activities.png and /dev/null differ diff --git a/app/src/main/res/drawable/jb_aspects.png b/app/src/main/res/drawable/jb_aspects.png deleted file mode 100644 index 1f2b3a4f1..000000000 Binary files a/app/src/main/res/drawable/jb_aspects.png and /dev/null differ diff --git a/app/src/main/res/drawable/jb_commented.png b/app/src/main/res/drawable/jb_commented.png deleted file mode 100644 index 64f27b0b3..000000000 Binary files a/app/src/main/res/drawable/jb_commented.png and /dev/null differ diff --git a/app/src/main/res/drawable/jb_heart.png b/app/src/main/res/drawable/jb_heart.png deleted file mode 100644 index ea9685e3e..000000000 Binary files a/app/src/main/res/drawable/jb_heart.png and /dev/null differ diff --git a/app/src/main/res/drawable/jb_license.png b/app/src/main/res/drawable/jb_license.png deleted file mode 100644 index b552a1c77..000000000 Binary files a/app/src/main/res/drawable/jb_license.png and /dev/null differ diff --git a/app/src/main/res/drawable/jb_mentions.png b/app/src/main/res/drawable/jb_mentions.png deleted file mode 100644 index 726393c00..000000000 Binary files a/app/src/main/res/drawable/jb_mentions.png and /dev/null differ diff --git a/app/src/main/res/drawable/jb_settings.png b/app/src/main/res/drawable/jb_settings.png deleted file mode 100644 index 9d65f2a17..000000000 Binary files a/app/src/main/res/drawable/jb_settings.png and /dev/null differ diff --git a/app/src/main/res/drawable/jb_stream.png b/app/src/main/res/drawable/jb_stream.png deleted file mode 100644 index a5586dbd4..000000000 Binary files a/app/src/main/res/drawable/jb_stream.png and /dev/null differ diff --git a/app/src/main/res/drawable/jb_tag2.png b/app/src/main/res/drawable/jb_tag2.png deleted file mode 100644 index c8711117f..000000000 Binary files a/app/src/main/res/drawable/jb_tag2.png and /dev/null differ diff --git a/app/src/main/res/drawable/progressbar.xml b/app/src/main/res/drawable/progressbar.xml new file mode 100644 index 000000000..2a6e82ecd --- /dev/null +++ b/app/src/main/res/drawable/progressbar.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + \ 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 000000000..3ca0e79fe --- /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 000000000..fb9e361a7 --- /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 000000000..a100d6652 --- /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 000000000..ed8ad05f2 --- /dev/null +++ b/app/src/main/res/drawable/sc_tags.xml @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/splashscreen1.jpg b/app/src/main/res/drawable/splashscreen1.jpg deleted file mode 100644 index bd2f04d61..000000000 Binary files a/app/src/main/res/drawable/splashscreen1.jpg and /dev/null differ diff --git a/app/src/main/res/drawable/splashscreen2.jpg b/app/src/main/res/drawable/splashscreen2.jpg deleted file mode 100644 index 2be7268dd..000000000 Binary files a/app/src/main/res/drawable/splashscreen2.jpg and /dev/null differ diff --git a/app/src/main/res/drawable/splashscreen3.jpg b/app/src/main/res/drawable/splashscreen3.jpg deleted file mode 100644 index 8f1a89974..000000000 Binary files a/app/src/main/res/drawable/splashscreen3.jpg and /dev/null differ diff --git a/app/src/main/res/drawable/splashscreen4.jpg b/app/src/main/res/drawable/splashscreen4.jpg deleted file mode 100644 index 77ef17d66..000000000 Binary files a/app/src/main/res/drawable/splashscreen4.jpg and /dev/null differ diff --git a/app/src/main/res/drawable/splashscreen5.jpg b/app/src/main/res/drawable/splashscreen5.jpg deleted file mode 100644 index e1128818f..000000000 Binary files a/app/src/main/res/drawable/splashscreen5.jpg and /dev/null differ diff --git a/app/src/main/res/drawable/splashscreen6.jpg b/app/src/main/res/drawable/splashscreen6.jpg deleted file mode 100644 index 6f97fa4c4..000000000 Binary files a/app/src/main/res/drawable/splashscreen6.jpg and /dev/null differ diff --git a/app/src/main/res/drawable/tor_onion.xml b/app/src/main/res/drawable/tor_onion.xml new file mode 100644 index 000000000..7df39ce0a --- /dev/null +++ b/app/src/main/res/drawable/tor_onion.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout/about__activity.xml b/app/src/main/res/layout/about__activity.xml new file mode 100644 index 000000000..6f0f20061 --- /dev/null +++ b/app/src/main/res/layout/about__activity.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/about__fragment_about.xml b/app/src/main/res/layout/about__fragment_about.xml new file mode 100644 index 000000000..61ecc491d --- /dev/null +++ b/app/src/main/res/layout/about__fragment_about.xml @@ -0,0 +1,237 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + +