├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── fake-google-services.json └── workflows │ ├── cla.yml │ ├── runOnGitHub.yml │ └── update-gradle-wrapper.yml ├── .gitignore ├── .idea └── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── app ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── to │ │ └── dev │ │ └── dev_android │ │ └── ExampleInstrumentedTest.kt │ ├── debug │ └── res │ │ └── values │ │ └── strings.xml │ ├── main │ ├── AndroidManifest.xml │ ├── ic_launcher-web.png │ ├── java │ │ └── to │ │ │ └── dev │ │ │ └── dev_android │ │ │ ├── activities │ │ │ ├── BaseActivity.kt │ │ │ ├── ForemAppDialog.kt │ │ │ ├── MainActivity.kt │ │ │ ├── StarterApplication.kt │ │ │ └── VideoPlayerActivity.kt │ │ │ ├── events │ │ │ ├── NetworkStatusEvent.kt │ │ │ ├── VideoPlayerPauseEvent.kt │ │ │ └── VideoPlayerTickEvent.kt │ │ │ ├── media │ │ │ ├── AudioService.kt │ │ │ └── PodcastPlayerNotificationManager.kt │ │ │ ├── util │ │ │ ├── AndroidWebViewBridge.kt │ │ │ └── network │ │ │ │ ├── NetworkStatus.kt │ │ │ │ ├── NetworkUtils.kt │ │ │ │ └── NetworkWatcher.kt │ │ │ └── webclients │ │ │ ├── CustomWebChromeClient.kt │ │ │ └── CustomWebViewClient.kt │ └── res │ │ ├── drawable │ │ ├── forem_dialog_fragment_background.xml │ │ ├── ic_baseline_arrow_downward_24.xml │ │ ├── ic_compass.xml │ │ ├── ic_forem_bot.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_launcher_foreground.xml │ │ ├── ic_news.xml │ │ ├── rounded_blue_button.xml │ │ └── rounded_dialog.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── activity_video_player.xml │ │ ├── exo_player_control_view.xml │ │ └── forem_app_dialog.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── to │ └── dev │ └── dev_android │ └── view │ └── main │ └── view │ ├── CustomWebViewClientTest.kt │ └── MainActivityTest.kt ├── build.gradle.kts ├── buildSrc ├── .gitignore ├── build.gradle.kts └── src │ └── main │ └── kotlin │ ├── Android.kt │ ├── CustomTasks.kt │ └── Libs.kt ├── config └── detekt │ └── detekt.yml ├── fastlane ├── Appfile └── Fastfile ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── versions.properties /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | 12 | 13 | **To Reproduce** 14 | 15 | 16 | 17 | 18 | 19 | 20 | **Expected behavior** 21 | 22 | 23 | **Screenshots** 24 | 25 | 26 | **Smartphone (please complete the following information):** 27 | - Device: 28 | - OS: 29 | - Version: 30 | 31 | **Additional context** 32 | 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | 12 | 13 | **Describe the solution you'd like** 14 | 15 | 16 | **Describe alternatives you've considered** 17 | 18 | 19 | **Additional context** 20 | 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## What type of PR is this? (check all applicable) 4 | 5 | - [ ] Refactor 6 | - [ ] Feature 7 | - [ ] Bug Fix 8 | - [ ] Documentation Update 9 | 10 | ## Description 11 | 12 | ## Related Tickets & Documents 13 | 14 | ## Screenshots/Recordings (if there are UI changes) 15 | 16 | ## [optional] What gif best describes this PR or how it makes you feel? 17 | 18 | ![alt_text](gif_link) 19 | -------------------------------------------------------------------------------- /.github/fake-google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "1092521184945", 4 | "firebase_url": "https://dev-personal-XXX.firebaseio.com", 5 | "project_id": "dev-personal-XXX", 6 | "storage_bucket": "dev-personal-XXX.appspot.com" 7 | }, 8 | "client": [ 9 | { 10 | "client_info": { 11 | "mobilesdk_app_id": "1:1092521184945:android:af137629aab59de5af1c90", 12 | "android_client_info": { 13 | "package_name": "to.dev.dev_android.debug" 14 | } 15 | }, 16 | "oauth_client": [ 17 | { 18 | "client_id": "XX.apps.googleusercontent.com", 19 | "client_type": 3 20 | } 21 | ], 22 | "api_key": [ 23 | { 24 | "current_key": "XXX" 25 | } 26 | ], 27 | "services": { 28 | "appinvite_service": { 29 | "other_platform_oauth_client": [ 30 | { 31 | "client_id": "XXX.apps.googleusercontent.com", 32 | "client_type": 3 33 | } 34 | ] 35 | } 36 | } 37 | } 38 | ], 39 | "configuration_version": "1" 40 | } -------------------------------------------------------------------------------- /.github/workflows/cla.yml: -------------------------------------------------------------------------------- 1 | name: "CLA" 2 | on: 3 | issue_comment: 4 | types: [created] 5 | pull_request_target: 6 | types: [opened,closed,synchronize] 7 | merge_group: 8 | branches: 9 | - main 10 | 11 | permissions: 12 | actions: write 13 | contents: write 14 | pull-requests: write 15 | statuses: write 16 | 17 | jobs: 18 | check_cla: 19 | uses: forem/forem/.github/workflows/cla.yml@main 20 | secrets: inherit 21 | -------------------------------------------------------------------------------- /.github/workflows/runOnGitHub.yml: -------------------------------------------------------------------------------- 1 | # .github/workflows/runOnGitHub.yml 2 | # GitHub Actions documentation 3 | # => https://docs.github.com/en/actions 4 | name: runOnGitHub 5 | 6 | # Controls when the action will run. Triggers the workflow on push or pull request 7 | # events but only for the master branch 8 | on: push 9 | jobs: 10 | gradle: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v1 14 | - name: Copy fake google-services.json 15 | run: cp -f .github/fake-google-services.json app/google-services.json 16 | - uses: actions/setup-java@v1 17 | with: 18 | java-version: 8 19 | 20 | # Execute Gradle commands in GitHub Actions workflows 21 | # => https://github.com/marketplace/actions/gradle-command 22 | - uses: eskatos/gradle-command-action@v1 23 | with: 24 | arguments: runOnGitHub 25 | wrapper-cache-enabled: true 26 | dependencies-cache-enabled: true 27 | configuration-cache-enabled: true 28 | -------------------------------------------------------------------------------- /.github/workflows/update-gradle-wrapper.yml: -------------------------------------------------------------------------------- 1 | name: Update Gradle Wrapper 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | 7 | jobs: 8 | update-gradle-wrapper: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - name: Update Gradle Wrapper 15 | uses: gradle-update/update-gradle-wrapper-action@v1 16 | with: 17 | repo-token: ${{ secrets.GITHUB_TOKEN }} 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | *.aab 5 | 6 | # Files for the ART/Dalvik VM 7 | *.dex 8 | 9 | # Java class files 10 | *.class 11 | 12 | # Generated files 13 | bin/ 14 | gen/ 15 | out/ 16 | 17 | # Gradle files 18 | .gradle/ 19 | /buildSrc/.gradle 20 | 21 | # Build files 22 | /build 23 | /buildSrc/build 24 | /app/build 25 | 26 | # Local configuration file (sdk path, etc) 27 | local.properties 28 | 29 | # Proguard folder generated by Eclipse 30 | proguard/ 31 | 32 | # Log Files 33 | *.log 34 | 35 | # Android Studio Navigation editor temp files 36 | .navigation/ 37 | 38 | # Android Studio captures folder 39 | captures/ 40 | 41 | # IntelliJ 42 | *.iml 43 | .idea/**/workspace.xml 44 | .idea/**/tasks.xml 45 | .idea/**/usage.statistics.xml 46 | .idea/**/contentModel.xml 47 | .idea/**/gradle.xml 48 | .idea/**/misc.xml 49 | .idea/**/dictionaries 50 | .idea/**/shelf 51 | .idea/assetWizardSettings.xml 52 | .idea/libraries 53 | .idea/caches 54 | .idea/caches/build_file_checksums.ser 55 | 56 | # Sensitive or high-churn files 57 | .idea/**/dataSources/ 58 | .idea/**/dataSources.ids 59 | .idea/**/dataSources.local.xml 60 | .idea/**/sqlDataSources.xml 61 | .idea/**/dynamic.xml 62 | .idea/**/uiDesigner.xml 63 | .idea/**/dbnavigator.xml 64 | 65 | # Gradle and Maven with auto-import 66 | .idea/modules.xml 67 | .idea/jarRepositories.xml 68 | .idea/artifacts 69 | .idea/compiler.xml 70 | .idea/*.iml 71 | .idea/modules 72 | *.iml 73 | *.ipr 74 | *.iws 75 | 76 | # Keystore files 77 | # Uncomment the following lines if you do not want to check your keystore files in. 78 | *.jks 79 | *.keystore 80 | keystore.properties 81 | 82 | # External native build folder generated in Android Studio 2.2 and later 83 | .externalNativeBuild 84 | 85 | # Google Services (e.g. APIs or Firebase) 86 | google-services.json 87 | app/**/google-services.json 88 | 89 | # Freeline 90 | freeline.py 91 | freeline/ 92 | freeline_project_description.json 93 | 94 | # fastlane 95 | fastlane/report.xml 96 | fastlane/Preview.html 97 | fastlane/screenshots 98 | fastlane/test_output 99 | fastlane/readme.md 100 | 101 | # Version control 102 | vcs.xml 103 | 104 | # lint 105 | lint/intermediates/ 106 | lint/generated/ 107 | lint/outputs/ 108 | lint/tmp/ 109 | lint/reports/ 110 | 111 | # Detekt Reports 112 | reports/ 113 | 114 | # Firebase config 115 | app/google-services.json 116 | 117 | # Google's service account user 118 | google-service-account-user.json 119 | 120 | # Fastlane 121 | fastlane/README.md 122 | fastlane/metadata/ 123 | fastlane/*.xml 124 | 125 | # Extra 126 | *.DS_Store 127 | 128 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 20 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | xmlns:android 30 | 31 | ^$ 32 | 33 | 34 | 35 |
36 |
37 | 38 | 39 | 40 | xmlns:.* 41 | 42 | ^$ 43 | 44 | 45 | BY_NAME 46 | 47 |
48 |
49 | 50 | 51 | 52 | .*:id 53 | 54 | http://schemas.android.com/apk/res/android 55 | 56 | 57 | 58 |
59 |
60 | 61 | 62 | 63 | .*:name 64 | 65 | http://schemas.android.com/apk/res/android 66 | 67 | 68 | 69 |
70 |
71 | 72 | 73 | 74 | name 75 | 76 | ^$ 77 | 78 | 79 | 80 |
81 |
82 | 83 | 84 | 85 | style 86 | 87 | ^$ 88 | 89 | 90 | 91 |
92 |
93 | 94 | 95 | 96 | .* 97 | 98 | ^$ 99 | 100 | 101 | BY_NAME 102 | 103 |
104 |
105 | 106 | 107 | 108 | .* 109 | 110 | http://schemas.android.com/apk/res/android 111 | 112 | 113 | ANDROID_ATTRIBUTE_ORDER 114 | 115 |
116 |
117 | 118 | 119 | 120 | .* 121 | 122 | .* 123 | 124 | 125 | BY_NAME 126 | 127 |
128 |
129 |
130 |
131 | 132 | 134 |
135 |
-------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.5.0](https://github.com/thepracticaldev/DEV-Android/tree/1.5.0) (2020-11-16) 4 | 5 | [Full Changelog](https://github.com/thepracticaldev/DEV-Android/compare/1.4.2...1.5.0) 6 | 7 | **Fixed bugs:** 8 | 9 | - App crashing upon trying to login [\#98](https://github.com/thepracticaldev/DEV-Android/issues/98) 10 | - App crashes immediately \(can't open\) - Android 5.0 Lollipop [\#94](https://github.com/thepracticaldev/DEV-Android/issues/94) 11 | - Add a Swipe to Refresh option [\#76](https://github.com/thepracticaldev/DEV-Android/issues/76) 12 | - Title "MY READING LIST \(EMPTY\) is clipped on the navigation menu [\#17](https://github.com/thepracticaldev/DEV-Android/issues/17) 13 | 14 | **Closed issues:** 15 | 16 | - App crashes after closing it [\#106](https://github.com/thepracticaldev/DEV-Android/issues/106) 17 | - Android app is broken [\#105](https://github.com/thepracticaldev/DEV-Android/issues/105) 18 | - Bump the project minimum SDK API [\#88](https://github.com/thepracticaldev/DEV-Android/issues/88) 19 | - Push Notification [\#10](https://github.com/thepracticaldev/DEV-Android/issues/10) 20 | 21 | **Merged pull requests:** 22 | 23 | - Removes duplicate ic\_launcher\_foreground.xml [\#114](https://github.com/thepracticaldev/DEV-Android/pull/114) ([fdoxyz](https://github.com/fdoxyz)) 24 | - .gitignore bump [\#112](https://github.com/thepracticaldev/DEV-Android/pull/112) ([fdoxyz](https://github.com/fdoxyz)) 25 | - Setup fastlane for CI/CD [\#110](https://github.com/thepracticaldev/DEV-Android/pull/110) ([maestromac](https://github.com/maestromac)) 26 | - Updates Gradle Wrapper from 6.2.1 to 6.7 [\#109](https://github.com/thepracticaldev/DEV-Android/pull/109) ([github-actions[bot]](https://github.com/apps/github-actions)) 27 | - Fix bug on MainActivity onDestro. App crashes when closing the app with back button [\#107](https://github.com/thepracticaldev/DEV-Android/pull/107) ([fdoxyz](https://github.com/fdoxyz)) 28 | - Bumped minSdkVersion to 21 [\#104](https://github.com/thepracticaldev/DEV-Android/pull/104) ([Stuie](https://github.com/Stuie)) 29 | - Add workflow for Update Gradle Wrapper Action [\#102](https://github.com/thepracticaldev/DEV-Android/pull/102) ([cristiangreco](https://github.com/cristiangreco)) 30 | - 🏗 Setup a GitHub Action using Gradle to run the unit tests \#2 [\#100](https://github.com/thepracticaldev/DEV-Android/pull/100) ([jmfayard](https://github.com/jmfayard)) 31 | 32 | ## [1.4.2](https://github.com/thepracticaldev/DEV-Android/tree/1.4.2) (2020-09-15) 33 | 34 | [Full Changelog](https://github.com/thepracticaldev/DEV-Android/compare/1.4.1-11...1.4.2) 35 | 36 | **Fixed bugs:** 37 | 38 | - Web Share API not working [\#11](https://github.com/thepracticaldev/DEV-Android/issues/11) 39 | 40 | **Merged pull requests:** 41 | 42 | - Bumps targetSdkVersion to support API 29 [\#101](https://github.com/thepracticaldev/DEV-Android/pull/101) ([fdoxyz](https://github.com/fdoxyz)) 43 | - \#94 fixed event bus related app crushing issue on Android 5.0 Lollipop [\#96](https://github.com/thepracticaldev/DEV-Android/pull/96) ([bluetoothfx](https://github.com/bluetoothfx)) 44 | - Add method to open native share sheet from within web-view. [\#61](https://github.com/thepracticaldev/DEV-Android/pull/61) ([VarunBarad](https://github.com/VarunBarad)) 45 | 46 | ## [1.4.1-11](https://github.com/thepracticaldev/DEV-Android/tree/1.4.1-11) (2020-07-21) 47 | 48 | [Full Changelog](https://github.com/thepracticaldev/DEV-Android/compare/1.4.1...1.4.1-11) 49 | 50 | **Fixed bugs:** 51 | 52 | - Can't play video on google pixel [\#86](https://github.com/thepracticaldev/DEV-Android/issues/86) 53 | 54 | **Closed issues:** 55 | 56 | - Title [\#84](https://github.com/thepracticaldev/DEV-Android/issues/84) 57 | 58 | **Merged pull requests:** 59 | 60 | - Support for video messaging back into the DOM & other enhancements [\#92](https://github.com/thepracticaldev/DEV-Android/pull/92) ([fdoxyz](https://github.com/fdoxyz)) 61 | - Native video streaming support [\#90](https://github.com/thepracticaldev/DEV-Android/pull/90) ([fdoxyz](https://github.com/fdoxyz)) 62 | - Filter incoming URL data from MainActivity intent handling [\#89](https://github.com/thepracticaldev/DEV-Android/pull/89) ([fdoxyz](https://github.com/fdoxyz)) 63 | 64 | ## [1.4.1](https://github.com/thepracticaldev/DEV-Android/tree/1.4.1) (2020-05-29) 65 | 66 | [Full Changelog](https://github.com/thepracticaldev/DEV-Android/compare/1.4...1.4.1) 67 | 68 | **Fixed bugs:** 69 | 70 | - Login via Twitter issue [\#82](https://github.com/thepracticaldev/DEV-Android/issues/82) 71 | - Image uploading still doesn't work [\#51](https://github.com/thepracticaldev/DEV-Android/issues/51) 72 | 73 | **Closed issues:** 74 | 75 | - Duplicate .gitignore files in the repository [\#78](https://github.com/thepracticaldev/DEV-Android/issues/78) 76 | 77 | **Merged pull requests:** 78 | 79 | - Includes error url to override for twitter authentication [\#83](https://github.com/thepracticaldev/DEV-Android/pull/83) ([fdoxyz](https://github.com/fdoxyz)) 80 | - Removed duplicate .gitignore files and contents merged into one root [\#80](https://github.com/thepracticaldev/DEV-Android/pull/80) ([Rec0iL99](https://github.com/Rec0iL99)) 81 | - Implemented Auto Refresh for WebView [\#77](https://github.com/thepracticaldev/DEV-Android/pull/77) ([nizarmah](https://github.com/nizarmah)) 82 | - Some cleanup and enhancements to the Native Audio implementation [\#75](https://github.com/thepracticaldev/DEV-Android/pull/75) ([fdoxyz](https://github.com/fdoxyz)) 83 | 84 | ## [1.4](https://github.com/thepracticaldev/DEV-Android/tree/1.4) (2020-05-01) 85 | 86 | [Full Changelog](https://github.com/thepracticaldev/DEV-Android/compare/1.3.1...1.4) 87 | 88 | **Fixed bugs:** 89 | 90 | - Authentication error with GitHub [\#73](https://github.com/thepracticaldev/DEV-Android/issues/73) 91 | 92 | **Merged pull requests:** 93 | 94 | - Native Audio Player [\#72](https://github.com/thepracticaldev/DEV-Android/pull/72) ([fdoxyz](https://github.com/fdoxyz)) 95 | 96 | ## [1.3.1](https://github.com/thepracticaldev/DEV-Android/tree/1.3.1) (2020-04-30) 97 | 98 | [Full Changelog](https://github.com/thepracticaldev/DEV-Android/compare/1.3...1.3.1) 99 | 100 | **Merged pull requests:** 101 | 102 | - Reverts changes made to AndroidManifest.xml [\#74](https://github.com/thepracticaldev/DEV-Android/pull/74) ([fdoxyz](https://github.com/fdoxyz)) 103 | 104 | ## [1.3](https://github.com/thepracticaldev/DEV-Android/tree/1.3) (2020-04-24) 105 | 106 | [Full Changelog](https://github.com/thepracticaldev/DEV-Android/compare/1.2.2...1.3) 107 | 108 | **Merged pull requests:** 109 | 110 | - Adds Pusher beams and code that enables push notifications handling [\#68](https://github.com/thepracticaldev/DEV-Android/pull/68) ([fdoxyz](https://github.com/fdoxyz)) 111 | 112 | ## [1.2.2](https://github.com/thepracticaldev/DEV-Android/tree/1.2.2) (2020-04-22) 113 | 114 | [Full Changelog](https://github.com/thepracticaldev/DEV-Android/compare/1.2.1...1.2.2) 115 | 116 | **Merged pull requests:** 117 | 118 | - Disable backup [\#71](https://github.com/thepracticaldev/DEV-Android/pull/71) ([maestromac](https://github.com/maestromac)) 119 | - Adds native bridge ability to handle "copy to clipboard" requests from JS [\#70](https://github.com/thepracticaldev/DEV-Android/pull/70) ([fdoxyz](https://github.com/fdoxyz)) 120 | - Add methods to implement native copy-to-clipboard functionality. [\#60](https://github.com/thepracticaldev/DEV-Android/pull/60) ([VarunBarad](https://github.com/VarunBarad)) 121 | 122 | ## [1.2.1](https://github.com/thepracticaldev/DEV-Android/tree/1.2.1) (2020-03-30) 123 | 124 | [Full Changelog](https://github.com/thepracticaldev/DEV-Android/compare/1.2...1.2.1) 125 | 126 | **Fixed bugs:** 127 | 128 | - Copy URL broken & Android share dialog [\#58](https://github.com/thepracticaldev/DEV-Android/issues/58) 129 | - Stuck on Offline page upon start [\#36](https://github.com/thepracticaldev/DEV-Android/issues/36) 130 | - Unable to upload profile image in android mobile app [\#33](https://github.com/thepracticaldev/DEV-Android/issues/33) 131 | - The App does not load user profile or articles [\#24](https://github.com/thepracticaldev/DEV-Android/issues/24) 132 | - Choose File option does not work [\#18](https://github.com/thepracticaldev/DEV-Android/issues/18) 133 | 134 | **Closed issues:** 135 | 136 | - upgrade pip 20.0.1 [\#65](https://github.com/thepracticaldev/DEV-Android/issues/65) 137 | - App looses state when external link is open or when its sent to background [\#48](https://github.com/thepracticaldev/DEV-Android/issues/48) 138 | - Adding Detekt [\#45](https://github.com/thepracticaldev/DEV-Android/issues/45) 139 | - Does Dev share API? [\#38](https://github.com/thepracticaldev/DEV-Android/issues/38) 140 | - Clear back history on tapping home link [\#35](https://github.com/thepracticaldev/DEV-Android/issues/35) 141 | - Unable to upload profile image in android mobile app [\#32](https://github.com/thepracticaldev/DEV-Android/issues/32) 142 | - Unable to upload profile image in android mobile app [\#31](https://github.com/thepracticaldev/DEV-Android/issues/31) 143 | 144 | **Merged pull requests:** 145 | 146 | - Upgrade Gradle and runtime dependencies [\#64](https://github.com/thepracticaldev/DEV-Android/pull/64) ([msfjarvis](https://github.com/msfjarvis)) 147 | - Delete removed custom gradle task `clean` from README. [\#59](https://github.com/thepracticaldev/DEV-Android/pull/59) ([VarunBarad](https://github.com/VarunBarad)) 148 | - The binding should not be passed around [\#57](https://github.com/thepracticaldev/DEV-Android/pull/57) ([sierisimo](https://github.com/sierisimo)) 149 | - Adding 'when' to substitute if [\#56](https://github.com/thepracticaldev/DEV-Android/pull/56) ([sierisimo](https://github.com/sierisimo)) 150 | - The 'clean' task is already present [\#55](https://github.com/thepracticaldev/DEV-Android/pull/55) ([sierisimo](https://github.com/sierisimo)) 151 | - Updating versions to latest [\#54](https://github.com/thepracticaldev/DEV-Android/pull/54) ([sierisimo](https://github.com/sierisimo)) 152 | - Add a bridge between WebView JavaScript and native Android code. [\#53](https://github.com/thepracticaldev/DEV-Android/pull/53) ([VarunBarad](https://github.com/VarunBarad)) 153 | - Bug/save webview state retain history [\#52](https://github.com/thepracticaldev/DEV-Android/pull/52) ([sduduzog](https://github.com/sduduzog)) 154 | - Refactor and clean-up build [\#50](https://github.com/thepracticaldev/DEV-Android/pull/50) ([jmfayard](https://github.com/jmfayard)) 155 | - Add detekt static analysis [\#47](https://github.com/thepracticaldev/DEV-Android/pull/47) ([xuhaibahmad](https://github.com/xuhaibahmad)) 156 | - Update Android Gradle plugin to 3.5.0 [\#46](https://github.com/thepracticaldev/DEV-Android/pull/46) ([friederbluemle](https://github.com/friederbluemle)) 157 | - Dependencies management with Gradle buildSrcVersions [\#44](https://github.com/thepracticaldev/DEV-Android/pull/44) ([jmfayard](https://github.com/jmfayard)) 158 | - Uprev AGP from 3.4.1 to 3.4.2 [\#43](https://github.com/thepracticaldev/DEV-Android/pull/43) ([msfjarvis](https://github.com/msfjarvis)) 159 | - Dependency upgrades [\#42](https://github.com/thepracticaldev/DEV-Android/pull/42) ([msfjarvis](https://github.com/msfjarvis)) 160 | - Migrate to Gradle Kotlin DSL [\#41](https://github.com/thepracticaldev/DEV-Android/pull/41) ([dbaelz](https://github.com/dbaelz)) 161 | - Moving code into JDK8 [\#40](https://github.com/thepracticaldev/DEV-Android/pull/40) ([sierisimo](https://github.com/sierisimo)) 162 | - Clear history when returning to home [\#39](https://github.com/thepracticaldev/DEV-Android/pull/39) ([robertoissc](https://github.com/robertoissc)) 163 | - fix: Fixes issue to launch file chooser every time [\#34](https://github.com/thepracticaldev/DEV-Android/pull/34) ([subbramanil](https://github.com/subbramanil)) 164 | 165 | ## [1.2](https://github.com/thepracticaldev/DEV-Android/tree/1.2) (2019-05-30) 166 | 167 | [Full Changelog](https://github.com/thepracticaldev/DEV-Android/compare/1.1...1.2) 168 | 169 | **Closed issues:** 170 | 171 | - Image Upload Tool Not Working [\#29](https://github.com/thepracticaldev/DEV-Android/issues/29) 172 | - Moving `main url` out of `strings.xml` [\#20](https://github.com/thepracticaldev/DEV-Android/issues/20) 173 | 174 | **Merged pull requests:** 175 | 176 | - Fix Android lint warnings [\#30](https://github.com/thepracticaldev/DEV-Android/pull/30) ([robertoissc](https://github.com/robertoissc)) 177 | - Codestyle and dependency upgrades [\#28](https://github.com/thepracticaldev/DEV-Android/pull/28) ([msfjarvis](https://github.com/msfjarvis)) 178 | - Feature/Moving `main url` out of `strings.xml` [\#27](https://github.com/thepracticaldev/DEV-Android/pull/27) ([VarunBarad](https://github.com/VarunBarad)) 179 | - fix: Choose File Option Not Working Issue [\#26](https://github.com/thepracticaldev/DEV-Android/pull/26) ([subbramanil](https://github.com/subbramanil)) 180 | - Codestyle cleanup [\#22](https://github.com/thepracticaldev/DEV-Android/pull/22) ([msfjarvis](https://github.com/msfjarvis)) 181 | 182 | ## [1.1](https://github.com/thepracticaldev/DEV-Android/tree/1.1) (2019-04-30) 183 | 184 | [Full Changelog](https://github.com/thepracticaldev/DEV-Android/compare/1.0...1.1) 185 | 186 | **Fixed bugs:** 187 | 188 | - Update Adaptive Icon [\#13](https://github.com/thepracticaldev/DEV-Android/issues/13) 189 | - Signing in with Firefox as the default browser does not work [\#12](https://github.com/thepracticaldev/DEV-Android/issues/12) 190 | 191 | **Merged pull requests:** 192 | 193 | - Fix sign in issue [\#21](https://github.com/thepracticaldev/DEV-Android/pull/21) ([maestromac](https://github.com/maestromac)) 194 | - Set adaptive icon background to black and update readme [\#19](https://github.com/thepracticaldev/DEV-Android/pull/19) ([jhw866](https://github.com/jhw866)) 195 | - Update dependencies [\#16](https://github.com/thepracticaldev/DEV-Android/pull/16) ([msfjarvis](https://github.com/msfjarvis)) 196 | - Refactored to AndroidX [\#15](https://github.com/thepracticaldev/DEV-Android/pull/15) ([tujson](https://github.com/tujson)) 197 | - Update README [\#9](https://github.com/thepracticaldev/DEV-Android/pull/9) ([maestromac](https://github.com/maestromac)) 198 | - Code of Conduct [\#8](https://github.com/thepracticaldev/DEV-Android/pull/8) ([jessleenyc](https://github.com/jessleenyc)) 199 | 200 | ## [1.0](https://github.com/thepracticaldev/DEV-Android/tree/1.0) (2019-04-18) 201 | 202 | [Full Changelog](https://github.com/thepracticaldev/DEV-Android/compare/c5252691c5d8befa7555399e7dbec2ec36209db3...1.0) 203 | 204 | **Closed issues:** 205 | 206 | - Interested to contribute on the following project [\#6](https://github.com/thepracticaldev/DEV-Android/issues/6) 207 | - Chrome share only shares Dev.to [\#5](https://github.com/thepracticaldev/DEV-Android/issues/5) 208 | 209 | **Merged pull requests:** 210 | 211 | - Create base architecture [\#7](https://github.com/thepracticaldev/DEV-Android/pull/7) ([luchfilip](https://github.com/luchfilip)) 212 | 213 | 214 | 215 | \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* 216 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at yo@dev.to. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 6 | 7 | # gem "rails" 8 | 9 | gem "fastlane", "~> 2.166" 10 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.2) 5 | addressable (2.8.0) 6 | public_suffix (>= 2.0.2, < 5.0) 7 | atomos (0.1.3) 8 | aws-eventstream (1.1.0) 9 | aws-partitions (1.391.0) 10 | aws-sdk-core (3.109.2) 11 | aws-eventstream (~> 1, >= 1.0.2) 12 | aws-partitions (~> 1, >= 1.239.0) 13 | aws-sigv4 (~> 1.1) 14 | jmespath (~> 1.0) 15 | aws-sdk-kms (1.39.0) 16 | aws-sdk-core (~> 3, >= 3.109.0) 17 | aws-sigv4 (~> 1.1) 18 | aws-sdk-s3 (1.84.1) 19 | aws-sdk-core (~> 3, >= 3.109.0) 20 | aws-sdk-kms (~> 1) 21 | aws-sigv4 (~> 1.1) 22 | aws-sigv4 (1.2.2) 23 | aws-eventstream (~> 1, >= 1.0.2) 24 | babosa (1.0.4) 25 | claide (1.0.3) 26 | colored (1.2) 27 | colored2 (3.1.2) 28 | commander-fastlane (4.4.6) 29 | highline (~> 1.7.2) 30 | declarative (0.0.20) 31 | declarative-option (0.1.0) 32 | digest-crc (0.6.1) 33 | rake (~> 13.0) 34 | domain_name (0.5.20190701) 35 | unf (>= 0.0.5, < 1.0.0) 36 | dotenv (2.7.6) 37 | emoji_regex (3.2.1) 38 | excon (0.78.0) 39 | faraday (1.1.0) 40 | multipart-post (>= 1.2, < 3) 41 | ruby2_keywords 42 | faraday-cookie_jar (0.0.7) 43 | faraday (>= 0.8.0) 44 | http-cookie (~> 1.0.0) 45 | faraday_middleware (1.0.0) 46 | faraday (~> 1.0) 47 | fastimage (2.2.0) 48 | fastlane (2.166.0) 49 | CFPropertyList (>= 2.3, < 4.0.0) 50 | addressable (>= 2.3, < 3.0.0) 51 | aws-sdk-s3 (~> 1.0) 52 | babosa (>= 1.0.3, < 2.0.0) 53 | bundler (>= 1.12.0, < 3.0.0) 54 | colored 55 | commander-fastlane (>= 4.4.6, < 5.0.0) 56 | dotenv (>= 2.1.1, < 3.0.0) 57 | emoji_regex (>= 0.1, < 4.0) 58 | excon (>= 0.71.0, < 1.0.0) 59 | faraday (~> 1.0) 60 | faraday-cookie_jar (~> 0.0.6) 61 | faraday_middleware (~> 1.0) 62 | fastimage (>= 2.1.0, < 3.0.0) 63 | gh_inspector (>= 1.1.2, < 2.0.0) 64 | google-api-client (>= 0.37.0, < 0.39.0) 65 | google-cloud-storage (>= 1.15.0, < 2.0.0) 66 | highline (>= 1.7.2, < 2.0.0) 67 | json (< 3.0.0) 68 | jwt (>= 2.1.0, < 3) 69 | mini_magick (>= 4.9.4, < 5.0.0) 70 | multipart-post (~> 2.0.0) 71 | plist (>= 3.1.0, < 4.0.0) 72 | rubyzip (>= 2.0.0, < 3.0.0) 73 | security (= 0.1.3) 74 | simctl (~> 1.6.3) 75 | slack-notifier (>= 2.0.0, < 3.0.0) 76 | terminal-notifier (>= 2.0.0, < 3.0.0) 77 | terminal-table (>= 1.4.5, < 2.0.0) 78 | tty-screen (>= 0.6.3, < 1.0.0) 79 | tty-spinner (>= 0.8.0, < 1.0.0) 80 | word_wrap (~> 1.0.0) 81 | xcodeproj (>= 1.13.0, < 2.0.0) 82 | xcpretty (~> 0.3.0) 83 | xcpretty-travis-formatter (>= 0.0.3) 84 | gh_inspector (1.1.3) 85 | google-api-client (0.38.0) 86 | addressable (~> 2.5, >= 2.5.1) 87 | googleauth (~> 0.9) 88 | httpclient (>= 2.8.1, < 3.0) 89 | mini_mime (~> 1.0) 90 | representable (~> 3.0) 91 | retriable (>= 2.0, < 4.0) 92 | signet (~> 0.12) 93 | google-cloud-core (1.5.0) 94 | google-cloud-env (~> 1.0) 95 | google-cloud-errors (~> 1.0) 96 | google-cloud-env (1.4.0) 97 | faraday (>= 0.17.3, < 2.0) 98 | google-cloud-errors (1.0.1) 99 | google-cloud-storage (1.29.1) 100 | addressable (~> 2.5) 101 | digest-crc (~> 0.4) 102 | google-api-client (~> 0.33) 103 | google-cloud-core (~> 1.2) 104 | googleauth (~> 0.9) 105 | mini_mime (~> 1.0) 106 | googleauth (0.14.0) 107 | faraday (>= 0.17.3, < 2.0) 108 | jwt (>= 1.4, < 3.0) 109 | memoist (~> 0.16) 110 | multi_json (~> 1.11) 111 | os (>= 0.9, < 2.0) 112 | signet (~> 0.14) 113 | highline (1.7.10) 114 | http-cookie (1.0.3) 115 | domain_name (~> 0.5) 116 | httpclient (2.8.3) 117 | jmespath (1.4.0) 118 | json (2.3.1) 119 | jwt (2.2.2) 120 | memoist (0.16.2) 121 | mini_magick (4.11.0) 122 | mini_mime (1.0.2) 123 | multi_json (1.15.0) 124 | multipart-post (2.0.0) 125 | nanaimo (0.3.0) 126 | naturally (2.2.0) 127 | os (1.1.1) 128 | plist (3.5.0) 129 | public_suffix (4.0.6) 130 | rake (13.0.1) 131 | representable (3.0.4) 132 | declarative (< 0.1.0) 133 | declarative-option (< 0.2.0) 134 | uber (< 0.2.0) 135 | retriable (3.1.2) 136 | rouge (2.0.7) 137 | ruby2_keywords (0.0.2) 138 | rubyzip (2.3.0) 139 | security (0.1.3) 140 | signet (0.14.0) 141 | addressable (~> 2.3) 142 | faraday (>= 0.17.3, < 2.0) 143 | jwt (>= 1.5, < 3.0) 144 | multi_json (~> 1.10) 145 | simctl (1.6.8) 146 | CFPropertyList 147 | naturally 148 | slack-notifier (2.3.2) 149 | terminal-notifier (2.0.0) 150 | terminal-table (1.8.0) 151 | unicode-display_width (~> 1.1, >= 1.1.1) 152 | tty-cursor (0.7.1) 153 | tty-screen (0.8.1) 154 | tty-spinner (0.9.3) 155 | tty-cursor (~> 0.7) 156 | uber (0.1.0) 157 | unf (0.1.4) 158 | unf_ext 159 | unf_ext (0.0.7.7) 160 | unicode-display_width (1.7.0) 161 | word_wrap (1.0.0) 162 | xcodeproj (1.19.0) 163 | CFPropertyList (>= 2.3.3, < 4.0) 164 | atomos (~> 0.1.3) 165 | claide (>= 1.0.2, < 2.0) 166 | colored2 (~> 3.1) 167 | nanaimo (~> 0.3.0) 168 | xcpretty (0.3.0) 169 | rouge (~> 2.0.7) 170 | xcpretty-travis-formatter (1.0.0) 171 | xcpretty (~> 0.2, >= 0.0.7) 172 | 173 | PLATFORMS 174 | ruby 175 | 176 | DEPENDENCIES 177 | fastlane (~> 2.166) 178 | 179 | BUNDLED WITH 180 | 2.1.4 181 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEV Android 💝 2 | 3 | 4 | 5 | This is the official repository for the [dev.to](https://dev.to/)'s Android app. 6 | 7 | Get it on Google Play 8 | 9 | 10 | ## Design ethos 11 | 12 | DEV Android is an [WebView](https://developer.android.com/guide/webapps/webview) based application. This application is inspired by [Basecamp's approach](https://m.signalvnoise.com/basecamp-3-for-ios-hybrid-architecture-afc071589c25). We will grow to include more native code over time. 13 | 14 | By leveraging webviews as much as possible, we can smoothly sync up with our web dev work. And where it makes sense, we can re-implement certain things fully native, or build entirely native features. Life's a journey, not a destination. 15 | 16 | ## Contributions 17 | 18 | We expect contributors to abide by our underlying [code of conduct](./CODE_OF_CONDUCT.md). All conversations and discussions on GitHub (issues, pull requests) and across dev.to must be respectful and harassment-free. 19 | 20 | ### System Requirements 21 | 22 | You will need to have Android Studio 3.5 or up installed. 23 | 24 | ### Usage 25 | 26 | ```bash 27 | $ ./gradlew tasks --group=custom 28 | 29 | ------------------------------------------------------------ 30 | Tasks runnable from root project 31 | ------------------------------------------------------------ 32 | 33 | Custom tasks 34 | ------------ 35 | androidTest - Run android instrumentation tests 36 | hello - Hello World task - useful to solve build problems 37 | install - Build and install the app 38 | test - Run the unit tests 39 | 40 | To see all tasks and more detail, run gradlew tasks --all 41 | 42 | To see more detail about a task, run gradlew help --task 43 | 44 | ``` 45 | 46 | ### Push Notifications 47 | 48 | For Push Notification delivery we use [Pusher Beams](https://pusher.com/beams). In order to get the app running locally you'll need a `google-services.json` configuration file from Firebase, otherwise you'll encounter the following error: `File google-services.json is missing. The Google Services Plugin cannot function without it.` 49 | 50 | You can [sign up or sign in on Firebase](https://firebase.google.com/) account for free in order to get the app working locally. Steps 1-4 under **Firebase for Android Push Notifications** in our [official docs](https://docs.dev.to/backend/pusher/#mobile-push-notifications) show how to set this up in more detail. Drop the resulting `google-services.json` file in the `app` folder and you'll be good to go. 51 | 52 | ### How to contribute 53 | 54 | 1. Fork the project & clone locally. 55 | 1. Create a branch, naming it either a feature or bug: `git checkout -b feature/that-new-feature` or `bug/fixing-that-bug` 56 | 1. Code and commit your changes. Bonus points if you write a [good commit message](https://chris.beams.io/posts/git-commit/): `git commit -m 'Add some feature'` 57 | 1. Push to the branch: `git push origin feature/that-new-feature` 58 | 1. Create a pull request for your branch 🎉 59 | 60 | ## License 61 | 62 | This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Please see the [LICENSE](./LICENSE) file in our repository for the full text. 63 | 64 | Like many open source projects, we require that contributors provide us with a Contributor License Agreement (CLA). By submitting code to the DEV project, you are granting us a right to use that code under the terms of the CLA. 65 | 66 | Our version of the CLA was adapted from the Microsoft Contributor License Agreement, which they generously made available to the public domain under Creative Commons CC0 1.0 Universal. 67 | 68 | Any questions, please refer to our [license FAQ](https://docs.dev.to/licensing/) doc or email yo@dev.to 69 | 70 |
71 | 72 |

73 | sloan 78 |
79 | Happy Coding ❤️ 80 |

81 | -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import java.util.Properties 2 | import java.io.FileInputStream 3 | 4 | plugins { 5 | id("com.android.application") 6 | kotlin("android") 7 | kotlin("android.extensions") 8 | kotlin("kapt") 9 | } 10 | 11 | val keystorePropertiesFile = rootProject.file("keystore.properties") 12 | val keystoreProperties = Properties() 13 | if (keystorePropertiesFile.exists()) { 14 | keystoreProperties.load(FileInputStream(keystorePropertiesFile)) 15 | } 16 | 17 | android { 18 | configureAndroid(this) 19 | configureBuildConfig(this) 20 | signingConfigs { 21 | create("release") { 22 | keystoreProperties["storeFile"]?.let { 23 | keyAlias = keystoreProperties["keyAlias"].toString() 24 | keyPassword = keystoreProperties["keyPassword"].toString() 25 | storeFile = file(it) 26 | storePassword = keystoreProperties["storePassword"].toString() 27 | } 28 | } 29 | } 30 | 31 | buildTypes { 32 | getByName("release") { 33 | isMinifyEnabled = true 34 | proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro") 35 | signingConfig = signingConfigs.getByName("release") 36 | } 37 | getByName("debug") { 38 | applicationIdSuffix = ".debug" 39 | isDebuggable = true 40 | } 41 | } 42 | dataBinding { 43 | isEnabled = true 44 | } 45 | } 46 | 47 | dependencies { 48 | implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar")))) 49 | 50 | implementation(Libs.multidex) 51 | 52 | implementation(Libs.kotlin_stdlib_jdk8) 53 | implementation(Libs.browser) 54 | 55 | implementation(Libs.kotlinx_coroutines_core) 56 | implementation(Libs.kotlinx_coroutines_android) 57 | 58 | implementation(Libs.eventbus) 59 | kapt(Libs.eventbus_annotation_processor) 60 | 61 | implementation(Libs.exoplayer_core) 62 | implementation(Libs.exoplayer_ui) 63 | implementation(Libs.exoplayer_hls) 64 | implementation(Libs.extension_mediasession) 65 | implementation(Libs.firebase_messaging) 66 | implementation(Libs.push_notifications_android) 67 | implementation(Libs.gson) 68 | 69 | api(Libs.appcompat) 70 | api(Libs.constraintlayout) 71 | api(Libs.lifecycle_extensions) 72 | api(Libs.lifecycle_viewmodel) 73 | 74 | testImplementation(Libs.junit) 75 | androidTestImplementation(Libs.androidx_test_runner) 76 | androidTestImplementation(Libs.espresso_core) 77 | } 78 | 79 | kapt { 80 | 81 | arguments { 82 | arg("eventBusIndex", "to.dev.dev_android.webclients.EventBusClientIndex") 83 | } 84 | } 85 | 86 | apply(plugin = "com.google.gms.google-services") 87 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/to/dev/dev_android/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package to.dev.dev_android 2 | 3 | import androidx.test.InstrumentationRegistry 4 | import androidx.test.runner.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getTargetContext() 22 | assertEquals("to.dev.dev_android", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/debug/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | DEV (Debug) 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 19 | 22 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forem/DEV-Android/2da5d61a846a16e009acbb10126a25c2775baf9f/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /app/src/main/java/to/dev/dev_android/activities/BaseActivity.kt: -------------------------------------------------------------------------------- 1 | package to.dev.dev_android.activities 2 | 3 | import android.os.Bundle 4 | import androidx.annotation.LayoutRes 5 | import androidx.appcompat.app.AppCompatActivity 6 | import androidx.databinding.DataBindingUtil 7 | import androidx.databinding.ViewDataBinding 8 | 9 | abstract class BaseActivity : AppCompatActivity() { 10 | 11 | lateinit var binding: B 12 | 13 | @LayoutRes 14 | protected abstract fun layout(): Int 15 | 16 | override fun onCreate(savedInstanceState: Bundle?) { 17 | super.onCreate(savedInstanceState) 18 | binding = DataBindingUtil.setContentView(this, layout()) 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /app/src/main/java/to/dev/dev_android/activities/ForemAppDialog.kt: -------------------------------------------------------------------------------- 1 | package to.dev.dev_android.activities 2 | 3 | import android.app.Activity 4 | import android.content.ActivityNotFoundException 5 | import android.content.Intent 6 | import android.net.Uri 7 | import android.os.Bundle 8 | import android.view.LayoutInflater 9 | import android.view.View 10 | import android.view.ViewGroup 11 | import androidx.constraintlayout.widget.ConstraintLayout 12 | import androidx.fragment.app.DialogFragment 13 | import android.content.pm.PackageManager 14 | import android.widget.ImageView 15 | import android.widget.TextView 16 | import androidx.core.content.ContextCompat 17 | import to.dev.dev_android.R 18 | 19 | class ForemAppDialog : DialogFragment() { 20 | 21 | companion object { 22 | const val PACKAGE_NAME = "com.forem.android" 23 | private const val FOREM_URL = "ForemAppDialog.url" 24 | 25 | fun newInstance(url: String): ForemAppDialog { 26 | val foremAppDialog = ForemAppDialog() 27 | val args = Bundle() 28 | args.putString(FOREM_URL, url) 29 | foremAppDialog.arguments = args 30 | return foremAppDialog 31 | } 32 | 33 | fun isForemAppAlreadyInstalled(activity: Activity?): Boolean { 34 | return try { 35 | activity?.packageManager?.getPackageInfo(PACKAGE_NAME, 0) 36 | true 37 | } catch (e: PackageManager.NameNotFoundException) { 38 | false 39 | } 40 | } 41 | 42 | fun openForemApp(activity: Activity?, url: String?) { 43 | val packageManager: PackageManager? = activity?.packageManager 44 | val app = packageManager?.getLaunchIntentForPackage(PACKAGE_NAME) 45 | if (!url.isNullOrEmpty()) { 46 | app?.putExtra(Intent.EXTRA_TEXT, url) 47 | } 48 | activity?.startActivity(app) 49 | } 50 | } 51 | 52 | lateinit var url: String 53 | 54 | override fun onStart() { 55 | super.onStart() 56 | val width = resources.displayMetrics.widthPixels 57 | dialog!!.window?.setLayout(width, ViewGroup.LayoutParams.WRAP_CONTENT) 58 | } 59 | 60 | override fun onCreateView( 61 | inflater: LayoutInflater, 62 | container: ViewGroup?, 63 | savedInstanceState: Bundle? 64 | ): View? { 65 | val args = arguments 66 | url = args?.getString(FOREM_URL) ?: "" 67 | 68 | val view = inflater.inflate(R.layout.forem_app_dialog, container, false) 69 | if (dialog != null && dialog!!.window != null) { 70 | dialog!!.window?.setBackgroundDrawableResource(R.drawable.forem_dialog_fragment_background) 71 | } 72 | val downloadInstallForemAppTextView = 73 | view.findViewById(R.id.download_install_forem_app_text_view) 74 | val downloadOpenForemAppImageView = 75 | view.findViewById(R.id.download_open_forem_image_view) 76 | val descriptionTextView = 77 | view.findViewById(R.id.forem_app_dialog_description_text_view) 78 | 79 | if (isForemAppAlreadyInstalled(activity)) { 80 | downloadInstallForemAppTextView.text = getString(R.string.open_forem_app) 81 | downloadOpenForemAppImageView.setImageDrawable( 82 | ContextCompat.getDrawable( 83 | this.requireContext(), 84 | R.drawable.ic_compass 85 | ) 86 | ) 87 | descriptionTextView.text = getString(R.string.forem_app_dialog_description_if_installed) 88 | } else { 89 | downloadInstallForemAppTextView.text = getString(R.string.download_forem_app) 90 | downloadOpenForemAppImageView.setImageDrawable( 91 | ContextCompat.getDrawable( 92 | this.requireContext(), 93 | R.drawable.ic_baseline_arrow_downward_24 94 | ) 95 | ) 96 | descriptionTextView.text = getString(R.string.forem_app_dialog_description) 97 | } 98 | 99 | val downloadLayout = view.findViewById(R.id.download_forem_app_layout) 100 | 101 | downloadLayout.setOnClickListener { 102 | openForemAppLink() 103 | } 104 | return view 105 | } 106 | 107 | private fun openForemAppLink() { 108 | if (isForemAppAlreadyInstalled(activity)) { 109 | openForemApp(activity, url) 110 | } else { 111 | try { 112 | // Opens Forem app in Play Store, if Play Store app is available. 113 | activity?.startActivity( 114 | Intent( 115 | Intent.ACTION_VIEW, 116 | Uri.parse("market://details?id=$PACKAGE_NAME") 117 | ) 118 | ) 119 | } catch (e: ActivityNotFoundException) { 120 | // Opens Forem app on Play Store in browser. 121 | activity?.startActivity( 122 | Intent( 123 | Intent.ACTION_VIEW, 124 | Uri.parse("https://play.google.com/store/apps/details?id=$PACKAGE_NAME") 125 | ) 126 | ) 127 | } 128 | } 129 | this.dismiss() 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /app/src/main/java/to/dev/dev_android/activities/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package to.dev.dev_android.activities 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.Activity 5 | import android.content.Intent 6 | import android.net.Uri 7 | import android.os.Build 8 | import android.os.Bundle 9 | import android.util.Log 10 | import android.view.View 11 | import android.webkit.ValueCallback 12 | import android.webkit.WebView 13 | import com.pusher.pushnotifications.PushNotifications 14 | import kotlinx.coroutines.MainScope 15 | import kotlinx.coroutines.cancel 16 | import to.dev.dev_android.R 17 | import to.dev.dev_android.BuildConfig 18 | import to.dev.dev_android.databinding.ActivityMainBinding 19 | import to.dev.dev_android.util.AndroidWebViewBridge 20 | import to.dev.dev_android.webclients.CustomWebChromeClient 21 | import to.dev.dev_android.webclients.CustomWebViewClient 22 | 23 | class MainActivity : BaseActivity(), CustomWebChromeClient.CustomListener { 24 | private val webViewBridge: AndroidWebViewBridge = AndroidWebViewBridge(this) 25 | private lateinit var webViewClient: CustomWebViewClient 26 | 27 | private var filePathCallback: ValueCallback>? = null 28 | 29 | private val mainActivityScope = MainScope() 30 | 31 | override fun layout(): Int { 32 | return R.layout.activity_main 33 | } 34 | 35 | override fun onCreate(savedInstanceState: Bundle?) { 36 | super.onCreate(savedInstanceState) 37 | setWebViewSettings() 38 | savedInstanceState?.let { restoreState(it) } ?: navigateToHome() 39 | handleIntent(intent) 40 | PushNotifications.start(applicationContext, BuildConfig.pusherInstanceId) 41 | PushNotifications.addDeviceInterest("broadcast") 42 | 43 | binding.showPopupImageView.setOnClickListener { 44 | showForemAppAlert() 45 | } 46 | 47 | binding.openForemImageView.setOnClickListener { 48 | val url = binding.webView.url 49 | ForemAppDialog.openForemApp(this, url) 50 | } 51 | } 52 | 53 | override fun onResume() { 54 | if (intent.extras != null && intent.extras!!["url"] != null) { 55 | val targetUrl = intent.extras!!["url"].toString() 56 | try { 57 | val targetHost = Uri.parse(targetUrl).host ?: "" 58 | if (targetHost.contains(BuildConfig.baseHostname)) { 59 | binding.webView.loadUrl(targetUrl) 60 | } 61 | } catch (e: Exception) { 62 | Log.e(LOG_TAG, "${e.message}") 63 | } 64 | } 65 | 66 | if (ForemAppDialog.isForemAppAlreadyInstalled(this)) { 67 | binding.openForemImageView.visibility = View.VISIBLE 68 | } else { 69 | binding.openForemImageView.visibility = View.GONE 70 | } 71 | 72 | super.onResume() 73 | webViewClient.observeNetwork() 74 | } 75 | 76 | override fun onStop() { 77 | super.onStop() 78 | webViewClient.unobserveNetwork() 79 | } 80 | 81 | override fun onDestroy() { 82 | super.onDestroy() 83 | 84 | // Make sure we're not leaving any audio playing behind 85 | webViewBridge.terminatePodcast() 86 | 87 | // Coroutine cleanup 88 | mainActivityScope.cancel() 89 | } 90 | 91 | override fun onSaveInstanceState(outState: Bundle) { 92 | binding.webView.saveState(outState) 93 | super.onSaveInstanceState(outState) 94 | } 95 | 96 | override fun onNewIntent(intent: Intent) { 97 | super.onNewIntent(intent) 98 | handleIntent(intent) 99 | } 100 | 101 | private fun handleIntent(intent: Intent) { 102 | val appLinkData: Uri? = intent.data 103 | appLinkData?.host?.let { 104 | binding.webView.loadUrl(appLinkData.toString()) 105 | } 106 | } 107 | 108 | @SuppressLint("SetJavaScriptEnabled") 109 | private fun setWebViewSettings() { 110 | if (BuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 111 | WebView.setWebContentsDebuggingEnabled(true) 112 | } 113 | 114 | binding.webView.settings.javaScriptEnabled = true 115 | binding.webView.settings.domStorageEnabled = true 116 | binding.webView.settings.userAgentString = BuildConfig.userAgent 117 | 118 | binding.webView.addJavascriptInterface(webViewBridge, "AndroidBridge") 119 | webViewClient = CustomWebViewClient( 120 | this@MainActivity, 121 | binding.webView, 122 | mainActivityScope 123 | ) { 124 | binding.splash.visibility = View.GONE 125 | binding.webView.visibility = View.VISIBLE 126 | binding.bottomLayout.visibility = View.VISIBLE 127 | showForemAppAlert() 128 | } 129 | binding.webView.webViewClient = webViewClient 130 | webViewBridge.webViewClient = webViewClient 131 | binding.webView.webChromeClient = CustomWebChromeClient(BuildConfig.baseUrl, this) 132 | } 133 | 134 | private fun showForemAppAlert() { 135 | val url: String = binding.webView.url ?: "" 136 | ForemAppDialog.newInstance(url).show( 137 | supportFragmentManager, 138 | "ForemAppDialogFragment" 139 | ) 140 | } 141 | 142 | private fun restoreState(savedInstanceState: Bundle) { 143 | binding.webView.restoreState(savedInstanceState) 144 | } 145 | 146 | private fun navigateToHome() { 147 | binding.webView.loadUrl(BuildConfig.baseUrl) 148 | } 149 | 150 | override fun onBackPressed() { 151 | if (binding.webView.canGoBack()) { 152 | binding.webView.goBack() 153 | } else { 154 | super.onBackPressed() 155 | } 156 | } 157 | 158 | override fun launchGallery(filePathCallback: ValueCallback>?) { 159 | this.filePathCallback = filePathCallback 160 | 161 | val galleryIntent = Intent().apply { 162 | // Show only images, no videos or anything else 163 | type = "image/*" 164 | action = Intent.ACTION_PICK 165 | } 166 | 167 | // Always show the chooser (if there are multiple options available) 168 | startActivityForResult( 169 | Intent.createChooser(galleryIntent, "Select Picture"), 170 | PIC_CHOOSER_REQUEST, 171 | null // No additional data 172 | ) 173 | } 174 | 175 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { 176 | if (requestCode != PIC_CHOOSER_REQUEST) { 177 | return super.onActivityResult(requestCode, resultCode, data) 178 | } 179 | 180 | when (resultCode) { 181 | Activity.RESULT_OK -> data?.data?.let { 182 | filePathCallback?.onReceiveValue(arrayOf(it)) 183 | filePathCallback = null 184 | } 185 | Activity.RESULT_CANCELED -> { 186 | filePathCallback?.onReceiveValue(null) 187 | filePathCallback = null 188 | } 189 | } 190 | } 191 | 192 | companion object { 193 | private const val PIC_CHOOSER_REQUEST = 100 194 | private const val LOG_TAG = "MainActivity" 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /app/src/main/java/to/dev/dev_android/activities/StarterApplication.kt: -------------------------------------------------------------------------------- 1 | package to.dev.dev_android.activities 2 | 3 | import androidx.multidex.MultiDexApplication 4 | import org.greenrobot.eventbus.EventBus 5 | import to.dev.dev_android.webclients.EventBusClientIndex 6 | 7 | 8 | class StarterApplication : MultiDexApplication() { 9 | 10 | override fun onCreate() { 11 | super.onCreate() 12 | EventBus.builder().addIndex(EventBusClientIndex()).build() 13 | EventBus.builder().addIndex(EventBusClientIndex()).installDefaultEventBus() 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /app/src/main/java/to/dev/dev_android/activities/VideoPlayerActivity.kt: -------------------------------------------------------------------------------- 1 | package to.dev.dev_android.activities 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.net.Uri 6 | import android.os.Bundle 7 | import androidx.annotation.MainThread 8 | import com.google.android.exoplayer2.SimpleExoPlayer 9 | import com.google.android.exoplayer2.source.hls.HlsMediaSource 10 | import com.google.android.exoplayer2.upstream.DataSource 11 | import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory 12 | import org.greenrobot.eventbus.EventBus 13 | import to.dev.dev_android.BuildConfig 14 | import to.dev.dev_android.R 15 | import to.dev.dev_android.databinding.ActivityVideoPlayerBinding 16 | import to.dev.dev_android.events.VideoPlayerPauseEvent 17 | import to.dev.dev_android.events.VideoPlayerTickEvent 18 | import to.dev.dev_android.webclients.CustomWebViewClient 19 | import java.util.* 20 | 21 | class VideoPlayerActivity : BaseActivity() { 22 | 23 | private var player: SimpleExoPlayer? = null 24 | private val timer = Timer() 25 | 26 | override fun layout(): Int { 27 | return R.layout.activity_video_player 28 | } 29 | 30 | override fun onCreate(savedInstanceState: Bundle?) { 31 | super.onCreate(savedInstanceState) 32 | 33 | val videoUrl = intent.getStringExtra(argVideoUrl) 34 | val videoTime = intent.getStringExtra(argVideoTime) 35 | 36 | val streamUri= Uri.parse(videoUrl) 37 | val dataSourceFactory: DataSource.Factory = DefaultHttpDataSourceFactory(BuildConfig.userAgent) 38 | val mediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource(streamUri) 39 | 40 | player = SimpleExoPlayer.Builder(this).build() 41 | binding.playerView.player = player 42 | player?.prepare(mediaSource) 43 | player?.seekTo(videoTime.toLong() * 1000) 44 | player?.playWhenReady = true 45 | 46 | val timeUpdateTask = object: TimerTask() { 47 | override fun run() { 48 | timeUpdate() 49 | } 50 | } 51 | timer.schedule(timeUpdateTask, 0, 1000) 52 | } 53 | 54 | override fun onDestroy() { 55 | player?.playWhenReady = false 56 | timer.cancel() 57 | EventBus.getDefault().post(VideoPlayerPauseEvent()) 58 | super.onDestroy() 59 | } 60 | 61 | fun timeUpdate() { 62 | val milliseconds = (player?.currentPosition ?: 0) 63 | val currentTime = (milliseconds / 1000).toString() 64 | EventBus.getDefault().post(VideoPlayerTickEvent(currentTime)) 65 | } 66 | 67 | companion object { 68 | @MainThread 69 | fun newIntent( 70 | context: Context, 71 | url: String, 72 | time: String 73 | ) = Intent(context, VideoPlayerActivity::class.java).apply { 74 | putExtra(argVideoUrl, url) 75 | putExtra(argVideoTime, time) 76 | } 77 | 78 | const val argVideoUrl = "ARG_VIDEO_URL" 79 | const val argVideoTime = "ARG_VIDEO_TIME" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /app/src/main/java/to/dev/dev_android/events/NetworkStatusEvent.kt: -------------------------------------------------------------------------------- 1 | package to.dev.dev_android.events 2 | 3 | import to.dev.dev_android.util.network.NetworkStatus 4 | 5 | class NetworkStatusEvent(val networkStatus: NetworkStatus) -------------------------------------------------------------------------------- /app/src/main/java/to/dev/dev_android/events/VideoPlayerPauseEvent.kt: -------------------------------------------------------------------------------- 1 | package to.dev.dev_android.events 2 | 3 | class VideoPlayerPauseEvent { 4 | val action = "pause" 5 | } -------------------------------------------------------------------------------- /app/src/main/java/to/dev/dev_android/events/VideoPlayerTickEvent.kt: -------------------------------------------------------------------------------- 1 | package to.dev.dev_android.events 2 | 3 | class VideoPlayerTickEvent(val seconds: String) { 4 | val action = "tick" 5 | } -------------------------------------------------------------------------------- /app/src/main/java/to/dev/dev_android/media/AudioService.kt: -------------------------------------------------------------------------------- 1 | package to.dev.dev_android.media 2 | 3 | import android.app.Notification 4 | import android.app.PendingIntent 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.graphics.Bitmap 8 | import android.media.MediaMetadata 9 | import android.net.Uri 10 | import android.os.Binder 11 | import android.os.Build 12 | import android.os.IBinder 13 | import android.support.v4.media.MediaMetadataCompat 14 | import android.support.v4.media.session.MediaSessionCompat 15 | import androidx.annotation.MainThread 16 | import androidx.annotation.Nullable 17 | import androidx.lifecycle.LifecycleService 18 | import com.google.android.exoplayer2.* 19 | import com.google.android.exoplayer2.audio.AudioAttributes 20 | import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory 21 | import com.google.android.exoplayer2.source.ProgressiveMediaSource 22 | import com.google.android.exoplayer2.ui.PlayerNotificationManager 23 | import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory 24 | import to.dev.dev_android.R 25 | import to.dev.dev_android.BuildConfig 26 | 27 | class AudioService : LifecycleService() { 28 | private val binder = AudioServiceBinder() 29 | 30 | private var currentPodcastUrl: String? = null 31 | private var episodeName: String? = null 32 | private var podcastName: String? = null 33 | private var imageUrl: String? = null 34 | 35 | private var player: SimpleExoPlayer? = null 36 | private var playerNotificationManager: PlayerNotificationManager? = null 37 | private var mediaSession: MediaSessionCompat? = null 38 | 39 | inner class AudioServiceBinder : Binder() { 40 | val service: AudioService 41 | get() = this@AudioService 42 | } 43 | 44 | companion object { 45 | @MainThread 46 | fun newIntent( 47 | context: Context, 48 | episodeUrl: String 49 | ) = Intent(context, AudioService::class.java).apply { 50 | putExtra(argPodcastUrl, episodeUrl) 51 | } 52 | 53 | const val argPodcastUrl = "ARG_PODCAST_URL" 54 | const val playbackChannelId = "playback_channel" 55 | const val mediaSessionTag = "DEV Community Session" 56 | const val playbackNotificationId = 1 57 | const val incrementMs = 15000 58 | } 59 | 60 | override fun onBind(intent: Intent): IBinder { 61 | super.onBind(intent) 62 | 63 | val newPodcastUrl = intent.getStringExtra(argPodcastUrl) 64 | if (currentPodcastUrl != newPodcastUrl) { 65 | currentPodcastUrl = newPodcastUrl 66 | preparePlayer() 67 | } 68 | 69 | return binder 70 | } 71 | 72 | override fun onCreate() { 73 | super.onCreate() 74 | 75 | player = SimpleExoPlayer.Builder(this).build() 76 | player?.audioAttributes = AudioAttributes.Builder() 77 | .setUsage(C.USAGE_MEDIA) 78 | .setContentType(C.CONTENT_TYPE_SPEECH) 79 | .build() 80 | 81 | playerNotificationManager = PodcastPlayerNotificationManager.createWithNotificationChannel( 82 | applicationContext, 83 | playbackChannelId, 84 | R.string.app_name, 85 | R.string.playback_channel_description, 86 | playbackNotificationId, 87 | object : PlayerNotificationManager.MediaDescriptionAdapter { 88 | override fun getCurrentContentTitle(player: Player): String { 89 | return episodeName ?: getString(R.string.app_name) 90 | } 91 | 92 | @Nullable 93 | override fun createCurrentContentIntent(player: Player): PendingIntent? = null 94 | 95 | @Nullable 96 | override fun getCurrentContentText(player: Player): String? { 97 | return podcastName ?: getString(R.string.playback_channel_description) 98 | } 99 | 100 | @Nullable 101 | override fun getCurrentLargeIcon( 102 | player: Player, 103 | callback: PlayerNotificationManager.BitmapCallback 104 | ): Bitmap? { 105 | return null 106 | } 107 | }, 108 | object : PlayerNotificationManager.NotificationListener { 109 | override fun onNotificationStarted( 110 | notificationId: Int, 111 | notification: Notification 112 | ) { 113 | startForeground(notificationId, notification) 114 | } 115 | 116 | override fun onNotificationCancelled(notificationId: Int) { 117 | stopSelf() 118 | } 119 | 120 | override fun onNotificationPosted( 121 | notificationId: Int, 122 | notification: Notification, 123 | ongoing: Boolean 124 | ) { 125 | if (ongoing) { 126 | // Make sure the service will not get destroyed while playing media. 127 | startForeground(notificationId, notification) 128 | } else { 129 | // Make notification cancellable. 130 | stopForeground(false) 131 | } 132 | } 133 | } 134 | ).apply { 135 | setUseNavigationActions(false) 136 | setUseStopAction(true) 137 | setUseNavigationActionsInCompactView(true) 138 | setFastForwardIncrementMs(incrementMs.toLong()) 139 | setRewindIncrementMs(incrementMs.toLong()) 140 | setPlayer(player) 141 | invalidate() 142 | } 143 | 144 | // Show lock screen controls and let apps like Google assistant manager playback. 145 | mediaSession = MediaSessionCompat(this, mediaSessionTag) 146 | val builder = MediaMetadataCompat.Builder() 147 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 148 | builder.putString(MediaMetadata.METADATA_KEY_TITLE, episodeName) 149 | .putString(MediaMetadata.METADATA_KEY_ARTIST, podcastName) 150 | } 151 | mediaSession?.setMetadata(builder.build()) 152 | playerNotificationManager?.setMediaSessionToken(mediaSession!!.sessionToken) 153 | } 154 | 155 | @MainThread 156 | fun play(audioUrl: String?, seconds: String?) { 157 | if (currentPodcastUrl != audioUrl) { 158 | currentPodcastUrl = audioUrl 159 | preparePlayer() 160 | seekTo("0") 161 | } else { 162 | seekTo(seconds) 163 | } 164 | player?.playWhenReady = true 165 | } 166 | 167 | @MainThread 168 | fun pause() { 169 | player?.playWhenReady = false 170 | } 171 | 172 | @MainThread 173 | fun mute(muted: String?) { 174 | muted?.toBoolean()?.let { 175 | if (it) { 176 | player?.volume = 0F 177 | } else { 178 | player?.volume = 1F 179 | } 180 | } 181 | } 182 | 183 | @MainThread 184 | fun volume(volume: String?) { 185 | volume?.toFloat()?.let { 186 | player?.volume = it 187 | } 188 | } 189 | 190 | @MainThread 191 | fun rate(rate: String?) { 192 | rate?.toFloat()?.let { 193 | player?.setPlaybackParameters(PlaybackParameters(it)) 194 | } 195 | } 196 | 197 | @MainThread 198 | fun seekTo(seconds: String?) { 199 | seconds?.toFloat()?.let { 200 | player?.seekTo((it * 1000F).toLong()) 201 | } 202 | } 203 | 204 | @MainThread 205 | fun loadMetadata(epName: String?, pdName: String?, url: String?) { 206 | episodeName = epName 207 | podcastName = pdName 208 | imageUrl = url 209 | } 210 | 211 | @MainThread 212 | fun currentTimeInSec() : Long { 213 | return player?.currentPosition ?: 0 214 | } 215 | 216 | @MainThread 217 | fun durationInSec() : Long { 218 | return player?.duration ?: 0L 219 | } 220 | 221 | @MainThread 222 | private fun preparePlayer() { 223 | player?.playWhenReady = false 224 | 225 | // Allows the data source to be seekable 226 | val extractorsFactory: DefaultExtractorsFactory = 227 | DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true) 228 | 229 | val dataSourceFactory = DefaultDataSourceFactory(this, BuildConfig.userAgent) 230 | val streamUri = Uri.parse(currentPodcastUrl) 231 | val mediaSource = ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory) 232 | .createMediaSource(streamUri) 233 | player?.prepare(mediaSource) 234 | } 235 | } -------------------------------------------------------------------------------- /app/src/main/java/to/dev/dev_android/media/PodcastPlayerNotificationManager.kt: -------------------------------------------------------------------------------- 1 | package to.dev.dev_android.media 2 | 3 | import android.content.Context 4 | import androidx.annotation.IntegerRes 5 | import com.google.android.exoplayer2.Player 6 | import com.google.android.exoplayer2.ui.PlayerNotificationManager 7 | import com.google.android.exoplayer2.util.NotificationUtil 8 | import java.util.* 9 | 10 | /* 11 | * This subclass of PlayerNotificationManager customizes the controls available in the 12 | * notification by overriding the getActions method. 13 | */ 14 | class PodcastPlayerNotificationManager( 15 | context: Context, 16 | channelId: String, 17 | notificationId: Int, 18 | mediaDescriptionAdapter: MediaDescriptionAdapter, 19 | playerNotificationManager: NotificationListener 20 | ): PlayerNotificationManager( 21 | context, 22 | channelId, 23 | notificationId, 24 | mediaDescriptionAdapter, 25 | playerNotificationManager) { 26 | 27 | companion object { 28 | fun createWithNotificationChannel( 29 | context: Context, 30 | channelId: String, 31 | channelName: Int, 32 | channelDescription: Int, 33 | notificationId: Int, 34 | mediaDescriptionAdapter: MediaDescriptionAdapter, 35 | playerNotificationManager: NotificationListener): PodcastPlayerNotificationManager { 36 | 37 | NotificationUtil.createNotificationChannel( 38 | context, channelId, channelName, channelDescription, NotificationUtil.IMPORTANCE_LOW 39 | ) 40 | return PodcastPlayerNotificationManager( 41 | context, channelId, notificationId, mediaDescriptionAdapter, playerNotificationManager 42 | ) 43 | } 44 | } 45 | 46 | override fun getActions(player: Player): List { 47 | var stringActions: List = ArrayList() 48 | stringActions += ACTION_REWIND 49 | stringActions += if (shouldShowPauseButton(player)) { 50 | ACTION_PAUSE 51 | } else { 52 | ACTION_PLAY 53 | } 54 | stringActions += ACTION_FAST_FORWARD 55 | return stringActions 56 | } 57 | 58 | private fun shouldShowPauseButton(player: Player): Boolean { 59 | val state = player.playbackState 60 | return state != Player.STATE_ENDED && state != Player.STATE_IDLE && player.playWhenReady 61 | } 62 | } -------------------------------------------------------------------------------- /app/src/main/java/to/dev/dev_android/util/AndroidWebViewBridge.kt: -------------------------------------------------------------------------------- 1 | package to.dev.dev_android.util 2 | 3 | import android.content.* 4 | import android.os.IBinder 5 | import android.util.Log 6 | import android.webkit.JavascriptInterface 7 | import android.widget.Toast 8 | import com.google.gson.Gson 9 | import org.greenrobot.eventbus.EventBus 10 | import org.greenrobot.eventbus.Subscribe 11 | import org.greenrobot.eventbus.ThreadMode 12 | import to.dev.dev_android.activities.VideoPlayerActivity 13 | import to.dev.dev_android.events.VideoPlayerPauseEvent 14 | import to.dev.dev_android.events.VideoPlayerTickEvent 15 | import to.dev.dev_android.media.AudioService 16 | import to.dev.dev_android.webclients.CustomWebViewClient 17 | import java.util.* 18 | 19 | class AndroidWebViewBridge(private val context: Context) { 20 | 21 | var webViewClient: CustomWebViewClient? = null 22 | private var timer: Timer? = null 23 | 24 | // audioService is initialized when onServiceConnected is executed after/during binding is done 25 | private var audioService: AudioService? = null 26 | private val connection = object : ServiceConnection { 27 | override fun onServiceConnected(name: ComponentName?, service: IBinder?) { 28 | val binder = service as AudioService.AudioServiceBinder 29 | audioService = binder.service 30 | } 31 | 32 | override fun onServiceDisconnected(name: ComponentName?) { 33 | audioService = null 34 | } 35 | } 36 | 37 | @JavascriptInterface 38 | fun logError(errorTag: String, errorMessage: String) { 39 | Log.e(errorTag, errorMessage) 40 | } 41 | 42 | @JavascriptInterface 43 | fun copyToClipboard(copyText: String) { 44 | val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager 45 | val clipData = ClipData.newPlainText("DEV Community", copyText) 46 | clipboard?.setPrimaryClip(clipData) 47 | } 48 | 49 | @JavascriptInterface 50 | fun showToast(message: String) { 51 | Toast.makeText(context, message, Toast.LENGTH_LONG).show() 52 | } 53 | 54 | @JavascriptInterface 55 | fun podcastMessage(message: String) { 56 | var map: Map = HashMap() 57 | map = Gson().fromJson(message, map.javaClass) 58 | when(map["action"]) { 59 | "load" -> loadPodcast(map["url"]) 60 | "play" -> audioService?.play(map["url"], map["seconds"]) 61 | "pause" -> audioService?.pause() 62 | "seek" -> audioService?.seekTo(map["seconds"]) 63 | "rate" -> audioService?.rate(map["rate"]) 64 | "muted" -> audioService?.mute(map["muted"]) 65 | "volume" -> audioService?.volume(map["volume"]) 66 | "metadata" -> audioService?.loadMetadata(map["episodeName"], map["podcastName"], map["imageUrl"]) 67 | "terminate" -> terminatePodcast() 68 | else -> logError("Podcast Error", "Unknown action") 69 | } 70 | } 71 | 72 | @JavascriptInterface 73 | fun videoMessage(message: String) { 74 | var map: Map = HashMap() 75 | map = Gson().fromJson(message, map.javaClass) 76 | when(map["action"]) { 77 | "play" -> playVideo(map["url"], map["seconds"]) 78 | else -> logError("Video Error", "Unknown action") 79 | } 80 | } 81 | 82 | fun playVideo(url: String?, seconds: String?) { 83 | url?.let { 84 | // Pause the audio player in case the user is currently listening to a audio (podcast) 85 | audioService?.pause() 86 | timer?.cancel() 87 | 88 | // Launch VideoPlayerActivity 89 | val intent = VideoPlayerActivity.newIntent(context, url, seconds ?: "0") 90 | context.startActivity(intent) 91 | 92 | EventBus.getDefault().register(this) 93 | } 94 | } 95 | 96 | fun loadPodcast(url: String?) { 97 | if (url == null) return 98 | 99 | AudioService.newIntent(context, url).also { intent -> 100 | context.bindService(intent, connection, Context.BIND_AUTO_CREATE) 101 | } 102 | 103 | // Clear out lingering timer if it exists & recreate 104 | timer?.cancel() 105 | timer = Timer() 106 | val timeUpdateTask = object: TimerTask() { 107 | override fun run() { 108 | podcastTimeUpdate() 109 | } 110 | } 111 | timer?.schedule(timeUpdateTask, 0, 1000) 112 | } 113 | 114 | fun terminatePodcast() { 115 | timer?.cancel() 116 | timer = null 117 | audioService?.let { 118 | it.pause() 119 | context.unbindService(connection) 120 | context.stopService(Intent(context, AudioService::class.java)) 121 | audioService = null 122 | } 123 | } 124 | 125 | fun podcastTimeUpdate() { 126 | audioService?.let { 127 | val time = it.currentTimeInSec() / 1000 128 | val duration = it.durationInSec() / 1000 129 | if (duration < 0) { 130 | // The duration overflows into a negative when waiting to load audio (initializing) 131 | webViewClient?.sendBridgeMessage("podcast", mapOf("action" to "init")) 132 | } else { 133 | val message = mapOf("action" to "tick", "duration" to duration, "currentTime" to time) 134 | webViewClient?.sendBridgeMessage("podcast", message) 135 | } 136 | } 137 | } 138 | 139 | @Subscribe(threadMode = ThreadMode.MAIN) 140 | fun onMessageEvent(event: VideoPlayerPauseEvent) { 141 | webViewClient?.sendBridgeMessage("video", mapOf("action" to event.action)) 142 | EventBus.getDefault().unregister(this) 143 | } 144 | 145 | @Subscribe(threadMode = ThreadMode.MAIN) 146 | fun onMessageEvent(event: VideoPlayerTickEvent) { 147 | val message = mapOf("action" to event.action, "currentTime" to event.seconds) 148 | webViewClient?.sendBridgeMessage("video", message) 149 | } 150 | 151 | /** 152 | * This method is used to open the native share-sheet of Android when simple text is to be 153 | * shared from the web-view. 154 | */ 155 | @JavascriptInterface 156 | fun shareText(text: String) { 157 | val shareIntent = Intent.createChooser( 158 | Intent().apply { 159 | action = Intent.ACTION_SEND 160 | putExtra(Intent.EXTRA_TEXT, text) 161 | type = "text/plain" 162 | }, 163 | null 164 | ) 165 | context.startActivity(shareIntent) 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /app/src/main/java/to/dev/dev_android/util/network/NetworkStatus.kt: -------------------------------------------------------------------------------- 1 | package to.dev.dev_android.util.network 2 | 3 | enum class NetworkStatus { 4 | OFFLINE, 5 | BACK_ONLINE 6 | } 7 | -------------------------------------------------------------------------------- /app/src/main/java/to/dev/dev_android/util/network/NetworkUtils.kt: -------------------------------------------------------------------------------- 1 | package to.dev.dev_android.util.network 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.withContext 5 | import to.dev.dev_android.BuildConfig 6 | import java.io.IOException 7 | import java.net.InetSocketAddress 8 | import java.net.Socket 9 | 10 | typealias NetworkStatusCallback = (isOnline: Boolean) -> Unit 11 | 12 | object NetworkUtils { 13 | suspend fun isOnline(): Boolean = withContext(Dispatchers.IO) { 14 | try { 15 | val timeout = 1500 16 | val address = InetSocketAddress(BuildConfig.baseHostname, 80) 17 | 18 | Socket().apply { 19 | connect(address, timeout) 20 | close() 21 | } 22 | 23 | true 24 | } catch (e: IOException) { 25 | false 26 | } 27 | } 28 | 29 | suspend fun isOffline(): Boolean = !isOnline() 30 | } -------------------------------------------------------------------------------- /app/src/main/java/to/dev/dev_android/util/network/NetworkWatcher.kt: -------------------------------------------------------------------------------- 1 | package to.dev.dev_android.util.network 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.content.IntentFilter 7 | import kotlinx.coroutines.CoroutineScope 8 | import kotlinx.coroutines.async 9 | import kotlinx.coroutines.launch 10 | import org.greenrobot.eventbus.EventBus 11 | import to.dev.dev_android.events.NetworkStatusEvent 12 | 13 | class NetworkWatcher(val coroutineScope: CoroutineScope) : BroadcastReceiver() { 14 | companion object { 15 | val intentFilter = IntentFilter().apply { 16 | addAction("android.net.conn.CONNECTIVITY_CHANGE") 17 | } 18 | } 19 | 20 | override fun onReceive(context: Context?, intent: Intent?) { 21 | coroutineScope.launch { 22 | if (async { NetworkUtils.isOnline() }.await()) { 23 | EventBus.getDefault().post(NetworkStatusEvent(NetworkStatus.BACK_ONLINE)) 24 | } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /app/src/main/java/to/dev/dev_android/webclients/CustomWebChromeClient.kt: -------------------------------------------------------------------------------- 1 | package to.dev.dev_android.webclients 2 | 3 | import android.net.Uri 4 | import android.webkit.ValueCallback 5 | import android.webkit.WebChromeClient 6 | import android.webkit.WebView 7 | 8 | 9 | class CustomWebChromeClient( 10 | private val baseURL: String, 11 | private val listener: CustomListener 12 | ) : WebChromeClient() { 13 | 14 | override fun onShowFileChooser( 15 | webView: WebView?, 16 | filePathCallback: ValueCallback>?, 17 | fileChooserParams: FileChooserParams? 18 | ): Boolean { 19 | listener.launchGallery(filePathCallback) 20 | return true 21 | } 22 | 23 | override fun onProgressChanged(view: WebView, newProgress: Int) { 24 | super.onProgressChanged(view, newProgress) 25 | if (newProgress == 100 && view.url == baseURL) { 26 | //view.clearHistory() 27 | } 28 | } 29 | 30 | interface CustomListener { 31 | fun launchGallery(filePathCallback: ValueCallback>?) 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/to/dev/dev_android/webclients/CustomWebViewClient.kt: -------------------------------------------------------------------------------- 1 | package to.dev.dev_android.webclients 2 | 3 | import android.content.Context 4 | import android.graphics.Color 5 | import android.net.Uri 6 | import android.os.Build 7 | import android.view.View 8 | import android.webkit.* 9 | import androidx.browser.customtabs.CustomTabsIntent 10 | import org.json.JSONObject 11 | import com.pusher.pushnotifications.PushNotifications 12 | import kotlinx.coroutines.* 13 | import org.greenrobot.eventbus.EventBus 14 | import org.greenrobot.eventbus.Subscribe 15 | import org.greenrobot.eventbus.ThreadMode 16 | import to.dev.dev_android.events.NetworkStatusEvent 17 | import to.dev.dev_android.util.network.* 18 | import java.lang.Exception 19 | import java.lang.Runnable 20 | 21 | class CustomWebViewClient( 22 | private val context: Context, 23 | private val view: WebView, 24 | private val coroutineScope: CoroutineScope, 25 | private val onPageFinish: () -> Unit 26 | ) : WebViewClient() { 27 | 28 | private val overrideUrlList = listOf( 29 | "://dev.to", 30 | "api.twitter.com/oauth", 31 | "api.twitter.com/login/error", 32 | "api.twitter.com/account/login_verification", 33 | "github.com/login", 34 | "github.com/sessions/" 35 | ) 36 | 37 | private var registeredUserNotifications = false 38 | 39 | override fun onPageFinished(view: WebView, url: String?) { 40 | onPageFinish() 41 | view.visibility = View.VISIBLE 42 | super.onPageFinished(view, url) 43 | } 44 | 45 | override fun doUpdateVisitedHistory(view: WebView?, url: String?, isReload: Boolean) { 46 | val javascript = "JSON.parse(document.getElementsByTagName('body')[0].getAttribute('data-user')).id" 47 | view?.evaluateJavascript(javascript) { result -> 48 | if (result != "null" && !registeredUserNotifications) { 49 | try { 50 | val userId = result.toString().toInt() 51 | PushNotifications.addDeviceInterest("user-notifications-$userId") 52 | registeredUserNotifications = true 53 | } 54 | catch (e: Exception) { 55 | println(e) 56 | } 57 | } 58 | } 59 | 60 | super.doUpdateVisitedHistory(view, url, isReload) 61 | } 62 | 63 | override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean { 64 | if (view.originalUrl == "https://dev.to/signout_confirm" && url == "https://dev.to/") { 65 | view.clearCache(true) 66 | view.clearFormData() 67 | view.clearHistory() 68 | if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP) { 69 | CookieManager.getInstance().removeAllCookie() 70 | } else { 71 | CookieManager.getInstance().removeAllCookies(null) 72 | } 73 | } 74 | 75 | if (overrideUrlList.any { url.contains(it) }) { 76 | return false 77 | } 78 | 79 | CustomTabsIntent.Builder() 80 | .setToolbarColor(Color.parseColor("#00000000")) 81 | .build() 82 | .also { it.launchUrl(context, Uri.parse(url)) } 83 | 84 | return true 85 | } 86 | 87 | fun sendBridgeMessage(type: String, message: Map) { 88 | val jsonMessage = JSONObject(message).toString() 89 | var javascript = "" 90 | when(type) { 91 | "podcast" -> javascript = "document.getElementById('audiocontent').setAttribute('data-podcast', '$jsonMessage')" 92 | "video" -> javascript = "document.getElementById('video-player-source').setAttribute('data-message', '$jsonMessage')" 93 | else -> return 94 | } 95 | view?.post(Runnable { 96 | view?.evaluateJavascript(javascript, null) 97 | }) 98 | } 99 | 100 | private var networkWatcher: NetworkWatcher? = null 101 | private fun registerNetworkWatcher() { 102 | if (networkWatcher != null) return 103 | 104 | unregisterNetworkWatcher() 105 | 106 | networkWatcher = NetworkWatcher(coroutineScope) 107 | } 108 | 109 | private fun unregisterNetworkWatcher() { 110 | networkWatcher?.let { 111 | context.unregisterReceiver(it) 112 | 113 | networkWatcher = null 114 | } 115 | } 116 | 117 | override fun onReceivedError( 118 | view: WebView?, 119 | request: WebResourceRequest?, 120 | error: WebResourceError? 121 | ) { 122 | super.onReceivedError(view, request, error) 123 | 124 | coroutineScope.launch { 125 | if (async { NetworkUtils.isOffline() }.await()) { 126 | EventBus.getDefault().post(NetworkStatusEvent(NetworkStatus.OFFLINE)) 127 | } 128 | } 129 | } 130 | 131 | @Subscribe(threadMode = ThreadMode.BACKGROUND) 132 | fun onNetworkStatusEvent(event: NetworkStatusEvent) { 133 | when (event.networkStatus) { 134 | NetworkStatus.OFFLINE -> { 135 | registerNetworkWatcher() 136 | 137 | context.registerReceiver(networkWatcher, NetworkWatcher.intentFilter) 138 | } 139 | NetworkStatus.BACK_ONLINE -> { 140 | unregisterNetworkWatcher() 141 | 142 | coroutineScope.launch { 143 | withContext(Dispatchers.Main) { 144 | view.loadUrl(view.url) 145 | } 146 | } 147 | } 148 | } 149 | } 150 | 151 | fun observeNetwork() { 152 | if(EventBus.getDefault().isRegistered(this)) 153 | EventBus.getDefault().unregister(this) 154 | EventBus.getDefault().register(this) 155 | } 156 | 157 | fun unobserveNetwork() { 158 | coroutineScope.cancel() 159 | EventBus.getDefault().unregister(this) 160 | 161 | unregisterNetworkWatcher() 162 | } 163 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/forem_dialog_fragment_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_arrow_downward_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_compass.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_forem_bot.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 12 | 13 | 16 | 21 | 22 | 26 | 29 | 34 | 39 | 42 | 47 | 50 | 55 | 60 | 65 | 68 | 69 | 70 | 73 | 74 | 77 | 80 | 84 | 85 | 88 | 91 | 92 | 96 | 97 | 100 | 103 | 104 | 108 | 109 | 112 | 115 | 116 | 121 | 126 | 131 | 132 | 133 | 136 | 137 | 138 | 141 | 142 | 145 | 148 | 153 | 154 | 157 | 162 | 166 | 167 | 170 | 173 | 174 | 177 | 182 | 186 | 187 | 190 | 193 | 194 | 198 | 203 | 206 | 211 | 216 | 220 | 225 | 228 | 233 | 238 | 243 | 248 | 249 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 48 | 50 | 52 | 54 | 56 | 58 | 60 | 62 | 64 | 66 | 68 | 70 | 72 | 74 | 75 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 8 | 11 | 14 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_news.xml: -------------------------------------------------------------------------------- 1 | 6 | 13 | 20 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/rounded_blue_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/rounded_dialog.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 10 | 11 | 19 | 20 | 26 | 27 | 37 | 38 | 50 | 51 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_video_player.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 13 | 14 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/layout/exo_player_control_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 18 | 19 | 21 | 22 | 24 | 25 | 27 | 28 | 30 | 31 | 32 | 33 | 39 | 40 | 49 | 50 | 54 | 55 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /app/src/main/res/layout/forem_app_dialog.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 20 | 21 | 37 | 38 | 54 | 55 | 67 | 68 | 76 | 77 | 85 | 86 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forem/DEV-Android/2da5d61a846a16e009acbb10126a25c2775baf9f/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forem/DEV-Android/2da5d61a846a16e009acbb10126a25c2775baf9f/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forem/DEV-Android/2da5d61a846a16e009acbb10126a25c2775baf9f/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forem/DEV-Android/2da5d61a846a16e009acbb10126a25c2775baf9f/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forem/DEV-Android/2da5d61a846a16e009acbb10126a25c2775baf9f/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forem/DEV-Android/2da5d61a846a16e009acbb10126a25c2775baf9f/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forem/DEV-Android/2da5d61a846a16e009acbb10126a25c2775baf9f/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forem/DEV-Android/2da5d61a846a16e009acbb10126a25c2775baf9f/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forem/DEV-Android/2da5d61a846a16e009acbb10126a25c2775baf9f/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forem/DEV-Android/2da5d61a846a16e009acbb10126a25c2775baf9f/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forem/DEV-Android/2da5d61a846a16e009acbb10126a25c2775baf9f/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forem/DEV-Android/2da5d61a846a16e009acbb10126a25c2775baf9f/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forem/DEV-Android/2da5d61a846a16e009acbb10126a25c2775baf9f/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forem/DEV-Android/2da5d61a846a16e009acbb10126a25c2775baf9f/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forem/DEV-Android/2da5d61a846a16e009acbb10126a25c2775baf9f/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #000000 4 | #000000 5 | #87DFD5 6 | #FFFFFF 7 | #000000 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #000000 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | DEV Community 3 | Splash 4 | Podcast Player 5 | Find DEV in the Forem app 6 | Get Forem for android in the Play Store. Your DEV profile info will be carried over so you can pick up where you left off! 7 | Open Forem app now. Your DEV profile info will be carried over so you can pick up where you left off! 8 | Download Forem App 9 | Open Forem App 10 | Show Forem app promotion dialog 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/test/java/to/dev/dev_android/view/main/view/CustomWebViewClientTest.kt: -------------------------------------------------------------------------------- 1 | package to.dev.dev_android.view.main.view 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | class CustomWebViewClientTest { 8 | 9 | @Test 10 | fun onPageFinished() { 11 | } 12 | 13 | @Test 14 | fun shouldOverrideUrlLoading() { 15 | } 16 | 17 | @Test 18 | fun getContext() { 19 | } 20 | 21 | @Test 22 | fun getBinding() { 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/test/java/to/dev/dev_android/view/main/view/MainActivityTest.kt: -------------------------------------------------------------------------------- 1 | package to.dev.dev_android.view.main.view 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | class MainActivityTest { 8 | 9 | @Test 10 | fun `2 plus 2 equals 4`() { 11 | assertEquals(2+2, 4) 12 | } 13 | 14 | @Test 15 | fun onCreate() { 16 | 17 | 18 | } 19 | 20 | @Test 21 | fun onNewIntent() { 22 | } 23 | 24 | @Test 25 | fun onBackPressed() { 26 | } 27 | } -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | google() 6 | jcenter() 7 | } 8 | dependencies { 9 | val kotlinVersion = findProperty("version.org.jetbrains.kotlin") as String 10 | val agpVersion = findProperty("version.com.android.tools.build..gradle") as String 11 | classpath("com.android.tools.build:gradle:$agpVersion") 12 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") 13 | classpath("com.google.gms:google-services:4.2.0") 14 | // NOTE: Do not place your application dependencies here; they belong 15 | // in the individual module build.gradle files 16 | } 17 | } 18 | plugins { 19 | id("io.gitlab.arturbosch.detekt") version "1.1.1" 20 | } 21 | 22 | allprojects { 23 | repositories { 24 | google() 25 | jcenter() 26 | } 27 | } 28 | 29 | 30 | tasks.register("hello") { 31 | group = "custom" 32 | description = "Hello World task - useful to solve build problems" 33 | } 34 | tasks.register("install") { 35 | group = "custom" 36 | description = "Build and install the app" 37 | dependsOn(":app:installDebug") 38 | } 39 | tasks.register("test") { 40 | group = "custom" 41 | description = "Run the unit tests" 42 | dependsOn(":app:testDebugUnitTest") 43 | } 44 | tasks.register("androidTest") { 45 | group = "custom" 46 | description = "Run android instrumentation tests" 47 | dependsOn(":app:connectedDebugAndroidTest") 48 | } 49 | 50 | detekt { 51 | input = files("$projectDir/app/src/main/java") 52 | config = files("$projectDir/config/detekt/detekt.yml") 53 | reports { 54 | xml { 55 | enabled = true 56 | destination = file("$projectDir/reports/detekt.xml") 57 | } 58 | html { 59 | enabled = true 60 | destination = file("$projectDir/reports/detekt.html") 61 | } 62 | } 63 | parallel = true 64 | } 65 | 66 | 67 | tasks.named("wrapper") { 68 | distributionType = Wrapper.DistributionType.ALL 69 | } 70 | 71 | tasks.register("runOnGitHub") { 72 | // Documentation: https://guides.gradle.org/writing-gradle-tasks/ 73 | dependsOn(":app:testDebugUnitTest") 74 | group = "custom" 75 | description = "$ ./gradlew runOnGitHub # runs on GitHub Action" 76 | } -------------------------------------------------------------------------------- /buildSrc/.gitignore: -------------------------------------------------------------------------------- 1 | .gradle/ 2 | build/ -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | repositories { 6 | google() 7 | mavenCentral() 8 | jcenter() 9 | } 10 | 11 | dependencies { 12 | implementation("com.android.tools.build:gradle:3.5.0") // keep in sync with gradle.properties 13 | } 14 | 15 | kotlinDslPluginOptions { 16 | experimentalWarning.set(false) 17 | } -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/Android.kt: -------------------------------------------------------------------------------- 1 | import com.android.build.gradle.BaseExtension 2 | import com.android.build.gradle.internal.dsl.BuildType 3 | import org.gradle.api.JavaVersion 4 | import org.gradle.api.Project 5 | import org.gradle.api.plugins.ExtraPropertiesExtension 6 | import org.gradle.kotlin.dsl.extra 7 | 8 | 9 | fun ExtraPropertiesExtension.string(key: String): String = (get(key) as String) 10 | 11 | fun ExtraPropertiesExtension.int(key: String): Int = (get(key) as String).toInt() 12 | 13 | fun BuildType.buildConfigString(name: String, value: String) = 14 | buildConfigField("String", name, "\"$value\"") 15 | 16 | 17 | fun Project.configureAndroid(android: BaseExtension) { 18 | val properties = listOf("android.compileSdkVersion", "android.minSdkVersion", "android.targetSdkVersion", "android.versionCode") 19 | val found = properties.map { extra.has(it) } 20 | require(found.all { it }) { "Missing some properties=$properties => found=$found" } 21 | val versions = properties.map { p -> extra.int(p) } 22 | 23 | android.compileSdkVersion(versions[0]) 24 | android.defaultConfig { 25 | minSdkVersion(versions[1]) 26 | targetSdkVersion(versions[2]) 27 | versionCode = versions[3] 28 | versionName = extra.string("android.versionName") 29 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 30 | 31 | multiDexEnabled = true 32 | } 33 | 34 | 35 | android.compileOptions { 36 | sourceCompatibility = JavaVersion.VERSION_1_8 37 | targetCompatibility = JavaVersion.VERSION_1_8 38 | } 39 | } 40 | 41 | fun Project.configureBuildConfig(android: BaseExtension) { 42 | android.buildTypes.all { 43 | resValue("string", "baseUrl", extra.string("chrome.baseUrl")) 44 | buildConfigString("baseUrl", extra.string("chrome.baseUrl")) 45 | resValue("string", "baseHostname", extra.string("chrome.baseHostname")) 46 | buildConfigString("baseHostname", extra.string("chrome.baseHostname")) 47 | resValue("string", "pusherInstanceId", extra.string("pusher.instanceId")) 48 | buildConfigString("pusherInstanceId", extra.string("pusher.instanceId")) 49 | resValue("string", "userAgent", extra.string("chrome.userAgent")) 50 | buildConfigString("userAgent", extra.string("chrome.userAgent")) 51 | } 52 | } 53 | 54 | 55 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/CustomTasks.kt: -------------------------------------------------------------------------------- 1 | import org.gradle.api.Project 2 | 3 | fun Project.customTasks() { 4 | tasks.register("hello") { 5 | group = "custom" 6 | description = "Hello World task - useful to solve build problems" 7 | 8 | doLast { 9 | println("Hello :)") 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/Libs.kt: -------------------------------------------------------------------------------- 1 | import kotlin.String 2 | 3 | /** 4 | * Generated by 5 | * $ ./gradlew buildSrcVersions 6 | * Re-run when you add a new dependency to the build 7 | * 8 | * Find which updates are available by running 9 | * $ ./gradlew refreshVersions 10 | * And edit the file `versions.properties` 11 | * 12 | * See https://github.com/jmfayard/refreshVersions 13 | */ 14 | object Libs { 15 | const val appcompat: String = "androidx.appcompat:appcompat:_" 16 | 17 | const val browser: String = "androidx.browser:browser:_" 18 | 19 | const val constraintlayout: String = "androidx.constraintlayout:constraintlayout:_" 20 | 21 | const val databinding_adapters: String = "androidx.databinding:databinding-adapters:_" 22 | 23 | const val databinding_common: String = "androidx.databinding:databinding-common:_" 24 | 25 | const val databinding_compiler: String = "androidx.databinding:databinding-compiler:_" 26 | 27 | const val databinding_runtime: String = "androidx.databinding:databinding-runtime:_" 28 | 29 | const val lifecycle_extensions: String = "androidx.lifecycle:lifecycle-extensions:_" 30 | 31 | const val lifecycle_viewmodel: String = "androidx.lifecycle:lifecycle-viewmodel:_" 32 | 33 | const val multidex: String = "androidx.multidex:multidex:_" 34 | 35 | const val espresso_core: String = "androidx.test.espresso:espresso-core:_" 36 | 37 | const val androidx_test_runner: String = "androidx.test:runner:_" 38 | 39 | const val aapt2: String = "com.android.tools.build:aapt2:_" 40 | 41 | const val com_android_tools_build_gradle: String = "com.android.tools.build:gradle:_" 42 | 43 | const val lint_gradle: String = "com.android.tools.lint:lint-gradle:_" 44 | 45 | const val exoplayer_core: String = "com.google.android.exoplayer:exoplayer-core:_" 46 | 47 | const val exoplayer_hls: String = "com.google.android.exoplayer:exoplayer-hls:_" 48 | 49 | const val exoplayer_ui: String = "com.google.android.exoplayer:exoplayer-ui:_" 50 | 51 | const val extension_mediasession: String = 52 | "com.google.android.exoplayer:extension-mediasession:_" 53 | 54 | const val gson: String = "com.google.code.gson:gson:_" 55 | 56 | const val firebase_messaging: String = "com.google.firebase:firebase-messaging:_" 57 | 58 | const val google_services: String = "com.google.gms:google-services:_" 59 | 60 | const val push_notifications_android: String = "com.pusher:push-notifications-android:_" 61 | 62 | const val de_fayard_buildsrclibs_gradle_plugin: String = 63 | "de.fayard.buildSrcLibs:de.fayard.buildSrcLibs.gradle.plugin:_" 64 | 65 | const val io_gitlab_arturbosch_detekt_gradle_plugin: String = 66 | "io.gitlab.arturbosch.detekt:io.gitlab.arturbosch.detekt.gradle.plugin:_" 67 | 68 | const val junit: String = "junit:junit:_" 69 | 70 | const val eventbus: String = "org.greenrobot:eventbus:_" 71 | 72 | const val eventbus_annotation_processor: String = 73 | "org.greenrobot:eventbus-annotation-processor:_" 74 | 75 | const val kotlin_android_extensions: String = "org.jetbrains.kotlin:kotlin-android-extensions:_" 76 | 77 | const val kotlin_android_extensions_runtime: String = 78 | "org.jetbrains.kotlin:kotlin-android-extensions-runtime:_" 79 | 80 | const val kotlin_annotation_processing_gradle: String = 81 | "org.jetbrains.kotlin:kotlin-annotation-processing-gradle:_" 82 | 83 | const val kotlin_gradle_plugin: String = "org.jetbrains.kotlin:kotlin-gradle-plugin:_" 84 | 85 | const val kotlin_stdlib_jdk8: String = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:_" 86 | 87 | const val kotlinx_coroutines_android: String = 88 | "org.jetbrains.kotlinx:kotlinx-coroutines-android:_" 89 | 90 | const val kotlinx_coroutines_core: String = "org.jetbrains.kotlinx:kotlinx-coroutines-core:_" 91 | } 92 | -------------------------------------------------------------------------------- /config/detekt/detekt.yml: -------------------------------------------------------------------------------- 1 | build: 2 | maxIssues: 10 3 | weights: 4 | # complexity: 2 5 | # LongParameterList: 1 6 | # style: 1 7 | # comments: 1 8 | 9 | processors: 10 | active: true 11 | exclude: 12 | # - 'DetektProgressListener' 13 | # - 'FunctionCountProcessor' 14 | # - 'PropertyCountProcessor' 15 | # - 'ClassCountProcessor' 16 | # - 'PackageCountProcessor' 17 | # - 'KtFileCountProcessor' 18 | 19 | console-reports: 20 | active: true 21 | exclude: 22 | # - 'ProjectStatisticsReport' 23 | # - 'ComplexityReport' 24 | # - 'NotificationReport' 25 | # - 'FindingsReport' 26 | # - 'BuildFailureReport' 27 | 28 | comments: 29 | active: true 30 | excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" 31 | CommentOverPrivateFunction: 32 | active: false 33 | CommentOverPrivateProperty: 34 | active: false 35 | EndOfSentenceFormat: 36 | active: false 37 | endOfSentenceFormat: ([.?!][ \t\n\r\f<])|([.?!:]$) 38 | UndocumentedPublicClass: 39 | active: false 40 | searchInNestedClass: true 41 | searchInInnerClass: true 42 | searchInInnerObject: true 43 | searchInInnerInterface: true 44 | UndocumentedPublicFunction: 45 | active: false 46 | 47 | complexity: 48 | active: true 49 | ComplexCondition: 50 | active: true 51 | threshold: 4 52 | ComplexInterface: 53 | active: false 54 | threshold: 10 55 | includeStaticDeclarations: false 56 | ComplexMethod: 57 | active: true 58 | threshold: 10 59 | ignoreSingleWhenExpression: false 60 | ignoreSimpleWhenEntries: false 61 | LabeledExpression: 62 | active: false 63 | ignoredLabels: "" 64 | LargeClass: 65 | active: true 66 | threshold: 600 67 | LongMethod: 68 | active: true 69 | threshold: 60 70 | LongParameterList: 71 | active: true 72 | threshold: 6 73 | ignoreDefaultParameters: false 74 | MethodOverloading: 75 | active: false 76 | threshold: 6 77 | NestedBlockDepth: 78 | active: true 79 | threshold: 4 80 | StringLiteralDuplication: 81 | active: false 82 | excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" 83 | threshold: 3 84 | ignoreAnnotation: true 85 | excludeStringsWithLessThan5Characters: true 86 | ignoreStringsRegex: '$^' 87 | TooManyFunctions: 88 | active: true 89 | excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" 90 | thresholdInFiles: 11 91 | thresholdInClasses: 11 92 | thresholdInInterfaces: 11 93 | thresholdInObjects: 11 94 | thresholdInEnums: 11 95 | ignoreDeprecated: false 96 | ignorePrivate: false 97 | ignoreOverridden: false 98 | 99 | empty-blocks: 100 | active: true 101 | EmptyCatchBlock: 102 | active: true 103 | allowedExceptionNameRegex: "^(_|(ignore|expected).*)" 104 | EmptyClassBlock: 105 | active: true 106 | EmptyDefaultConstructor: 107 | active: true 108 | EmptyDoWhileBlock: 109 | active: true 110 | EmptyElseBlock: 111 | active: true 112 | EmptyFinallyBlock: 113 | active: true 114 | EmptyForBlock: 115 | active: true 116 | EmptyFunctionBlock: 117 | active: true 118 | ignoreOverriddenFunctions: false 119 | EmptyIfBlock: 120 | active: true 121 | EmptyInitBlock: 122 | active: true 123 | EmptyKtFile: 124 | active: true 125 | EmptySecondaryConstructor: 126 | active: true 127 | EmptyWhenBlock: 128 | active: true 129 | EmptyWhileBlock: 130 | active: true 131 | 132 | exceptions: 133 | active: true 134 | ExceptionRaisedInUnexpectedLocation: 135 | active: false 136 | methodNames: 'toString,hashCode,equals,finalize' 137 | InstanceOfCheckForException: 138 | active: false 139 | excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" 140 | NotImplementedDeclaration: 141 | active: false 142 | PrintStackTrace: 143 | active: false 144 | RethrowCaughtException: 145 | active: false 146 | ReturnFromFinally: 147 | active: false 148 | SwallowedException: 149 | active: false 150 | ignoredExceptionTypes: 'InterruptedException,NumberFormatException,ParseException,MalformedURLException' 151 | allowedExceptionNameRegex: "^(_|(ignore|expected).*)" 152 | ThrowingExceptionFromFinally: 153 | active: false 154 | ThrowingExceptionInMain: 155 | active: false 156 | ThrowingExceptionsWithoutMessageOrCause: 157 | active: false 158 | exceptions: 'IllegalArgumentException,IllegalStateException,IOException' 159 | ThrowingNewInstanceOfSameException: 160 | active: false 161 | TooGenericExceptionCaught: 162 | active: true 163 | excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" 164 | exceptionNames: 165 | - ArrayIndexOutOfBoundsException 166 | - Error 167 | - Exception 168 | - IllegalMonitorStateException 169 | - NullPointerException 170 | - IndexOutOfBoundsException 171 | - RuntimeException 172 | - Throwable 173 | allowedExceptionNameRegex: "^(_|(ignore|expected).*)" 174 | TooGenericExceptionThrown: 175 | active: true 176 | exceptionNames: 177 | - Error 178 | - Exception 179 | - Throwable 180 | - RuntimeException 181 | 182 | formatting: 183 | active: true 184 | android: false 185 | autoCorrect: true 186 | AnnotationOnSeparateLine: 187 | active: false 188 | autoCorrect: true 189 | ChainWrapping: 190 | active: true 191 | autoCorrect: true 192 | CommentSpacing: 193 | active: true 194 | autoCorrect: true 195 | Filename: 196 | active: true 197 | FinalNewline: 198 | active: true 199 | autoCorrect: true 200 | ImportOrdering: 201 | active: false 202 | autoCorrect: true 203 | Indentation: 204 | active: false 205 | autoCorrect: true 206 | indentSize: 4 207 | continuationIndentSize: 4 208 | MaximumLineLength: 209 | active: true 210 | maxLineLength: 120 211 | ModifierOrdering: 212 | active: true 213 | autoCorrect: true 214 | MultiLineIfElse: 215 | active: true 216 | autoCorrect: true 217 | NoBlankLineBeforeRbrace: 218 | active: true 219 | autoCorrect: true 220 | NoConsecutiveBlankLines: 221 | active: true 222 | autoCorrect: true 223 | NoEmptyClassBody: 224 | active: true 225 | autoCorrect: true 226 | NoLineBreakAfterElse: 227 | active: true 228 | autoCorrect: true 229 | NoLineBreakBeforeAssignment: 230 | active: true 231 | autoCorrect: true 232 | NoMultipleSpaces: 233 | active: true 234 | autoCorrect: true 235 | NoSemicolons: 236 | active: true 237 | autoCorrect: true 238 | NoTrailingSpaces: 239 | active: true 240 | autoCorrect: true 241 | NoUnitReturn: 242 | active: true 243 | autoCorrect: true 244 | NoUnusedImports: 245 | active: true 246 | autoCorrect: true 247 | NoWildcardImports: 248 | active: true 249 | PackageName: 250 | active: true 251 | autoCorrect: true 252 | ParameterListWrapping: 253 | active: true 254 | autoCorrect: true 255 | indentSize: 4 256 | SpacingAroundColon: 257 | active: true 258 | autoCorrect: true 259 | SpacingAroundComma: 260 | active: true 261 | autoCorrect: true 262 | SpacingAroundCurly: 263 | active: true 264 | autoCorrect: true 265 | SpacingAroundDot: 266 | active: true 267 | autoCorrect: true 268 | SpacingAroundKeyword: 269 | active: true 270 | autoCorrect: true 271 | SpacingAroundOperators: 272 | active: true 273 | autoCorrect: true 274 | SpacingAroundParens: 275 | active: true 276 | autoCorrect: true 277 | SpacingAroundRangeOperator: 278 | active: true 279 | autoCorrect: true 280 | StringTemplate: 281 | active: true 282 | autoCorrect: true 283 | 284 | naming: 285 | active: true 286 | ClassNaming: 287 | active: true 288 | excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" 289 | classPattern: '[A-Z$][a-zA-Z0-9$]*' 290 | ConstructorParameterNaming: 291 | active: true 292 | excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" 293 | parameterPattern: '[a-z][A-Za-z0-9]*' 294 | privateParameterPattern: '[a-z][A-Za-z0-9]*' 295 | excludeClassPattern: '$^' 296 | EnumNaming: 297 | active: true 298 | excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" 299 | enumEntryPattern: '^[A-Z][_a-zA-Z0-9]*' 300 | ForbiddenClassName: 301 | active: false 302 | excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" 303 | forbiddenName: '' 304 | FunctionMaxLength: 305 | active: false 306 | excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" 307 | maximumFunctionNameLength: 30 308 | FunctionMinLength: 309 | active: false 310 | excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" 311 | minimumFunctionNameLength: 3 312 | FunctionNaming: 313 | active: true 314 | excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" 315 | functionPattern: '^([a-z$][a-zA-Z$0-9]*)|(`.*`)$' 316 | excludeClassPattern: '$^' 317 | ignoreOverridden: true 318 | FunctionParameterNaming: 319 | active: true 320 | excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" 321 | parameterPattern: '[a-z][A-Za-z0-9]*' 322 | excludeClassPattern: '$^' 323 | ignoreOverriddenFunctions: true 324 | InvalidPackageDeclaration: 325 | active: false 326 | rootPackage: '' 327 | MatchingDeclarationName: 328 | active: true 329 | MemberNameEqualsClassName: 330 | active: false 331 | ignoreOverriddenFunction: true 332 | ObjectPropertyNaming: 333 | active: true 334 | excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" 335 | constantPattern: '[A-Za-z][_A-Za-z0-9]*' 336 | propertyPattern: '[A-Za-z][_A-Za-z0-9]*' 337 | privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*' 338 | PackageNaming: 339 | active: true 340 | excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" 341 | packagePattern: '^[a-z]+(\.[a-z][A-Za-z0-9]*)*$' 342 | TopLevelPropertyNaming: 343 | active: true 344 | excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" 345 | constantPattern: '[A-Z][_A-Z0-9]*' 346 | propertyPattern: '[A-Za-z][_A-Za-z0-9]*' 347 | privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*' 348 | VariableMaxLength: 349 | active: false 350 | excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" 351 | maximumVariableNameLength: 64 352 | VariableMinLength: 353 | active: false 354 | excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" 355 | minimumVariableNameLength: 1 356 | VariableNaming: 357 | active: true 358 | excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" 359 | variablePattern: '[a-z][A-Za-z0-9]*' 360 | privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' 361 | excludeClassPattern: '$^' 362 | ignoreOverridden: true 363 | 364 | performance: 365 | active: true 366 | ArrayPrimitive: 367 | active: false 368 | ForEachOnRange: 369 | active: true 370 | excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" 371 | SpreadOperator: 372 | active: true 373 | excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" 374 | UnnecessaryTemporaryInstantiation: 375 | active: true 376 | 377 | potential-bugs: 378 | active: true 379 | DuplicateCaseInWhenExpression: 380 | active: true 381 | EqualsAlwaysReturnsTrueOrFalse: 382 | active: false 383 | EqualsWithHashCodeExist: 384 | active: true 385 | ExplicitGarbageCollectionCall: 386 | active: true 387 | InvalidRange: 388 | active: false 389 | IteratorHasNextCallsNextMethod: 390 | active: false 391 | IteratorNotThrowingNoSuchElementException: 392 | active: false 393 | LateinitUsage: 394 | active: false 395 | excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" 396 | excludeAnnotatedProperties: "" 397 | ignoreOnClassesPattern: "" 398 | MissingWhenCase: 399 | active: false 400 | RedundantElseInWhen: 401 | active: false 402 | UnconditionalJumpStatementInLoop: 403 | active: false 404 | UnreachableCode: 405 | active: true 406 | UnsafeCallOnNullableType: 407 | active: false 408 | UnsafeCast: 409 | active: false 410 | UselessPostfixExpression: 411 | active: false 412 | WrongEqualsTypeParameter: 413 | active: false 414 | 415 | style: 416 | active: true 417 | CollapsibleIfStatements: 418 | active: false 419 | DataClassContainsFunctions: 420 | active: false 421 | conversionFunctionPrefix: 'to' 422 | DataClassShouldBeImmutable: 423 | active: false 424 | EqualsNullCall: 425 | active: false 426 | EqualsOnSignatureLine: 427 | active: false 428 | ExplicitItLambdaParameter: 429 | active: false 430 | ExpressionBodySyntax: 431 | active: false 432 | includeLineWrapping: false 433 | ForbiddenComment: 434 | active: true 435 | values: 'TODO:,FIXME:,STOPSHIP:' 436 | ForbiddenImport: 437 | active: false 438 | imports: '' 439 | ForbiddenVoid: 440 | active: false 441 | ignoreOverridden: false 442 | ignoreUsageInGenerics: false 443 | FunctionOnlyReturningConstant: 444 | active: false 445 | ignoreOverridableFunction: true 446 | excludedFunctions: 'describeContents' 447 | LibraryCodeMustSpecifyReturnType: 448 | active: false 449 | LoopWithTooManyJumpStatements: 450 | active: false 451 | maxJumpCount: 1 452 | MagicNumber: 453 | active: true 454 | excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" 455 | ignoreNumbers: '-1,0,1,2' 456 | ignoreHashCodeFunction: true 457 | ignorePropertyDeclaration: false 458 | ignoreConstantDeclaration: true 459 | ignoreCompanionObjectPropertyDeclaration: true 460 | ignoreAnnotation: false 461 | ignoreNamedArgument: true 462 | ignoreEnums: false 463 | ignoreRanges: false 464 | MandatoryBracesIfStatements: 465 | active: false 466 | MaxLineLength: 467 | active: true 468 | maxLineLength: 120 469 | excludePackageStatements: true 470 | excludeImportStatements: true 471 | excludeCommentStatements: false 472 | MayBeConst: 473 | active: false 474 | ModifierOrder: 475 | active: true 476 | NestedClassesVisibility: 477 | active: false 478 | NewLineAtEndOfFile: 479 | active: true 480 | NoTabs: 481 | active: false 482 | OptionalAbstractKeyword: 483 | active: true 484 | OptionalUnit: 485 | active: false 486 | OptionalWhenBraces: 487 | active: false 488 | PreferToOverPairSyntax: 489 | active: false 490 | ProtectedMemberInFinalClass: 491 | active: false 492 | RedundantVisibilityModifierRule: 493 | active: false 494 | ReturnCount: 495 | active: true 496 | max: 2 497 | excludedFunctions: "equals" 498 | excludeLabeled: false 499 | excludeReturnFromLambda: true 500 | SafeCast: 501 | active: true 502 | SerialVersionUIDInSerializableClass: 503 | active: false 504 | SpacingBetweenPackageAndImports: 505 | active: false 506 | ThrowsCount: 507 | active: true 508 | max: 2 509 | TrailingWhitespace: 510 | active: false 511 | UnderscoresInNumericLiterals: 512 | active: false 513 | acceptableDecimalLength: 5 514 | UnnecessaryAbstractClass: 515 | active: false 516 | excludeAnnotatedClasses: "dagger.Module" 517 | UnnecessaryApply: 518 | active: false 519 | UnnecessaryInheritance: 520 | active: false 521 | UnnecessaryLet: 522 | active: false 523 | UnnecessaryParentheses: 524 | active: false 525 | UntilInsteadOfRangeTo: 526 | active: false 527 | UnusedImports: 528 | active: false 529 | UnusedPrivateClass: 530 | active: false 531 | UnusedPrivateMember: 532 | active: false 533 | allowedNames: "(_|ignored|expected|serialVersionUID)" 534 | UseCheckOrError: 535 | active: false 536 | UseDataClass: 537 | active: false 538 | excludeAnnotatedClasses: "" 539 | UseRequire: 540 | active: false 541 | UselessCallOnNotNull: 542 | active: false 543 | UtilityClassWithPublicConstructor: 544 | active: false 545 | VarCouldBeVal: 546 | active: false 547 | WildcardImport: 548 | active: true 549 | excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt" 550 | excludeImports: 'java.util.*,kotlinx.android.synthetic.*' 551 | -------------------------------------------------------------------------------- /fastlane/Appfile: -------------------------------------------------------------------------------- 1 | json_key_file("./google-service-account-user.json") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one 2 | package_name("to.dev.dev_android") # e.g. com.krausefx.app 3 | -------------------------------------------------------------------------------- /fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | # This file contains the fastlane.tools configuration 2 | # You can find the documentation at https://docs.fastlane.tools 3 | # 4 | # For a list of all available actions, check out 5 | # 6 | # https://docs.fastlane.tools/actions 7 | # 8 | # For a list of all available plugins, check out 9 | # 10 | # https://docs.fastlane.tools/plugins/available-plugins 11 | # 12 | 13 | # Uncomment the line if you want fastlane to automatically update itself 14 | # update_fastlane 15 | 16 | default_platform(:android) 17 | 18 | platform :android do 19 | desc "Runs all the tests" 20 | lane :test do 21 | gradle(task: "test") 22 | end 23 | 24 | desc "Submit a new Beta Build to Google Play" 25 | lane :beta do 26 | # gradle(task: "clean") 27 | gradle(task: 'bundle', build_type: 'Release') 28 | upload_to_play_store( 29 | track: "beta", 30 | release_status: "draft", 31 | skip_upload_apk: true, 32 | skip_upload_changelogs: true, 33 | skip_upload_images: true, 34 | skip_upload_metadata: true, 35 | skip_upload_screenshots: true, 36 | ) 37 | # slack(message: 'Successfully distributed a new beta build') 38 | end 39 | 40 | desc "Deploy a new version to the Google Play" 41 | lane :deploy do 42 | gradle(task: "clean assembleRelease") 43 | upload_to_play_store 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | ## dev.to configurations 2 | chrome.baseUrl=https://dev.to/ 3 | chrome.baseHostname=dev.to 4 | chrome.userAgent=DEV-Native-android 5 | pusher.instanceId=cdaf9857-fad0-4bfb-b360-64c1b2693ef3 6 | 7 | # Android release configurations 8 | android.targetSdkVersion=29 9 | android.compileSdkVersion=29 10 | android.minSdkVersion=21 11 | android.applicationId=to.dev.dev_android 12 | android.versionCode=14 13 | android.versionName=1.5.1 14 | 15 | # Keep in sync with buildSrc/build.gradle.kts 16 | 17 | 18 | # Project-wide Gradle settings. 19 | # IDE (e.g. Android Studio) users: 20 | # Gradle settings configured through the IDE *will override* 21 | # any settings specified in this file. 22 | # For more details on how to configure your build environment visit 23 | # http://www.gradle.org/docs/current/userguide/build_environment.html 24 | # Specifies the JVM arguments used for the daemon process. 25 | # The setting is particularly useful for tweaking memory settings. 26 | org.gradle.jvmargs=-Xmx1536m 27 | # When configured, Gradle will run in incubating parallel mode. 28 | # This option should only be used with decoupled projects. More details, visit 29 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 30 | # org.gradle.parallel=true 31 | # Kotlin code style for this project: "official" or "obsolete": 32 | kotlin.code.style=official 33 | kotlin.setJvmTargetFromAndroidCompileOptions = true 34 | android.useAndroidX=true 35 | android.enableJetifier=true 36 | studio.projectview=true 37 | # Dependencies and Plugin versions with their available updates 38 | # Generated by $ ./gradlew refreshVersions 39 | # You can edit the rest of the file, it will be kept intact 40 | # See https://github.com/jmfayard/buildSrcVersions/issues/77 41 | plugin.com.github.ben-manes.versions=0.25.0 42 | plugin.io.gitlab.arturbosch.detekt=1.1.1 43 | # # available=1.6.0 44 | plugin.de.fayard.buildSrcVersions=0.7.0 45 | version.androidx.databinding=3.6.0 46 | version.org.jetbrains.kotlin=1.3.71 47 | version.androidx.lifecycle=2.1.0 48 | version.com.android.tools.build..gradle=3.6.0 49 | version.androidx.test..runner=1.2.0 50 | version.gradleLatestVersion=6.2.1 51 | version.constraintlayout=1.1.3 52 | version.espresso-core=3.2.0 53 | version.lint-gradle=26.5.3 54 | version.appcompat=1.1.0 55 | version.browser=1.0.0 56 | version.aapt2=3.5.3-5435860 57 | version.junit=4.13 -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forem/DEV-Android/2da5d61a846a16e009acbb10126a25c2775baf9f/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | mavenLocal() 5 | } 6 | } 7 | 8 | 9 | plugins { 10 | // See https://jmfayard.github.io/refreshVersions 11 | id("de.fayard.refreshVersions") version "0.10.0" 12 | 13 | // See https://dev.to/jmfayard/the-one-gradle-trick-that-supersedes-all-the-others-5bpg 14 | id("com.gradle.enterprise").version("3.1.1") 15 | } 16 | 17 | gradleEnterprise { 18 | buildScan { 19 | termsOfServiceUrl = "https://gradle.com/terms-of-service" 20 | termsOfServiceAgree = "yes" 21 | publishOnFailure() 22 | } 23 | } 24 | 25 | refreshVersions { 26 | enableBuildSrcLibs() 27 | } 28 | 29 | include(":app") 30 | -------------------------------------------------------------------------------- /versions.properties: -------------------------------------------------------------------------------- 1 | #### Dependencies and Plugin versions with their available updates. 2 | #### Generated by `./gradlew refreshVersions` version 0.10.0 3 | #### 4 | #### Don't manually edit or split the comments that start with four hashtags (####), 5 | #### they will be overwritten by refreshVersions. 6 | #### 7 | #### suppress inspection "SpellCheckingInspection" for whole file 8 | #### suppress inspection "UnusedProperty" for whole file 9 | 10 | 11 | 12 | version.androidx.appcompat=1.1.0 13 | 14 | version.androidx.browser=1.0.0 15 | 16 | version.androidx.constraintlayout=1.1.3 17 | 18 | version.androidx.databinding=3.5.0 19 | 20 | version.androidx.lifecycle=2.1.0 21 | 22 | version.androidx.multidex=2.0.1 23 | 24 | version.androidx.test=1.2.0 25 | 26 | version.androidx.test.espresso=3.2.0 27 | 28 | version.com.google.android.exoplayer..exoplayer-core=2.11.4 29 | 30 | version.com.google.android.exoplayer..exoplayer-hls=2.11.4 31 | 32 | version.com.google.android.exoplayer..exoplayer-ui=2.11.4 33 | 34 | version.com.google.android.exoplayer..extension-mediasession=2.11.4 35 | 36 | version.com.google.code.gson..gson=2.8.6 37 | 38 | version.com.pusher..push-notifications-android=1.6.2 39 | 40 | version.firebase-messaging=18.0.0 41 | 42 | version.junit.junit=4.13 43 | 44 | version.kotlin=1.3.71 45 | 46 | version.kotlinx.coroutines=1.3.7 47 | 48 | version.org.greenrobot..eventbus=3.2.0 49 | 50 | version.org.greenrobot..eventbus-annotation-processor=3.2.0 51 | --------------------------------------------------------------------------------