Compare commits

...
70 Commits
Author SHA1 Message Date
AmineB 11c39ae87c Merge pull request 'Bump dependencies' (#173) from upgarde into master
Check master code / build (push) Successful in 19m42s
Create tag / build (push) Successful in 27m4s
Create tag / createTagAndChangelog (push) Successful in 52s
Create tag / release (push) Successful in 7m41s
Reviewed-on: #173
2025-01-03 08:41:15 +00:00
AmineB 6645902ec8 chore: "faster" action.
Check PR code / Lint (pull_request) Successful in 5m59s
Check PR code / build (pull_request) Successful in 14m16s
2025-01-03 09:19:12 +01:00
AmineB 0a07a5dfad fastlane: icon change.
Check PR code / Lint (pull_request) Has been cancelled
Check PR code / build (pull_request) Has been cancelled
2025-01-03 09:18:06 +01:00
AmineB d88d38fd3b chore: ignoring a pixel issue.
Check PR code / Lint (pull_request) Successful in 9m15s
Check PR code / build (pull_request) Successful in 23m30s
2025-01-02 20:53:00 +01:00
AmineB 28fe38aa17 test: fixed an ui test issue.
Check PR code / Lint (pull_request) Successful in 11m23s
Check PR code / build (pull_request) Successful in 19m41s
2025-01-02 20:32:53 +01:00
AmineB d524c30732 fix: center the loading thing.
Check PR code / Lint (pull_request) Successful in 5m35s
Check PR code / build (pull_request) Successful in 18m15s
2024-12-31 15:31:47 +01:00
AmineB 8c00aa65da test: items displaying.
Check PR code / Lint (pull_request) Successful in 9m8s
Check PR code / build (pull_request) Successful in 36m8s
2024-12-31 15:23:26 +01:00
AmineB ae81261cb1 bump: sqldelight.
Check PR code / Lint (pull_request) Successful in 1m47s
Check PR code / build (pull_request) Successful in 15m9s
2024-12-31 12:37:36 +01:00
AmineB 03c567ee33 bump: material, desugar jdk, jsoup, kodein, settings, napier, mock.
Check PR code / Lint (pull_request) Successful in 1m37s
Check PR code / build (pull_request) Successful in 13m29s
2024-12-31 11:45:11 +01:00
AmineB d23dd82fc2 bump: androix and coroutines.
Check PR code / Lint (pull_request) Successful in 1m50s
Check PR code / build (pull_request) Successful in 13m29s
2024-12-31 11:06:36 +01:00
AmineB 2e7a168424 bump: ktor. Closes #67.
Check PR code / Lint (pull_request) Successful in 47s
Check PR code / build (pull_request) Successful in 15m22s
2024-12-31 10:03:21 +01:00
giteadrone 5bc2f614af Changelog for v124123651
Check master code / build (push) Successful in 12m31s
2024-12-30 22:54:15 +00:00
AmineB 934c112db5 Merge pull request 'Bugfixes' (#171) from bugfixes into master
Check master code / build (push) Successful in 19m51s
Create tag / build (push) Successful in 21m4s
Create tag / createTagAndChangelog (push) Successful in 50s
Create tag / release (push) Successful in 3m39s
Reviewed-on: #171
2024-12-30 22:32:11 +00:00
AmineB ad7549a89f config: crowdin
Check PR code / Lint (pull_request) Successful in 54s
Check PR code / build (pull_request) Successful in 10m35s
2024-12-30 23:20:03 +01:00
AmineB fb9ceecabd chore: can links be empty ?
Check PR code / Lint (pull_request) Successful in 1m3s
Check PR code / build (pull_request) Successful in 11m14s
2024-12-30 22:45:18 +01:00
AmineB 61b9fd30e0 fix: Context issues in article fragment. 2024-12-30 22:37:36 +01:00
AmineB 806e56e20b fix: Context issues in fragment sheet.
Check PR code / Lint (pull_request) Successful in 1m7s
Check PR code / build (pull_request) Successful in 12m15s
2024-12-30 22:12:38 +01:00
AmineB cd8b7aaf9d fix: build. 2024-12-30 22:12:21 +01:00
AmineB c25ad7621e chore: compile issue fix.
Check PR code / Lint (pull_request) Successful in 1m10s
Check PR code / build (pull_request) Successful in 9m57s
2024-12-30 15:07:35 +01:00
AmineB 63da3b9fe7 chore: filter some bugs. 2024-12-30 15:07:18 +01:00
AmineB 1d99eeb633 bugfix: catch users using something other than selfoss. 2024-12-30 13:51:45 +01:00
AmineB 162a350a8f bugfix: No browser, no link. 2024-12-30 13:51:14 +01:00
AmineB 27c1bba146 translations
Check PR code / Lint (pull_request) Successful in 54s
Check PR code / build (pull_request) Successful in 9m26s
2024-12-30 13:49:42 +01:00
AmineB b7f3a9877a chore: remove log. 2024-12-30 12:58:34 +01:00
AmineB 47f78754dc translation
Check PR code / Lint (pull_request) Successful in 46s
Check PR code / build (pull_request) Failing after 3m20s
2024-12-30 12:48:42 +01:00
giteadrone 1bdfb143ac Changelog for v124123641
Check master code / coverage (push) Successful in 11m32s
Check master code / build (push) Successful in 15m2s
2024-12-29 22:02:12 +00:00
AmineB d81ced3964 Chore: no tests on build.
Check master code / coverage (push) Successful in 9m42s
Check master code / build (push) Successful in 12m57s
Create tag / build (push) Successful in 9m8s
Create tag / createTagAndChangelog (push) Successful in 46s
Create tag / release (push) Successful in 6m34s
2024-12-29 22:37:59 +01:00
AmineB fbafece1fa Merge pull request 'testing' (#170) from testing into master
Check master code / coverage (push) Successful in 10m16s
Check master code / build (push) Successful in 12m51s
Reviewed-on: #170
2024-12-29 21:23:52 +00:00
AmineB cbed8f07cb fix: Displaying fixes. Fixes #155
Check PR code / Lint (pull_request) Successful in 1m24s
Check PR code / build (pull_request) Successful in 9m20s
2024-12-29 22:13:45 +01:00
AmineB f54fcc3ba1 test: coverage
Check PR code / Lint (pull_request) Successful in 1m13s
Check PR code / build (pull_request) Successful in 9m43s
2024-12-29 21:35:12 +01:00
aminecmi aad93ef722 chore: update and use multiplatform datetime 2024-12-21 20:32:58 +01:00
giteadrone 9e83af0302 Changelog for v124123421
Check master code / build (push) Successful in 11m23s
2024-12-07 17:41:21 +00:00
aminecmi 24b86e66b4 fix: Trying to fix the serialization issue.
Check master code / build (push) Successful in 9m40s
Create tag / build (push) Successful in 8m10s
Create tag / createTagAndChangelog (push) Successful in 32s
Create tag / release (push) Successful in 6m48s
2024-12-07 18:20:49 +01:00
giteadrone 641c444061 Changelog for v124113311 2024-11-26 20:40:47 +00:00
0902c61544 chore: update versions. (#165)
Check master code / build (push) Successful in 8m8s
Create tag / build (push) Successful in 8m15s
Create tag / createTagAndChangelog (push) Successful in 1m0s
Create tag / release (push) Successful in 6m52s
## Types of changes

- [ ] I have read the **CONTRIBUTING** document.
- [ ] My code follows the code style of this project.
- [ ] I have updated the documentation accordingly.
- [ ] I have added tests to cover my changes.
- [ ] All new and existing tests passed.
- [ ] This is **NOT** translation related.

This closes issue #XXX

This is implements feature #YYY

This finishes chore #ZZZ

Co-authored-by: aminecmi <aminecmi@gmail.com>
Reviewed-on: #165
Co-authored-by: administrateur <administrateur@hidden.hidden>
Co-committed-by: administrateur <administrateur@hidden.hidden>
2024-11-26 20:19:43 +00:00
aminecmi 6790152a0b chore: fastlane changelog.
Check master code / build (push) Successful in 8m3s
2024-11-26 21:06:57 +01:00
aminecmi 46d1ba418e chore: fastlane fixes.
Check master code / build (push) Has been cancelled
2024-11-26 21:03:31 +01:00
giteadroneandaminecmi 436373d0ad Changelog for v124113301
Check master code / build (push) Successful in 7m42s
2024-11-25 22:23:23 +01:00
aminecmi 5b9b51c02d chore: Gitea Action
Check master code / build (push) Successful in 7m51s
Create tag / build (push) Successful in 8m15s
Create tag / createTagAndChangelog (push) Successful in 46s
Create tag / release (push) Successful in 6m27s
2024-11-25 21:55:48 +01:00
administrateur b81abe384a Merge pull request 'chore: Gitea Action' (#164) from runner into master
Push/PR Steps / BuildAndTest (push) Has been cancelled
Reviewed-on: #164
2024-11-23 14:14:45 +00:00
aminecmi 851f862dbe chore: Gitea Action
Push/PR Steps / Lint (pull_request) Successful in 1m0s
Push/PR Steps / BuildAndTest (pull_request) Successful in 7m49s
2024-11-23 14:54:45 +01:00
aminecmi 8d7e302af8 chore: Readme update. 2024-11-20 20:45:06 +01:00
AmineBandaminecmi 236e1cca90 Fix recycleview article positions (#163)
Reviewed-on: #163
Reviewed-by: Amine Bouabdallaoui <amineb@hidden.hidden>
2024-11-20 09:32:29 +01:00
davidoskky 3a33cb4510 Provide method to update items in the home
Removed in the previous commit, the item adapter accepts a method to update the articles list in the home page.
Renamed the method to make its objective more clear.
Removed a debugging log.
Reverted change to function name.
2024-11-20 01:28:47 +01:00
davidoskky 0bf9ca9a49 Fix recycleview article positions
The articles were being opened by setting a click listener on the binding.
In card view this was being done through a function and as such it used the overall viewbind of the view rather than the binding of the item viewholder.
Now the functions are more explicit to avoid future errors.
Pulled up a few members from ItemCardAdapter and ItemListAdapter to ItemAdapter.
2024-11-19 01:50:58 +01:00
aminecmi 61e0087894 Changelog for v124041081 [CI SKIP] 2024-04-17 10:57:12 +00:00
aminecmi 1ec05d9913 chore: comment.
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing
continuous-integration/drone/tag Build is passing
2024-04-17 12:39:45 +02:00
aminecmi 859bd91bbb fix: Last time fixing the parsing date hack before moving it to os version. 2024-04-17 12:22:33 +02:00
aminecmi 204b736c53 Changelog for v124030731 [CI SKIP] 2024-03-13 19:51:14 +00:00
aminecmi f24609c143 fix: Basic auth and password can have non whitspace characters. Fixes 142.
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing
2024-03-13 20:31:24 +01:00
aminecmi b94d7dc537 Changelog for v124020451 [CI SKIP] 2024-02-14 19:54:05 +00:00
aminecmi 41910cc4cd fix: Fixed handling of position in card adapter.
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing
2024-02-13 21:31:02 +01:00
aminecmi db166ca9d4 Changelog for v124010301 [CI SKIP] 2024-01-30 19:36:37 +00:00
aminecmi db0d5a4a85 fix: This may fix the oom errors.
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing
2024-01-29 20:55:29 +01:00
aminecmi 3bc0d7cf95 Changelog for v124010191 [CI SKIP] 2024-01-19 21:16:23 +00:00
aminecmi 8f464d95fd fix: moving listeners.
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing
2024-01-19 21:32:43 +01:00
aminecmi 5ccd6a3368 chore: removed a useless log. 2024-01-19 21:32:43 +01:00
aminecmi cdbded246e Changelog for v124010032 [CI SKIP] 2024-01-03 22:16:35 +00:00
aminecmi 750c7758bd fix: Another date format thing.
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing
2024-01-03 23:07:57 +01:00
aminecmi 22f8b14ecd Changelog for v124010031 [CI SKIP] 2024-01-03 21:46:48 +00:00
aminecmi 6e27d6d4e6 fix: Checking if selfoss instance.
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing
2024-01-03 22:28:57 +01:00
aminecmi 14ff4dbd05 fix: handle three characters lenght hexcode colors. 2024-01-03 21:35:58 +01:00
aminecmi 390c2d0cf3 Changelog for v123113311 [CI SKIP] 2023-11-27 20:48:32 +00:00
aminecmi e58914ef58 chore: Source tracker url in the menu.
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing
2023-11-27 21:31:46 +01:00
aminecmi a03f08fca1 fix: Handle kodein proguard rules. 2023-11-27 21:31:26 +01:00
aminecmi 8e9b87f00c Changelog for v123102961 [CI SKIP] 2023-10-23 21:11:15 +00:00
aminecmi f765224a86 chore: domain changes.
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing
2023-10-21 23:41:17 +02:00
aminecmi 14d2219eb8 Changelog for v123102852 [CI SKIP] 2023-10-12 18:48:06 +00:00
aminecmi 137580ccf9 chore: lint cleaning.
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing
2023-10-12 20:38:01 +02:00
aminecmi f101d22f54 Changelog for v123102841 [CI SKIP] 2023-10-12 20:13:17 +02:00
128 changed files with 4316 additions and 2256 deletions
-170
View File
@@ -1,170 +0,0 @@
kind: pipeline
type: docker
name: test
steps:
- name: Lint
failure: ignore
image: mingc/android-build-box:latest
commands:
- echo "---------------------------------------------------------"
- echo "Install linters..."
- curl -sSLO https://github.com/pinterest/ktlint/releases/download/1.0.0/ktlint && chmod a+x ktlint && mv ktlint /usr/local/bin/
- curl -sSLO https://github.com/detekt/detekt/releases/download/v1.23.1/detekt-cli-1.23.1.zip && unzip detekt-cli-1.23.1.zip
- echo "---------------------------------------------------------"
- echo "Linting..."
- ktlint || true
- echo "---------------------------------------------------------"
- echo "Detecting..."
- ./detekt-cli-1.23.1/bin/detekt-cli --all-rules || true
- echo "---------------------------------------------------------"
command_timeout: 1m
- name: BuildAndTest
image: mingc/android-build-box:latest
commands:
- echo "---------------------------------------------------------"
- echo "Configure gradle..."
- mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true\nsystemProp.org.gradle.internal.http.connectionTimeout=180000\nsystemProp.org.gradle.internal.http.socketTimeout=180000" >> ~/.gradle/gradle.properties
- echo "---------------------------------------------------------"
- echo "Configure java..."
- . ~/.bash_profile
- jenv global 17.0
- java --version
- date
- echo "---------------------------------------------------------"
- echo "Building and testing..."
- ./gradlew build
- echo "---------------------------------------------------------"
trigger:
event:
- push
- pull_request
---
kind: pipeline
type: docker
name: Publish
steps:
- name: createTagAndChangelog
image: ubuntu:latest
commands:
- apt-get update && apt-get install -y git
- git fetch --tags -p
- PREV=$(git describe --tags --abbrev=0)
- ./build.sh --publish --from-ci
- VER=$(git describe --tags --abbrev=0)
- CHANGELOG=$(git log $PREV..HEAD --pretty="- %s")
- echo "**$VER**\n\n$CHANGELOG\n\n--------------------------------------------------------------------\n\n$(cat CHANGELOG.md)" > CHANGELOG.md
- git add CHANGELOG.md
- git commit -m "Changelog for $VER [CI SKIP]"
environment:
TZ: Europe/Paris
- name: git-push
image: appleboy/drone-git-push
settings:
branch: master
remote:
from_secret: remoteUrl
followtags: true
ssh_key:
from_secret: privateKey
skip_verify: true
- name: scpFiles
image: appleboy/drone-scp
settings:
host: amine-louveau.fr
username: ubuntu
key:
from_secret: privateKey
port: 22
target: /home/ubuntu/
source: version.txt
- name: deploy
image: appleboy/drone-ssh
settings:
host: amine-louveau.fr
user: ubuntu
key:
from_secret: privateKey
command_timeout: 2m
script:
- cd /home/ubuntu && sudo rm -rf /var/www/amine/version.txt && sudo chown www-data:www-data ./version.txt && sudo mv version.txt /var/www/amine/
trigger:
event:
- promote
target:
- production
---
kind: pipeline
type: docker
name: Release
steps:
- name: build
image: mingc/android-build-box:latest
commands:
- echo "---------------------------------------------------------"
- echo "Fetch tags..."
- git fetch --tags
- echo "---------------------------------------------------------"
- echo "Configure gradle..."
- mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=false\nsystemProp.org.gradle.internal.http.connectionTimeout=180000\nsystemProp.org.gradle.internal.http.socketTimeout=180000" >> ~/.gradle/gradle.properties
- echo "---------------------------------------------------------"
- echo "Generate APK"
- ./gradlew :androidApp:assembleGithubConfigRelease
- echo "---------------------------------------------------------"
- echo "Get Key"
- wget https://amine-louveau.fr/key
- echo "---------------------------------------------------------"
- echo "Zipalign"
- $ANDROID_HOME/build-tools/31.0.0/zipalign -f -v 4 androidApp/build/outputs/apk/githubConfig/release/androidApp-githubConfig-release-unsigned.apk androidApp/build/outputs/apk/githubConfig/release/android-prod-released-ziped.apk
- echo "---------------------------------------------------------"
- echo "Sign"
- $ANDROID_HOME/build-tools/31.0.0/apksigner sign -v --out signed.apk --ks ./key --ks-key-alias $YOUR_KEY_ALIAS --ks-pass pass:$YOUR_KEYSTORE_PASSWORD --v1-signing-enabled true --v2-signing-enabled true androidApp/build/outputs/apk/githubConfig/release/android-prod-released-ziped.apk
- echo "---------------------------------------------------------"
- echo "Verify"
- $ANDROID_HOME/build-tools/31.0.0/apksigner verify signed.apk
environment:
TZ: Europe/Paris
YOUR_KEYSTORE_PASSWORD:
from_secret: keyPass
YOUR_KEY_ALIAS:
from_secret: keyAlias
- name: gitea_release
image: plugins/gitea-release
settings:
api_key:
from_secret: giteaAPI
base_url: https://gitea.amine-louveau.fr
files: signed.apk
- name: notify
image: drillster/drone-email
failure: ignore
settings:
host:
from_secret: smtpHOST
port:
from_secret: smtpPORT
username:
from_secret: smtpUSERNAME
password:
from_secret: smtpPASSWORD
from:
from_secret: smtpFROM
subject: Mapping file
recipients:
from_secret: smtpTO
recipients_only: true
skip_verify: true
attachment: androidApp/build/outputs/mapping/githubConfigRelease/mapping.txt
trigger:
event:
- tag
@@ -0,0 +1,10 @@
version: '3'
services:
selfoss:
container_name: selfoss
image: rsprta/selfoss
network_mode: "host"
ports:
- "8888:8888"
+45
View File
@@ -0,0 +1,45 @@
name: Build
on:
workflow_call:
jobs:
BuildAndTestAndCoverage:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Fetch tags
run: git fetch --tags -p
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
cache: gradle
- uses: gradle/actions/setup-gradle@v3
- uses: android-actions/setup-android@v3
- name: Configure gradle...
run: mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true" >> ~/.gradle/gradle.properties
- name: Build and test
run: ./gradlew build -x testReleaseUnitTest -x testDebugUnitTest -x testGithubConfigReleaseUnitTest -x testGithubConfigDebugUnitTest # These tests will be done
- uses: KengoTODA/actions-setup-docker-compose@v1
with:
version: "2.23.3"
- name: run selfoss
run: |
docker compose -f .gitea/workflows/assets/docker-compose.yml up -d
- name: coverage
run: |
./gradlew :koverHtmlReport
- uses: actions/upload-artifact@v3
with:
name: coverage
path: build/reports/kover/html
retention-days: 1
overwrite: true
include-hidden-files: true
- name: Clean
if: always()
run: |
docker compose -f .gitea/workflows/assets/docker-compose.yml stop
+127
View File
@@ -0,0 +1,127 @@
name: Create tag
on:
push:
branches:
- release
workflow_dispatch:
jobs:
build:
uses: ./.gitea/workflows/common_build.yml
createTagAndChangelog:
runs-on: ubuntu-latest
needs: build
steps:
- name: Check out repository code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Config git
run: |
git config --global user.email aminecmi+giteadrone@pm.me
git config --global user.name giteadrone
- name: Creating the tag and generate changelog
run: |
git fetch --tags -p
PREV=$(git describe --tags --abbrev=0)
./build.sh --publish --from-ci
VER=$(git describe --tags --abbrev=0)
CHANGELOG=$(git log $PREV..HEAD --pretty="- %s")
echo "**$VER
$CHANGELOG
--------------------------------------------------------------------
$(cat CHANGELOG.md)" > CHANGELOG.md
git add CHANGELOG.md
touch ./fastlane/metadata/android/en\-US/changelogs/$VER.txt
echo "**$VER**
$CHANGELOG" > ./fastlane/metadata/android/en\-US/changelogs/$VER.txt
git add ./fastlane/metadata/android/en\-US/changelogs/$VER.txt
git commit -m "Changelog for $VER"
- name: Push changes
uses: appleboy/git-push-action@v1.0.0
with:
author_name: giteadrone
author_email: aminecmi+giteadrone@pm.me
remote: ${{ secrets.REMOTE_URL }}
followtags: true
ssh_key: ${{ secrets.PRIVATE_KEY }}
tags: true
branch: release
- name: copy file via ssh password
uses: appleboy/scp-action@v0.1.7
with:
host: amine-bouabdallaoui.fr
username: ubuntu
key: ${{ secrets.PRIVATE_KEY }}
source: "version.txt"
target: "/home/ubuntu/"
- name: deploy version file
uses: appleboy/ssh-action@v1.2.0
with:
host: amine-bouabdallaoui.fr
username: ubuntu
key: ${{ secrets.PRIVATE_KEY }}
script: cd /home/ubuntu && sudo rm -rf /var/www/amine/version.txt && sudo chown www-data:www-data ./version.txt && sudo mv version.txt /var/www/amine/
release:
runs-on: ubuntu-latest
needs: createTagAndChangelog
steps:
- name: Check out repository code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Fetch tags
id: version
run: |
git fetch --tags -p
PREV=$(git describe --tags --abbrev=0)
echo $PREV
echo "VERSION=$PREV" >> $GITHUB_OUTPUT
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
cache: gradle
- name: Setup Android SDK
uses: android-actions/setup-android@v3
- name: Configure gradle...
run: mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=false\nsystemProp.org.gradle.internal.http.connectionTimeout=180000\nsystemProp.org.gradle.internal.http.socketTimeout=180000" >> ~/.gradle/gradle.properties
- name: setup go
uses: https://github.com/actions/setup-go@v4
with:
go-version: '>=1.20.1'
- name: Generate APK
run: ./gradlew :androidApp:assembleGithubConfigRelease
- name: Get Key
run: wget ${{ secrets.KEY_URL }}
- name: Zippalign
run: |
sdkmanager "build-tools;31.0.0"
ls $ANDROID_HOME/build-tools
$ANDROID_HOME/build-tools/31.0.0/zipalign -f -v 4 androidApp/build/outputs/apk/githubConfig/release/androidApp-githubConfig-release-unsigned.apk androidApp/build/outputs/apk/githubConfig/release/android-prod-released-ziped.apk
- name: Sigh
run: $ANDROID_HOME/build-tools/31.0.0/apksigner sign -v --out signed.apk --ks ./key --ks-key-alias ${{ secrets.KEY_ALIAS }} --ks-pass pass:${{ secrets.KEYSTORE_PASSWORD }} --v1-signing-enabled true --v2-signing-enabled true androidApp/build/outputs/apk/githubConfig/release/android-prod-released-ziped.apk
- name: Verify
run: $ANDROID_HOME/build-tools/31.0.0/apksigner verify signed.apk
- name: Release
uses: https://gitea.com/actions/gitea-release-action@main
with:
files: signed.apk
token: ${{ secrets.API_KEY }}
tag_name: ${{ steps.version.outputs.VERSION }}
name: ${{ steps.version.outputs.VERSION }}
- name: Send mail
uses: https://github.com/dawidd6/action-send-mail@v4
with:
connection_url: ${{ secrets.MAIL_CONNECTION }}
to: ${{ secrets.MAIL_TO }}
from: ${{ secrets.MAIL_FROM }}
subject: Mapping file
priority: high
convert_markdown: true
body: Nouveau fichier de mapping pour la version ${{ steps.version.outputs.VERSION }}
attachments: androidApp/build/outputs/mapping/githubConfigRelease/mapping.txt
+28
View File
@@ -0,0 +1,28 @@
name: Check PR code
on:
pull_request:
branches:
- master
jobs:
Lint:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
cache: gradle
- name: Install klint
run: curl -sSLO https://github.com/pinterest/ktlint/releases/download/1.0.0/ktlint && chmod a+x ktlint && mv ktlint /usr/local/bin/
- name: Install detekt
run: curl -sSLO https://github.com/detekt/detekt/releases/download/v1.23.1/detekt-cli-1.23.1.zip && unzip detekt-cli-1.23.1.zip
- name: Linting...
run: ktlint 'shared/**/*.kt' 'androidApp/**/*.kt' '!shared/build' || true
- name: Detecting...
run: ./detekt-cli-1.23.1/bin/detekt-cli --all-rules --excludes '**/shared/build/**/*.kt' || true
build:
needs: Lint
uses: ./.gitea/workflows/common_build.yml
+9
View File
@@ -0,0 +1,9 @@
name: Check master code
on:
push:
branches:
- master
jobs:
build:
uses: ./.gitea/workflows/common_build.yml
+1 -1
View File
@@ -10,7 +10,7 @@ Please read the guidelines before contributing, and follow them (or try to) when
There are many ways to contribute to this project, you could [translate the app](https://crowdin.com/project/readerforselfoss), report bugs, request missing features, suggest enhancements and changes to existing ones. You also can improve the README with useful tips that could help the other users. There are many ways to contribute to this project, you could [translate the app](https://crowdin.com/project/readerforselfoss), report bugs, request missing features, suggest enhancements and changes to existing ones. You also can improve the README with useful tips that could help the other users.
You can fork the repository, and [help me solve some issues](https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/issues?q=is%3Aissue+is%3Aopen+label%3A%22Up+For+Grabs%22) or [develop new things](https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/issues) You can fork the repository, and [help me solve some issues](https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderForSelfoss-multiplatform/issues?q=is%3Aissue+is%3Aopen+label%3A%22Up+For+Grabs%22) or [develop new things](https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderForSelfoss-multiplatform/issues)
### What I can't help you with. ### What I can't help you with.
+3
View File
@@ -321,3 +321,6 @@ fabric.properties
crowdin.properties crowdin.properties
.kotlin/
build-cache/
+137
View File
@@ -1,3 +1,140 @@
**v124123651
- Merge pull request 'Bugfixes' (#171) from bugfixes into master
- config: crowdin
- chore: can links be empty ?
- fix: Context issues in article fragment.
- fix: Context issues in fragment sheet.
- fix: build.
- chore: compile issue fix.
- chore: filter some bugs.
- bugfix: catch users using something other than selfoss.
- bugfix: No browser, no link.
- translations
- chore: remove log.
- translation
- Changelog for v124123641
--------------------------------------------------------------------
**v124123641
- Chore: no tests on build.
- Merge pull request 'testing' (#170) from testing into master
- fix: Displaying fixes. Fixes #155
- test: coverage
- chore: update and use multiplatform datetime
- Changelog for v124123421
--------------------------------------------------------------------
**v124123421
- fix: Trying to fix the serialization issue.
- Changelog for v124113311
--------------------------------------------------------------------
**v124113311
- chore: update versions. (#165)
- chore: fastlane changelog.
- chore: fastlane fixes.
- Changelog for v124113301
--------------------------------------------------------------------
**v124113301**
- chore: Gitea Action
- Merge pull request 'chore: Gitea Action' (#164) from runner into master
- chore: Gitea Action
- chore: Readme update.
--------------------------------------------------------------------
**v124041081**
- chore: comment.
- fix: Last time fixing the parsing date hack before moving it to os version.
- Changelog for v124030731 [CI SKIP]
--------------------------------------------------------------------
**v124030731**
- fix: Basic auth and password can have non whitspace characters. Fixes 142.
- Changelog for v124020451 [CI SKIP]
--------------------------------------------------------------------
**v124020451**
- fix: Fixed handling of position in card adapter.
- Changelog for v124010301 [CI SKIP]
--------------------------------------------------------------------
**v124010301**
- fix: This may fix the oom errors.
- Changelog for v124010191 [CI SKIP]
--------------------------------------------------------------------
**v124010191**
- fix: moving listeners.
- chore: removed a useless log.
- Changelog for v124010032 [CI SKIP]
--------------------------------------------------------------------
**v124010032**
- fix: Another date format thing.
- Changelog for v124010031 [CI SKIP]
--------------------------------------------------------------------
**v124010031**
- fix: Checking if selfoss instance.
- fix: handle three characters lenght hexcode colors.
- Changelog for v123113311 [CI SKIP]
--------------------------------------------------------------------
**v123113311**
- chore: Source tracker url in the menu.
- fix: Handle kodein proguard rules.
- Changelog for v123102961 [CI SKIP]
--------------------------------------------------------------------
**v123102961**
- chore: domain changes.
- Changelog for v123102852 [CI SKIP]
--------------------------------------------------------------------
**v123102852**
- chore: lint cleaning.
- Changelog for v123102841 [CI SKIP]
--------------------------------------------------------------------
**v123102841**
- chore: cleaning ci steps and upgrading dependencies.
- feat: Self signed ssl support.
- Changelog for v123061811 [CI SKIP]
--------------------------------------------------------------------
**v123061811** **v123061811**
- feat: Added confirmation dialog for disconnect item menu. - feat: Added confirmation dialog for disconnect item menu.
+6 -10
View File
@@ -1,4 +1,4 @@
# ReaderForSelfoss-multiplatform [![Build Status](https://build.amine-louveau.fr/api/badges/Louvorg/ReaderForSelfoss-multiplatform/status.svg)](https://build.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform) # ReaderForSelfoss-multiplatform [![Build Status](https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderForSelfoss-multiplatform/actions/workflows/on_push.yml/badge.svg)](https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderForSelfoss-multiplatform/actions?workflow=on_push.yml&actor=0&status=0)
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/readerforselfoss/localized.svg)](https://crowdin.com/project/readerforselfoss) [![Crowdin](https://d322cqt584bo4o.cloudfront.net/readerforselfoss/localized.svg)](https://crowdin.com/project/readerforselfoss)
@@ -10,10 +10,6 @@ If you are a user, you can still create new issues. I'll fix them when I can.
<a href="https://f-droid.org/packages/bou.amine.apps.readerforselfossv2.android"><img src="https://f-droid.org/badge/get-it-on.png" alt="Get it on F-Droid" height="100"></a> <a href="https://f-droid.org/packages/bou.amine.apps.readerforselfossv2.android"><img src="https://f-droid.org/badge/get-it-on.png" alt="Get it on F-Droid" height="100"></a>
## Screen captures
<img src="res//fr-card.png?raw=true" alt="card view" width="400"/> <img src="res//fr-list.png?raw=true" alt="list view" width="400"/>
## Like my app ? ## Like my app ?
<a href="https://www.buymeacoffee.com/aminecmi" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/lato-orange.png" alt="Buy Me A Coffee" style="height: 51px !important;width: 217px !important;" ></a> <a href="https://www.buymeacoffee.com/aminecmi" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/lato-orange.png" alt="Buy Me A Coffee" style="height: 51px !important;width: 217px !important;" ></a>
@@ -22,15 +18,15 @@ If you are a user, you can still create new issues. I'll fix them when I can.
1. **You'll have to have a Selfoss instance running.** You'll find everything you need to install it [here](https://selfoss.aditu.de/). 1. **You'll have to have a Selfoss instance running.** You'll find everything you need to install it [here](https://selfoss.aditu.de/).
2. Check the [Contribution guide](https://gitea.amine-louveau.fr/Louvorg/ReaderforSelfoss-multiplatform/src/branch/master/.github/CONTRIBUTING.md). 2. Check the [Contribution guide](https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderforSelfoss-multiplatform/src/branch/master/.github/CONTRIBUTING.md).
3. Build the project by following [these steps](https://gitea.amine-louveau.fr/Louvorg/ReaderforSelfoss-multiplatform/src/branch/master/.github/CONTRIBUTING.md#build-the-project) (you should have read them after the contribution guide) 3. Build the project by following [these steps](https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderforSelfoss-multiplatform/src/branch/master/.github/CONTRIBUTING.md#build-the-project) (you should have read them after the contribution guide)
## Useful links ## Useful links
- [Check what changed](https://gitea.amine-louveau.fr/Louvorg/ReaderforSelfoss-multiplatform/src/branch/master/CHANGELOG.md) - [Check what changed](https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderforSelfoss-multiplatform/src/branch/master/CHANGELOG.md)
- [See what I'm doing](https://gitea.amine-louveau.fr/Louvorg/ReaderforSelfoss-multiplatform/projects/1) - [See what I'm doing](https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderforSelfoss-multiplatform/projects/1)
- [Create an issue, or request a new feature](https://gitea.amine-louveau.fr/Louvorg/ReaderforSelfoss-multiplatform/issues) - [Create an issue, or request a new feature](https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderforSelfoss-multiplatform/issues)
- [Help translation the app](https://crowdin.com/project/readerforselfoss) - [Help translation the app](https://crowdin.com/project/readerforselfoss)
## Contributors (V1) (Alphabetical order) ❤️ ## Contributors (V1) (Alphabetical order) ❤️
+1
View File
@@ -1 +1,2 @@
/build /build
.kotlin/
+44 -28
View File
@@ -1,7 +1,7 @@
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
val ignoreGitVersion: String by project val ignoreGitVersion: String by project
val acraVersion = "5.9.7" val acraVersion = "5.12.0"
plugins { plugins {
id("com.android.application") id("com.android.application")
@@ -9,6 +9,7 @@ plugins {
kotlin("kapt") kotlin("kapt")
id("com.mikepenz.aboutlibraries.plugin") id("com.mikepenz.aboutlibraries.plugin")
id("org.jetbrains.kotlinx.kover") id("org.jetbrains.kotlinx.kover")
id("app.cash.sqldelight") version "2.0.2"
} }
fun Project.execWithOutput(cmd: String, ignore: Boolean = false): String { fun Project.execWithOutput(cmd: String, ignore: Boolean = false): String {
@@ -65,14 +66,14 @@ android {
kotlinOptions { kotlinOptions {
jvmTarget = "17" jvmTarget = "17"
} }
compileSdk = 34 compileSdk = 35
buildFeatures { buildFeatures {
viewBinding = true viewBinding = true
} }
defaultConfig { defaultConfig {
applicationId = "bou.amine.apps.readerforselfossv2.android" applicationId = "bou.amine.apps.readerforselfossv2.android"
minSdk = 25 minSdk = 25
targetSdk = 34 targetSdk = 34 // 35 when edge-to-edge is handled
versionCode = versionCodeFromGit() versionCode = versionCodeFromGit()
versionName = versionNameFromGit() versionName = versionNameFromGit()
@@ -84,6 +85,7 @@ android {
// tests // tests
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments["clearPackageData"] = "true"
} }
packaging { packaging {
resources { resources {
@@ -107,32 +109,37 @@ android {
} }
} }
namespace = "bou.amine.apps.readerforselfossv2.android" namespace = "bou.amine.apps.readerforselfossv2.android"
testOptions {
animationsDisabled = true
execution = "ANDROIDX_TEST_ORCHESTRATOR"
unitTests {
isIncludeAndroidResources = true
}
}
} }
dependencies { dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
implementation(project(":shared")) implementation(project(":shared"))
implementation("com.google.android.material:material:1.9.0") implementation("androidx.appcompat:appcompat:1.7.0")
implementation("androidx.appcompat:appcompat:1.6.1") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1")
implementation("androidx.preference:preference-ktx:1.2.1") implementation("androidx.preference:preference-ktx:1.2.1")
implementation(fileTree(mapOf("include" to listOf("*.jar"), "dir" to "libs"))) implementation(fileTree(mapOf("include" to listOf("*.jar"), "dir" to "libs")))
// Android Support // Android Support
implementation("androidx.appcompat:appcompat:1.6.1") implementation("com.google.android.material:material:1.12.0")
implementation("com.google.android.material:material:1.9.0") implementation("androidx.recyclerview:recyclerview:1.4.0-rc01")
implementation("androidx.recyclerview:recyclerview:1.3.1")
implementation("androidx.legacy:legacy-support-v4:1.0.0") implementation("androidx.legacy:legacy-support-v4:1.0.0")
implementation("androidx.vectordrawable:vectordrawable:1.2.0-beta01") implementation("androidx.vectordrawable:vectordrawable:1.2.0")
implementation("androidx.cardview:cardview:1.0.0") implementation("androidx.cardview:cardview:1.0.0")
implementation("androidx.annotation:annotation:1.7.0") implementation("androidx.annotation:annotation:1.9.1")
implementation("androidx.work:work-runtime-ktx:2.8.1") implementation("androidx.work:work-runtime-ktx:2.10.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4") implementation("androidx.constraintlayout:constraintlayout:2.2.0")
implementation("org.jsoup:jsoup:1.15.4") implementation("org.jsoup:jsoup:1.18.3")
//multidex //multidex
implementation("androidx.multidex:multidex:2.0.1") implementation("androidx.multidex:multidex:2.0.1")
@@ -145,31 +152,31 @@ dependencies {
implementation("com.ashokvarma.android:bottom-navigation-bar:2.2.0") implementation("com.ashokvarma.android:bottom-navigation-bar:2.2.0")
// glide // glide
kapt("com.github.bumptech.glide:compiler:4.15.0") kapt("com.github.bumptech.glide:compiler:4.16.0")
implementation("com.github.bumptech.glide:okhttp3-integration:4.15.0") implementation("com.github.bumptech.glide:okhttp3-integration:4.16.0")
// Themes // Themes
implementation("com.github.rubensousa:floatingtoolbar:1.5.1") implementation("com.github.rubensousa:floatingtoolbar:1.5.1")
// Pager // Pager
implementation("me.relex:circleindicator:2.1.6") implementation("me.relex:circleindicator:2.1.6")
implementation("androidx.viewpager2:viewpager2:1.1.0-beta02") implementation("androidx.viewpager2:viewpager2:1.1.0")
//Dependency Injection //Dependency Injection
implementation("org.kodein.di:kodein-di:7.14.0") implementation("org.kodein.di:kodein-di:7.23.1")
implementation("org.kodein.di:kodein-di-framework-android-x:7.14.0") implementation("org.kodein.di:kodein-di-framework-android-x:7.23.1")
implementation("org.kodein.di:kodein-di-framework-android-x-viewmodel:7.14.0") implementation("org.kodein.di:kodein-di-framework-android-x-viewmodel:7.23.1")
//Settings //Settings
implementation("com.russhwolf:multiplatform-settings-no-arg:0.9") implementation("com.russhwolf:multiplatform-settings-no-arg:1.3.0")
//Logging //Logging
implementation("io.github.aakira:napier:2.6.1") implementation("io.github.aakira:napier:2.7.1")
//PhotoView //PhotoView
implementation("com.github.chrisbanes:PhotoView:2.3.0") implementation("com.github.chrisbanes:PhotoView:2.3.0")
implementation("androidx.core:core-ktx:1.12.0") implementation("androidx.core:core-ktx:1.15.0")
implementation("androidx.lifecycle:lifecycle-extensions:2.2.0") implementation("androidx.lifecycle:lifecycle-extensions:2.2.0")
@@ -177,16 +184,25 @@ dependencies {
implementation("com.github.ln-12:multiplatform-connectivity-status:1.3.0") implementation("com.github.ln-12:multiplatform-connectivity-status:1.3.0")
// SQLDELIGHT // SQLDELIGHT
implementation("com.squareup.sqldelight:android-driver:1.5.4") implementation("app.cash.sqldelight:android-driver:2.0.2")
//test //test
testImplementation("junit:junit:4.13.2") testImplementation("junit:junit:4.13.2")
testImplementation("io.mockk:mockk:1.12.0") testImplementation("io.mockk:mockk:1.13.14")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0") implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.1")
androidTestImplementation("androidx.test:runner:1.6.2")
androidTestImplementation("androidx.test:rules:1.6.1")
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
implementation("androidx.test.espresso:espresso-idling-resource:3.6.1")
androidTestImplementation("androidx.test.ext:junit-ktx:1.2.1")
androidTestUtil("androidx.test:orchestrator:1.5.1")
testImplementation("org.robolectric:robolectric:4.14.1")
testImplementation("androidx.test:core-ktx:1.6.1")
implementation("ch.acra:acra-http:$acraVersion") implementation("ch.acra:acra-http:$acraVersion")
implementation("ch.acra:acra-toast:$acraVersion") implementation("ch.acra:acra-toast:$acraVersion")
implementation("com.google.auto.service:auto-service:1.1.1")
} }
tasks.withType<Test> { tasks.withType<Test> {
+9
View File
@@ -86,3 +86,12 @@
-dontwarn io.mockk.** -dontwarn io.mockk.**
-keep class io.mockk.** { *; } -keep class io.mockk.** { *; }
# Kodein
-keep, allowobfuscation, allowoptimization class org.kodein.type.TypeReference
-keep, allowobfuscation, allowoptimization class org.kodein.type.JVMAbstractTypeToken$Companion$WrappingTest
-keep, allowobfuscation, allowoptimization class * extends org.kodein.type.TypeReference
-keep, allowobfuscation, allowoptimization class * extends org.kodein.type.JVMAbstractTypeToken$Companion$WrappingTest
@@ -0,0 +1,109 @@
package bou.amine.apps.readerforselfossv2.android
import android.content.Context
import androidx.annotation.ArrayRes
import androidx.test.espresso.Espresso.onData
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.replaceText
import androidx.test.espresso.action.ViewActions.typeTextIntoFocusedView
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isNotChecked
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.Matchers.hasToString
fun performLogin(someUrl: String? = null) {
onView(withId(R.id.urlView)).perform(click()).perform(
typeTextIntoFocusedView(
if (!someUrl.isNullOrEmpty()) someUrl else "http://10.0.2.2:8888"
)
)
onView(withId(R.id.signInButton)).perform(click())
}
fun loginAndInitHome() {
performLogin()
onView(withText(R.string.gdpr_dialog_title)).check(matches(isDisplayed()))
onView(withText("OK")).perform(click())
}
fun changeAndCancelSetting(oldValue: String, newValue: String, openSettingItem: () -> Unit) {
openSettingItem()
onView(
withId(android.R.id.edit)
).perform(replaceText(newValue))
onView(
withId(android.R.id.button2)
).perform(click())
openSettingItem()
onView(
withId(android.R.id.edit)
).check(matches(withText(oldValue)))
onView(
withText(newValue)
).check(doesNotExist())
onView(
withId(android.R.id.button2)
).perform(click())
}
fun changeAndSaveSetting(oldValue: String, newValue: String, openSettingItem: () -> Unit) {
openSettingItem()
onView(
withId(android.R.id.edit)
).perform(replaceText(newValue))
onView(
withId(android.R.id.button1)
).perform(click())
openSettingItem()
onView(
withId(android.R.id.edit)
).check(matches(withText(newValue)))
if (oldValue.isNotEmpty()) {
onView(
withText(oldValue)
).check(doesNotExist())
}
onView(
withId(android.R.id.button2)
).perform(click())
}
fun testPreferencesFromArray(
context: Context,
@ArrayRes arrayRes: Int,
openSettingItem: () -> Unit
) {
openSettingItem()
context.resources.getStringArray(arrayRes).forEach { res ->
onView(withText(res)).check(matches(allOf(isDisplayed(), isNotChecked())))
onView(withText(res)).perform(click())
onView(withText(res)).check(doesNotExist())
openSettingItem()
onView(withText(res)).check(matches(allOf(isDisplayed(), isChecked())))
}
}
fun testAddSourceWithUrl(url: String, sourceName: String) {
onView(withId(R.id.fab))
.perform(click())
onView(withId(R.id.nameInput))
.perform(click()).perform(typeTextIntoFocusedView(sourceName))
onView(withId(R.id.sourceUri))
.perform(click())
.perform(typeTextIntoFocusedView(url))
onView(withId(R.id.tags))
.perform(click()).perform(typeTextIntoFocusedView("tag1,tag2,tag3"))
onView(withId(R.id.spoutsSpinner))
.perform(click())
onData(hasToString("RSS Feed")).perform(click())
onView(withId(R.id.saveBtn))
.perform(click())
onView(withText(sourceName)).check(matches(isDisplayed()))
}
@@ -0,0 +1,120 @@
package bou.amine.apps.readerforselfossv2.android
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isClickable
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isFocused
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.isSelected
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import org.hamcrest.CoreMatchers.not
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@LargeTest
class HomeActivityTest {
@get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java)
@Before
fun init() {
loginAndInitHome()
}
@Test
fun testMenu() {
onView(withId(R.id.action_search)).check(matches(isDisplayed())).check(
matches(
isClickable()
)
)
onView(withId(R.id.action_filter)).check(matches(isDisplayed())).check(
matches(
isClickable()
)
)
openMenu()
onView(withText(R.string.readAll)).check(matches(isDisplayed()))
onView(withText(R.string.menu_home_sources)).check(matches(isDisplayed()))
onView(withText(R.string.title_activity_settings)).check(matches(isDisplayed()))
onView(withText(R.string.menu_home_refresh)).check(matches(isDisplayed()))
onView(withText(R.string.issue_tracker_link)).check(matches(isDisplayed()))
onView(withText(R.string.action_disconnect)).check(matches(isDisplayed()))
}
@Test
fun testMenuActions() {
onView(withId(R.id.action_search)).perform(click())
onView(
withId(R.id.search_src_text)
).check(matches(isFocused()))
onView(isRoot()).perform(ViewActions.pressBack())
onView(withId(R.id.action_filter)).perform(click())
onView(
withText(R.string.filter_item_sources)
).check(matches(isDisplayed()))
onView(
withText(R.string.filter_item_tags)
).check(matches(isDisplayed()))
onView(
withId(R.id.floatingActionButton2)
).check(matches(isDisplayed()))
onView(isRoot()).perform(ViewActions.pressBack())
openMenu()
onView(withText(R.string.readAll)).perform(click())
onView(withText(R.string.markall_dialog_message)).check(matches(isDisplayed()))
onView(isRoot()).perform(ViewActions.pressBack())
openMenu()
onView(withText(R.string.menu_home_sources)).perform(click())
onView(withId(R.id.fab)).check(matches(isDisplayed()))
onView(isRoot()).perform(ViewActions.pressBack())
openMenu()
onView(withText(R.string.title_activity_settings)).perform(click())
onView(withText(R.string.pref_header_general)).check(matches(isDisplayed()))
onView(isRoot()).perform(ViewActions.pressBack())
openMenu()
onView(withText(R.string.menu_home_refresh)).perform(click())
onView(withText(R.string.refresh_dialog_message)).check(matches(isDisplayed()))
onView(isRoot()).perform(ViewActions.pressBack())
openMenu()
/*onView(withText(R.string.issue_tracker_link)).perform(click())
onView(withText(R.string.markall_dialog_message)).check(matches(isDisplayed()))
onView(isRoot()).perform(ViewActions.pressBack())
openMenu()*/
onView(withText(R.string.action_disconnect)).perform(click())
onView(withText(R.string.confirm_disconnect_title)).check(matches(isDisplayed()))
}
@Test
fun testEmptyView() {
onView(withId(R.id.emptyText)).check(matches(isDisplayed()))
onView(
hasBottombarItemText(R.string.tab_new)
).check(matches(isDisplayed())).check(matches(isSelected()))
onView(
hasBottombarItemText(R.string.tab_read)
).check(matches(isDisplayed())).check(matches(not(isSelected())))
onView(
hasBottombarItemText(R.string.tab_favs)
).check(matches(isDisplayed())).check(matches(not(isSelected())))
}
}
@@ -0,0 +1,83 @@
package bou.amine.apps.readerforselfossv2.android
import android.app.Activity
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isClickable
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isNotChecked
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@LargeTest
class LoginActivityTest {
@get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java)
private fun getActivity(): Activity? {
var activity: Activity? = null
activityRule.scenario.onActivity {
activity = it
}
return activity
}
@Before
fun registerIdlingResource() {
IdlingRegistry.getInstance()
.register(CountingIdlingResourceSingleton.countingIdlingResource)
}
@After
fun unregisterIdlingResource() {
IdlingRegistry.getInstance()
.unregister(CountingIdlingResourceSingleton.countingIdlingResource)
}
@Test
fun viewIsInitialized() {
onView(withId(R.id.urlView)).check(matches(isDisplayed()))
onView(withId(R.id.selfSigned)).check(matches(isDisplayed())).check(matches(isNotChecked()))
.check(
matches(isClickable())
)
onView(withId(R.id.withLogin)).check(matches(isDisplayed()))
.check(matches(isNotChecked())).check(
matches(isClickable())
)
}
@Test
fun urlError() {
performLogin("10.0.2.2:8888")
onView(withId(R.id.urlView)).perform(click())
onView(withId(R.id.urlView)).check(matches(withError(R.string.wrong_infos)))
}
@Test
fun multiError() {
onView(withId(R.id.signInButton)).perform(click())
onView(withId(R.id.signInButton)).perform(click())
onView(withId(R.id.signInButton)).perform(click())
onView(withText(R.string.warning_wrong_url)).check(matches(isDisplayed()))
}
@Test
fun connect() {
performLogin()
onView(withText(R.string.gdpr_dialog_title)).check(matches(isDisplayed()))
}
}
@@ -0,0 +1,174 @@
package bou.amine.apps.readerforselfossv2.android
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.replaceText
import androidx.test.espresso.action.ViewActions.swipeUp
import androidx.test.espresso.action.ViewActions.typeTextIntoFocusedView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isEnabled
import androidx.test.espresso.matcher.ViewMatchers.isFocused
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.not
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@LargeTest
class SettingsActivityGeneralTest {
@get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java)
@Before
fun init() {
loginAndInitHome()
openActionBarOverflowOrOptionsMenu(
ApplicationProvider.getApplicationContext()
)
onView(withText(R.string.title_activity_settings)).perform(click())
onView(withText(R.string.pref_header_general)).perform(click())
}
@Test
fun testGeneral() {
onView(withText(R.string.pref_api_items_number_title)).check(matches(isDisplayed()))
onView(
withSettingsCheckboxWidget(R.string.pref_general_infinite_loading_title)
).check(
matches(
allOf(
isDisplayed(), not(isChecked())
)
)
)
onView(withText(R.string.pref_general_category_links)).check(matches(isDisplayed()))
onView(withSettingsCheckboxWidget(R.string.pref_article_viewer_title)).check(
matches(
allOf(
isDisplayed(), isChecked()
)
)
)
onView(withSettingsCheckboxWidget(R.string.reader_static_bar_title)).check(
matches(
allOf(
isDisplayed(), not(isChecked())
)
)
)
onView(withSettingsCheckboxFrame(R.string.reader_static_bar_title)).check(
matches(
isEnabled()
)
)
onView(withText(R.string.pref_general_category_displaying)).check(matches(isDisplayed()))
onView(withSettingsCheckboxWidget(R.string.pref_switch_card_view_title)).check(
matches(
allOf(
isDisplayed(), not(isChecked())
)
)
)
onView(withSettingsCheckboxWidget(R.string.card_height_title)).check(
matches(
allOf(
isDisplayed(), not(isChecked())
)
)
)
onView(withSettingsCheckboxFrame(R.string.card_height_title)).check(
matches(
not(isEnabled())
)
)
onView(withSettingsCheckboxWidget(R.string.switch_unread_count_title)).check(
matches(
allOf(
isDisplayed(), isChecked()
)
)
)
onView(withId(R.id.settings)).perform(swipeUp())
onView(withSettingsCheckboxWidget(R.string.display_all_counts_title)).check(
matches(
allOf(
isDisplayed(), not(isChecked())
)
)
)
}
@Test
fun testGeneralActionsNumberItems() {
onView(withText(R.string.pref_api_items_number_title)).perform(click())
onView(withId(android.R.id.edit)).check(matches(isFocused()))
// Value check
onView(
withId(android.R.id.edit)
).perform(replaceText("AVC"))
.check(matches(withText("")))
// TODO: should check message error. Not working for api level 30+
onView(
withId(android.R.id.edit)
).perform(replaceText("-1"))
.check(matches(withText("")))
// TODO: should check message error. Not working for api level 30+
onView(
withId(android.R.id.edit)
).perform(replaceText("300"))
.check(matches(withText("")))
onView(
withId(android.R.id.edit)
).perform(typeTextIntoFocusedView("300"))
.check(matches(withText("30")))
onView(
withId(android.R.id.edit)
).perform(replaceText("10"))
.check(matches(withText("10")))
onView(isRoot()).perform(ViewActions.pressBack())
// Value saving
changeAndCancelSetting("20", "10") {
onView(withText(R.string.pref_api_items_number_title)).perform(click())
}
changeAndSaveSetting("20", "10") {
onView(withText(R.string.pref_api_items_number_title)).perform(click())
}
}
@Test
fun testGeneralActionsCheckboxes() {
// article viewer settings
onView(withSettingsCheckboxFrame(R.string.reader_static_bar_title)).check(
matches(
isEnabled()
)
)
onView(withSettingsCheckboxWidget(R.string.pref_article_viewer_title)).perform(click())
onView(withSettingsCheckboxFrame(R.string.reader_static_bar_title)).check(
matches(
not(isEnabled())
)
)
onView(withSettingsCheckboxFrame(R.string.card_height_title)).check(matches(not(isEnabled())))
onView(withSettingsCheckboxWidget(R.string.pref_switch_card_view_title)).perform(click())
onView(withSettingsCheckboxFrame(R.string.card_height_title)).check(matches(isEnabled()))
}
}
@@ -0,0 +1,169 @@
package bou.amine.apps.readerforselfossv2.android
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isEnabled
import androidx.test.espresso.matcher.ViewMatchers.isNotEnabled
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.not
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@LargeTest
class SettingsActivityOfflineTest {
@get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java)
lateinit var context: Context
@Before
fun init() {
activityRule.scenario.onActivity { activity ->
context = activity.window.context
}
loginAndInitHome()
openActionBarOverflowOrOptionsMenu(
ApplicationProvider.getApplicationContext()
)
onView(withText(R.string.title_activity_settings)).perform(click())
onView(withText(R.string.pref_header_offline)).perform(click())
}
@Test
fun testOffline() {
onView(withSettingsCheckboxWidget(R.string.pref_switch_periodic_refresh)).check(
matches(
allOf(
isDisplayed(), not(isChecked())
)
)
)
onView(withSettingsCheckboxWidget(R.string.pref_switch_items_caching)).check(
matches(
allOf(
isDisplayed(), not(isChecked())
)
)
)
onView(withSettingsCheckboxFrame(R.string.pref_switch_items_caching)).check(
matches(
isEnabled()
)
)
onView(withText(R.string.pref_periodic_refresh_minutes_title)).check(
matches(
allOf(isNotEnabled(), isDisplayed())
)
)
onView(withSettingsCheckboxWidget(R.string.pref_switch_refresh_when_charging)).check(
matches(
allOf(
isDisplayed(), not(isChecked())
)
)
)
onView(withSettingsCheckboxFrame(R.string.pref_switch_refresh_when_charging)).check(
matches(
isNotEnabled()
)
)
onView(withSettingsCheckboxWidget(R.string.pref_switch_notify_new_items)).check(
matches(
allOf(
isDisplayed(), not(isChecked())
)
)
)
onView(withSettingsCheckboxFrame(R.string.pref_switch_notify_new_items)).check(
matches(
isNotEnabled()
)
)
onView(withSettingsCheckboxWidget(R.string.pref_switch_update_sources)).check(
matches(
allOf(
isDisplayed(), isChecked()
)
)
)
}
@Test
fun testOfflineActions() {
onView(withText(R.string.pref_switch_items_caching_off)).check(matches(isDisplayed()))
onView(withText(R.string.pref_switch_items_caching)).perform(click())
onView(withText(R.string.pref_switch_items_caching_on)).check(matches(isDisplayed()))
onView(withSettingsCheckboxFrame(R.string.pref_switch_items_caching)).check(
matches(
isEnabled()
)
)
onView(withText(R.string.pref_periodic_refresh_minutes_title)).check(
matches(
isNotEnabled()
)
)
onView(withSettingsCheckboxFrame(R.string.pref_switch_refresh_when_charging)).check(
matches(
isNotEnabled()
)
)
onView(withSettingsCheckboxFrame(R.string.pref_switch_notify_new_items)).check(
matches(
isNotEnabled()
)
)
onView(withText(R.string.pref_switch_periodic_refresh_off)).check(
matches(
isDisplayed()
)
)
onView(withSettingsCheckboxWidget(R.string.pref_switch_periodic_refresh)).perform(click())
onView(withText(R.string.pref_switch_periodic_refresh_on)).check(
matches(
isDisplayed()
)
)
onView(withSettingsCheckboxFrame(R.string.pref_periodic_refresh_minutes_title)).check(
matches(
isEnabled()
)
)
onView(withSettingsCheckboxFrame(R.string.pref_switch_refresh_when_charging)).check(
matches(
isEnabled()
)
)
onView(withSettingsCheckboxFrame(R.string.pref_switch_notify_new_items)).check(
matches(
isEnabled()
)
)
changeAndCancelSetting("360", "123") {
onView(withText(R.string.pref_periodic_refresh_minutes_title)).perform(click())
}
changeAndSaveSetting("360", "123") {
onView(withText(R.string.pref_periodic_refresh_minutes_title)).perform(click())
}
onView(withSettingsCheckboxFrame(R.string.pref_switch_refresh_when_charging)).perform(click())
onView(withSettingsCheckboxFrame(R.string.pref_switch_notify_new_items)).perform(click())
onView(withSettingsCheckboxWidget(R.string.pref_switch_update_sources)).perform(click())
}
}
@@ -0,0 +1,86 @@
package bou.amine.apps.readerforselfossv2.android
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.not
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@LargeTest
class SettingsActivityReaderTest {
@get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java)
lateinit var context: Context
@Before
fun init() {
activityRule.scenario.onActivity { activity ->
context = activity.window.context
}
loginAndInitHome()
openActionBarOverflowOrOptionsMenu(
ApplicationProvider.getApplicationContext()
)
onView(withText(R.string.title_activity_settings)).perform(click())
onView(withText(R.string.pref_header_viewer)).perform(click())
}
@Test
fun testReader() {
onView(withSettingsCheckboxFrame(R.string.pref_switch_actions_pager_scroll)).check(
matches(
allOf(
isDisplayed(), not(
isChecked()
)
)
)
)
onView(withText(R.string.pref_content_reader_font_size)).check(matches(isDisplayed()))
onView(withText(R.string.settings_reader_font)).check(matches(isDisplayed()))
}
@Test
fun testReaderActions() {
onView(withText(R.string.pref_switch_actions_pager_scroll_off)).check(
matches(
isDisplayed()
)
)
onView(withSettingsCheckboxFrame(R.string.pref_switch_actions_pager_scroll)).perform(click())
onView(withText(R.string.pref_switch_actions_pager_scroll_on)).check(
matches(
isDisplayed()
)
)
onView(withText(R.string.pref_content_reader_font_size)).perform(click())
changeAndCancelSetting("16", "10") {
onView(withText(R.string.pref_content_reader_font_size)).perform(click())
}
changeAndSaveSetting("16", "10") {
onView(withText(R.string.pref_content_reader_font_size)).perform(click())
}
testPreferencesFromArray(context, R.array.preloaded_fonts_values) {
onView(withText(R.string.settings_reader_font)).perform(click())
}
}
}
@@ -0,0 +1,100 @@
package bou.amine.apps.readerforselfossv2.android
import android.content.Context
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isSelected
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.not
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@LargeTest
class SettingsActivityTest {
@get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java)
lateinit var context: Context
@Before
fun init() {
activityRule.scenario.onActivity { activity ->
context = activity.window.context
}
loginAndInitHome()
openMenu()
onView(withText(R.string.title_activity_settings)).perform(click())
}
@Test
fun testAllSettings() {
onView(withText(R.string.pref_header_general)).check(matches(isDisplayed()))
onView(withText(R.string.pref_header_viewer)).check(matches(isDisplayed()))
onView(withText(R.string.pref_header_offline)).check(matches(isDisplayed()))
onView(withText(R.string.pref_header_theme)).check(matches(isDisplayed()))
onView(withText(R.string.pref_header_links)).check(matches(isDisplayed()))
onView(withText(R.string.pref_switch_disable_acra)).check(
matches(
allOf(
isDisplayed(),
not(isSelected())
)
)
)
onView(withText(R.string.action_about)).check(matches(isDisplayed()))
}
@Test
fun testThemes() {
testPreferencesFromArray(context, R.array.ModeTitles) {
onView(withText(R.string.pref_header_theme)).perform(click())
}
}
@Test
fun testExperimentail() {
onView(withText(R.string.pref_header_experimental)).perform(click())
changeAndCancelSetting("", "10") {
onView(withText(R.string.pref_api_timeout)).perform(click())
}
changeAndSaveSetting("", "10") {
onView(withText(R.string.pref_api_timeout)).perform(click())
}
}
@Test
fun testBugReports() {
onView(withText(R.string.pref_switch_disable_acra)).perform(click())
}
@Test
fun testLinks() {
onView(withText(R.string.pref_header_links)).perform(click())
onView(withText(R.string.issue_tracker_link)).check(matches(isDisplayed()))
onView(withText(R.string.issue_tracker_summary)).check(matches(isDisplayed()))
onView(withText(R.string.source_code)).check(matches(isDisplayed()))
onView(withText(R.string.translation)).check(matches(isDisplayed()))
}
@Test
fun testAbout() {
onView(withText(R.string.action_about)).perform(click())
onView(withText("ACRA")).check(matches(isDisplayed()))
}
}
@@ -0,0 +1,83 @@
package bou.amine.apps.readerforselfossv2.android
import androidx.test.espresso.AmbiguousViewMatcherException
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.swipeDown
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import java.util.UUID
@RunWith(AndroidJUnit4::class)
@LargeTest
class SourcesActivityTest {
@get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java)
lateinit var sourceName: String
@Before
fun init() {
sourceName = UUID.randomUUID().toString().substring(0, 15)
loginAndInitHome()
goToSources()
}
@Test
fun addSource() {
testAddSourceWithUrl(
"https://lorem-rss.herokuapp.com/feed?unit=year&interval=1&length=10",
sourceName
)
}
@Test
fun addSourceCheckContent() {
testAddSourceWithUrl("https://news.google.com/rss?hl=en-US&gl=US&ceid=US:en", sourceName)
onView(isRoot()).perform(ViewActions.pressBack())
openMenu()
onView(withText(R.string.menu_home_refresh)).perform(click())
onView(withText(R.string.refresh_dialog_message)).check(matches(isDisplayed()))
onView(
withId(android.R.id.button1)
).perform(click())
Thread.sleep(10000)
onView(withId(R.id.swipeRefreshLayout)).perform(swipeDown())
Thread.sleep(2000)
try {
onView(withId(R.id.sourceTitleAndDate)).check(matches(isDisplayed()))
} catch (e: AmbiguousViewMatcherException) {
assert(true)
}
goToSources()
}
@After
fun deleteTheCreatedSource() {
onView(withText(sourceName)).check(matches(isDisplayed()))
onView(withId(R.id.deleteBtn)).perform(click())
onView(withText(sourceName)).check(doesNotExist())
}
private fun goToSources() {
openMenu()
onView(withText(R.string.menu_home_sources))
.perform(click())
}
}
@@ -0,0 +1,110 @@
package bou.amine.apps.readerforselfossv2.android
import android.content.Context
import android.view.View
import android.widget.EditText
import android.widget.ImageView
import android.widget.RelativeLayout
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.core.graphics.drawable.toBitmap
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
import androidx.test.espresso.Root
import androidx.test.espresso.matcher.RootMatchers.isPlatformPopup
import androidx.test.espresso.matcher.ViewMatchers.hasSibling
import androidx.test.espresso.matcher.ViewMatchers.withChild
import androidx.test.espresso.matcher.ViewMatchers.withClassName
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withParent
import androidx.test.espresso.matcher.ViewMatchers.withResourceName
import androidx.test.espresso.matcher.ViewMatchers.withText
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.Description
import org.hamcrest.Matcher
import org.hamcrest.Matchers
import org.hamcrest.TypeSafeMatcher
fun withError(@StringRes id: Int): TypeSafeMatcher<View?> {
return object : TypeSafeMatcher<View?>() {
override fun matchesSafely(view: View?): Boolean {
if (view == null) {
return false
}
val context = view.context
if (view !is EditText) {
return false
}
if (view.error == null) {
return false
}
return view.error.toString() == context.getString(id)
}
override fun describeTo(description: Description?) {
}
}
}
fun isPopupWindow(): Matcher<Root> {
return isPlatformPopup()
}
fun withDrawable(@DrawableRes id: Int) = object : TypeSafeMatcher<View>() {
override fun describeTo(description: Description) {
description.appendText("ImageView with drawable same as drawable with id $id")
}
override fun matchesSafely(view: View): Boolean {
val context = view.context
val expectedBitmap = context.getDrawable(id)!!.toBitmap()
try {
return view is ImageView && view.drawable.toBitmap().sameAs(expectedBitmap)
} catch (e: Exception) {
return false
}
}
}
fun hasBottombarItemText(@StringRes id: Int): Matcher<View>? {
return allOf(
withResourceName("fixed_bottom_navigation_icon"),
withParent(
allOf(
withResourceName("fixed_bottom_navigation_icon_container"),
hasSibling(withText(id))
)
)
)
}
fun withSettingsCheckboxWidget(@StringRes id: Int): Matcher<View>? {
return allOf(
withId(android.R.id.switch_widget),
withParent(
withSettingsCheckboxFrame(id)
)
)
}
fun withSettingsCheckboxFrame(@StringRes id: Int): Matcher<View>? {
return allOf(
withId(android.R.id.widget_frame),
hasSibling(
allOf(
withClassName(Matchers.equalTo(RelativeLayout::class.java.name)),
withChild(
withText(id)
)
)
)
)
}
fun openMenu() {
openActionBarOverflowOrOptionsMenu(
ApplicationProvider.getApplicationContext<Context>()
)
}
@@ -12,7 +12,11 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.view.doOnNextLayout import androidx.core.view.doOnNextLayout
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.* import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import androidx.work.Constraints import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder import androidx.work.PeriodicWorkRequestBuilder
@@ -24,8 +28,10 @@ import bou.amine.apps.readerforselfossv2.android.background.LoadingWorker
import bou.amine.apps.readerforselfossv2.android.databinding.ActivityHomeBinding import bou.amine.apps.readerforselfossv2.android.databinding.ActivityHomeBinding
import bou.amine.apps.readerforselfossv2.android.fragments.FilterSheetFragment import bou.amine.apps.readerforselfossv2.android.fragments.FilterSheetFragment
import bou.amine.apps.readerforselfossv2.android.settings.SettingsActivity import bou.amine.apps.readerforselfossv2.android.settings.SettingsActivity
import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton
import bou.amine.apps.readerforselfossv2.android.utils.bottombar.maybeShow import bou.amine.apps.readerforselfossv2.android.utils.bottombar.maybeShow
import bou.amine.apps.readerforselfossv2.android.utils.bottombar.removeBadge import bou.amine.apps.readerforselfossv2.android.utils.bottombar.removeBadge
import bou.amine.apps.readerforselfossv2.android.utils.openUrlInBrowser
import bou.amine.apps.readerforselfossv2.model.SelfossModel import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.service.AppSettingsService import bou.amine.apps.readerforselfossv2.service.AppSettingsService
@@ -43,9 +49,7 @@ import org.kodein.di.instance
import java.security.MessageDigest import java.security.MessageDigest
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAware { class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAware {
private var items: ArrayList<SelfossModel.Item> = ArrayList() private var items: ArrayList<SelfossModel.Item> = ArrayList()
private var elementsShown: ItemType = ItemType.UNREAD private var elementsShown: ItemType = ItemType.UNREAD
@@ -59,11 +63,12 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
private lateinit var recyclerViewScrollListener: RecyclerView.OnScrollListener private lateinit var recyclerViewScrollListener: RecyclerView.OnScrollListener
private lateinit var binding: ActivityHomeBinding private lateinit var binding: ActivityHomeBinding
private var recyclerAdapter: RecyclerView.Adapter<*>? = null private var recyclerAdapter: ItemsAdapter<out RecyclerView.ViewHolder>? = null
private var fromTabShortcut: Boolean = false private var fromTabShortcut: Boolean = false
private val settingsLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { private val settingsLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
appSettingsService.refreshUserSettings() appSettingsService.refreshUserSettings()
} }
@@ -71,7 +76,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
private val repository: Repository by instance() private val repository: Repository by instance()
private val appSettingsService: AppSettingsService by instance() private val appSettingsService: AppSettingsService by instance()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityHomeBinding.inflate(layoutInflater) binding = ActivityHomeBinding.inflate(layoutInflater)
@@ -81,7 +85,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
repository.offlineOverride = intent.getBooleanExtra("startOffline", false) repository.offlineOverride = intent.getBooleanExtra("startOffline", false)
if (fromTabShortcut) { if (fromTabShortcut) {
elementsShown = ItemType.fromInt(intent.getIntExtra("shortcutTab", ItemType.UNREAD.position)) elementsShown =
ItemType.fromInt(intent.getIntExtra("shortcutTab", ItemType.UNREAD.position))
} }
setContentView(view) setContentView(view)
@@ -92,10 +97,11 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
handleSwipeRefreshLayout() handleSwipeRefreshLayout()
if (appSettingsService.isItemCachingEnabled()) { if (appSettingsService.isItemCachingEnabled()) {
CountingIdlingResourceSingleton.increment()
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
repository.tryToCacheItemsAndGetNewOnes() repository.tryToCacheItemsAndGetNewOnes()
CountingIdlingResourceSingleton.decrement()
} }
} }
} }
@@ -104,18 +110,21 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
binding.swipeRefreshLayout.setColorSchemeResources( binding.swipeRefreshLayout.setColorSchemeResources(
R.color.refresh_progress_1, R.color.refresh_progress_1,
R.color.refresh_progress_2, R.color.refresh_progress_2,
R.color.refresh_progress_3 R.color.refresh_progress_3,
) )
binding.swipeRefreshLayout.setOnRefreshListener { binding.swipeRefreshLayout.setOnRefreshListener {
repository.offlineOverride = false repository.offlineOverride = false
lastFetchDone = false lastFetchDone = false
CountingIdlingResourceSingleton.increment()
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
getElementsAccordingToTab() getElementsAccordingToTab()
binding.swipeRefreshLayout.isRefreshing = false binding.swipeRefreshLayout.isRefreshing = false
CountingIdlingResourceSingleton.decrement()
} }
} }
val swipeDirs = if (appSettingsService.getPublicAccess()) { val swipeDirs =
if (appSettingsService.getPublicAccess()) {
0 0
} else { } else {
ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
@@ -124,28 +133,31 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
val simpleItemTouchCallback = val simpleItemTouchCallback =
object : ItemTouchHelper.SimpleCallback( object : ItemTouchHelper.SimpleCallback(
0, 0,
swipeDirs swipeDirs,
) { ) {
override fun getSwipeDirs( override fun getSwipeDirs(
recyclerView: RecyclerView, recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder viewHolder: RecyclerView.ViewHolder,
): Int = ): Int =
if (elementsShown == ItemType.STARRED) { if (elementsShown == ItemType.STARRED) {
0 0
} else { } else {
super.getSwipeDirs( super.getSwipeDirs(
recyclerView, recyclerView,
viewHolder viewHolder,
) )
} }
override fun onMove( override fun onMove(
recyclerView: RecyclerView, recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder, viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder target: RecyclerView.ViewHolder,
): Boolean = false ): Boolean = false
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, swipeDir: Int) { override fun onSwiped(
viewHolder: RecyclerView.ViewHolder,
swipeDir: Int,
) {
val position = viewHolder.bindingAdapterPosition val position = viewHolder.bindingAdapterPosition
val i = items.elementAtOrNull(position) val i = items.elementAtOrNull(position)
@@ -162,7 +174,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
Toast.makeText( Toast.makeText(
this@HomeActivity, this@HomeActivity,
"Found null when swiping at positon $position.", "Found null when swiping at positon $position.",
Toast.LENGTH_LONG Toast.LENGTH_LONG,
).show() ).show()
} }
} }
@@ -171,7 +183,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
ItemTouchHelper(simpleItemTouchCallback).attachToRecyclerView(binding.recyclerView) ItemTouchHelper(simpleItemTouchCallback).attachToRecyclerView(binding.recyclerView)
} }
private fun updateBottomBarBadgeCount(badge: TextBadgeItem, count: Int) { private fun updateBottomBarBadgeCount(
badge: TextBadgeItem,
count: Int,
) {
if (count > 0) { if (count > 0) {
badge badge
.setText(count.toString()) .setText(count.toString())
@@ -182,14 +197,16 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
} }
private fun handleBottomBar() { private fun handleBottomBar() {
tabNewBadge =
tabNewBadge = TextBadgeItem() TextBadgeItem()
.setText("") .setText("")
.setHideOnSelect(false).hide(false) .setHideOnSelect(false).hide(false)
tabArchiveBadge = TextBadgeItem() tabArchiveBadge =
TextBadgeItem()
.setText("") .setText("")
.setHideOnSelect(false).hide(false) .setHideOnSelect(false).hide(false)
tabStarredBadge = TextBadgeItem() tabStarredBadge =
TextBadgeItem()
.setText("") .setText("")
.setHideOnSelect(false).hide(false) .setHideOnSelect(false).hide(false)
@@ -218,19 +235,19 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
val tabNew = val tabNew =
BottomNavigationItem( BottomNavigationItem(
R.drawable.ic_tab_fiber_new_black_24dp, R.drawable.ic_tab_fiber_new_black_24dp,
getString(R.string.tab_new) getString(R.string.tab_new),
) )
.setBadgeItem(tabNewBadge) .setBadgeItem(tabNewBadge)
val tabArchive = val tabArchive =
BottomNavigationItem( BottomNavigationItem(
R.drawable.ic_tab_archive_black_24dp, R.drawable.ic_tab_archive_black_24dp,
getString(R.string.tab_read) getString(R.string.tab_read),
) )
.setBadgeItem(tabArchiveBadge) .setBadgeItem(tabArchiveBadge)
val tabStarred = val tabStarred =
BottomNavigationItem( BottomNavigationItem(
R.drawable.ic_tab_favorite_black_24dp, R.drawable.ic_tab_favorite_black_24dp,
getString(R.string.tab_favs) getString(R.string.tab_favs),
).setActiveColorResource(R.color.pink) ).setActiveColorResource(R.color.pink)
.setBadgeItem(tabStarredBadge) .setBadgeItem(tabStarredBadge)
@@ -263,15 +280,15 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
handleGDPRDialog(appSettingsService.settings.getBoolean("GDPR_shown", false)) handleGDPRDialog(appSettingsService.settings.getBoolean("GDPR_shown", false))
handleRecurringTask() handleRecurringTask()
CountingIdlingResourceSingleton.increment()
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
repository.handleDBActions() repository.handleDBActions()
CountingIdlingResourceSingleton.decrement()
} }
getElementsAccordingToTab() getElementsAccordingToTab()
} }
private fun handleGDPRDialog(GDPRShown: Boolean) { private fun handleGDPRDialog(GDPRShown: Boolean) {
val messageDigest: MessageDigest = MessageDigest.getInstance("SHA-256") val messageDigest: MessageDigest = MessageDigest.getInstance("SHA-256")
messageDigest.update(appSettingsService.getBaseUrl().toByteArray()) messageDigest.update(appSettingsService.getBaseUrl().toByteArray())
@@ -281,7 +298,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
alertDialog.setMessage(getString(R.string.gdpr_dialog_message)) alertDialog.setMessage(getString(R.string.gdpr_dialog_message))
alertDialog.setButton( alertDialog.setButton(
AlertDialog.BUTTON_NEUTRAL, AlertDialog.BUTTON_NEUTRAL,
"OK" "OK",
) { dialog, _ -> ) { dialog, _ ->
appSettingsService.settings.putBoolean("GDPR_shown", true) appSettingsService.settings.putBoolean("GDPR_shown", true)
dialog.dismiss() dialog.dismiss()
@@ -298,34 +315,40 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
when (currentManager) { when (currentManager) {
is StaggeredGridLayoutManager -> is StaggeredGridLayoutManager ->
if (!appSettingsService.isCardViewEnabled()) { if (!appSettingsService.isCardViewEnabled()) {
layoutManager = GridLayoutManager( layoutManager =
GridLayoutManager(
this, this,
calculateNoOfColumns() calculateNoOfColumns(),
) )
binding.recyclerView.layoutManager = layoutManager binding.recyclerView.layoutManager = layoutManager
} }
is GridLayoutManager -> is GridLayoutManager ->
if (appSettingsService.isCardViewEnabled()) { if (appSettingsService.isCardViewEnabled()) {
layoutManager = StaggeredGridLayoutManager( layoutManager =
StaggeredGridLayoutManager(
calculateNoOfColumns(), calculateNoOfColumns(),
StaggeredGridLayoutManager.VERTICAL StaggeredGridLayoutManager.VERTICAL,
) )
layoutManager.gapStrategy = layoutManager.gapStrategy =
StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
binding.recyclerView.layoutManager = layoutManager binding.recyclerView.layoutManager = layoutManager
} }
else -> else ->
if (currentManager == null) { if (currentManager == null) {
if (!appSettingsService.isCardViewEnabled()) { if (!appSettingsService.isCardViewEnabled()) {
layoutManager = GridLayoutManager( layoutManager =
GridLayoutManager(
this, this,
calculateNoOfColumns() calculateNoOfColumns(),
) )
binding.recyclerView.layoutManager = layoutManager binding.recyclerView.layoutManager = layoutManager
} else { } else {
layoutManager = StaggeredGridLayoutManager( layoutManager =
StaggeredGridLayoutManager(
calculateNoOfColumns(), calculateNoOfColumns(),
StaggeredGridLayoutManager.VERTICAL StaggeredGridLayoutManager.VERTICAL,
) )
layoutManager.gapStrategy = layoutManager.gapStrategy =
StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
@@ -336,11 +359,11 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
} }
private fun handleBottomBarActions() { private fun handleBottomBarActions() {
binding.bottomBar.setTabSelectedListener(object : BottomNavigationBar.OnTabSelectedListener { binding.bottomBar.setTabSelectedListener(
object : BottomNavigationBar.OnTabSelectedListener {
override fun onTabUnselected(position: Int) = Unit override fun onTabUnselected(position: Int) = Unit
override fun onTabReselected(position: Int) { override fun onTabReselected(position: Int) {
when (val layoutManager = binding.recyclerView.adapter) { when (val layoutManager = binding.recyclerView.adapter) {
is StaggeredGridLayoutManager -> is StaggeredGridLayoutManager ->
if (layoutManager.findFirstCompletelyVisibleItemPositions(null)[0] == 0) { if (layoutManager.findFirstCompletelyVisibleItemPositions(null)[0] == 0) {
@@ -348,12 +371,14 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
} else { } else {
layoutManager.scrollToPositionWithOffset(0, 0) layoutManager.scrollToPositionWithOffset(0, 0)
} }
is GridLayoutManager -> is GridLayoutManager ->
if (layoutManager.findFirstCompletelyVisibleItemPosition() == 0) { if (layoutManager.findFirstCompletelyVisibleItemPosition() == 0) {
getElementsAccordingToTab() getElementsAccordingToTab()
} else { } else {
layoutManager.scrollToPositionWithOffset(0, 0) layoutManager.scrollToPositionWithOffset(0, 0)
} }
else -> Unit else -> Unit
} }
} }
@@ -368,7 +393,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
fetchOnEmptyList() fetchOnEmptyList()
} }
}) },
)
} }
fun fetchOnEmptyList() { fun fetchOnEmptyList() {
@@ -378,8 +404,13 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
} }
private fun handleInfiniteScroll() { private fun handleInfiniteScroll() {
recyclerViewScrollListener = object : RecyclerView.OnScrollListener() { recyclerViewScrollListener =
override fun onScrolled(localRecycler: RecyclerView, dx: Int, dy: Int) { object : RecyclerView.OnScrollListener() {
override fun onScrolled(
localRecycler: RecyclerView,
dx: Int,
dy: Int,
) {
if (dy > 0) { if (dy > 0) {
val lastVisibleItem = getLastVisibleItem() val lastVisibleItem = getLastVisibleItem()
@@ -396,9 +427,11 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
private fun getLastVisibleItem(): Int { private fun getLastVisibleItem(): Int {
return when (val manager = binding.recyclerView.layoutManager) { return when (val manager = binding.recyclerView.layoutManager) {
is StaggeredGridLayoutManager -> manager.findLastCompletelyVisibleItemPositions( is StaggeredGridLayoutManager ->
null manager.findLastCompletelyVisibleItemPositions(
null,
).last() ).last()
is GridLayoutManager -> manager.findLastCompletelyVisibleItemPosition() is GridLayoutManager -> manager.findLastCompletelyVisibleItemPosition()
else -> 0 else -> 0
} }
@@ -411,10 +444,9 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
binding.emptyText.visibility = View.GONE binding.emptyText.visibility = View.GONE
} }
fun getElementsAccordingToTab( fun getElementsAccordingToTab(appendResults: Boolean = false) {
appendResults: Boolean = false offset =
) { if (appendResults && items.size > 0) {
offset = if (appendResults && items.size > 0) {
items.size - 1 items.size - 1
} else { } else {
0 0
@@ -424,28 +456,37 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
getItems(appendResults, elementsShown) getItems(appendResults, elementsShown)
} }
private fun getItems(appendResults: Boolean, itemType: ItemType) { private fun getItems(
appendResults: Boolean,
itemType: ItemType,
) {
CountingIdlingResourceSingleton.increment()
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
binding.swipeRefreshLayout.isRefreshing = true binding.swipeRefreshLayout.isRefreshing = true
repository.displayedItems = itemType repository.displayedItems = itemType
items = if (appendResults) { items =
if (appendResults) {
repository.getOlderItems() repository.getOlderItems()
} else { } else {
repository.getNewerItems() repository.getNewerItems()
} }
binding.swipeRefreshLayout.isRefreshing = false binding.swipeRefreshLayout.isRefreshing = false
handleListResult() handleListResult()
CountingIdlingResourceSingleton.decrement()
} }
} }
private fun handleListResult(appendResults: Boolean = false) { private fun handleListResult(appendResults: Boolean = false) {
if (appendResults) { if (appendResults) {
val oldManager = binding.recyclerView.layoutManager val oldManager = binding.recyclerView.layoutManager
firstVisible = when (oldManager) { firstVisible =
when (oldManager) {
is StaggeredGridLayoutManager -> is StaggeredGridLayoutManager ->
oldManager.findFirstCompletelyVisibleItemPositions(null).last() oldManager.findFirstCompletelyVisibleItemPositions(null).last()
is GridLayoutManager -> is GridLayoutManager ->
oldManager.findFirstCompletelyVisibleItemPosition() oldManager.findFirstCompletelyVisibleItemPosition()
else -> 0 else -> 0
} }
} }
@@ -471,13 +512,13 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
binding.recyclerView.addItemDecoration( binding.recyclerView.addItemDecoration(
DividerItemDecoration( DividerItemDecoration(
this@HomeActivity, this@HomeActivity,
DividerItemDecoration.VERTICAL DividerItemDecoration.VERTICAL,
) ),
) )
} }
binding.recyclerView.adapter = recyclerAdapter binding.recyclerView.adapter = recyclerAdapter
} else { } else {
(recyclerAdapter as ItemsAdapter<*>).updateAllItems(items) recyclerAdapter!!.updateAllItems(items)
} }
reloadBadges() reloadBadges()
@@ -486,8 +527,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
private fun reloadBadges() { private fun reloadBadges() {
if (appSettingsService.isDisplayUnreadCountEnabled() || appSettingsService.isDisplayAllCountEnabled()) { if (appSettingsService.isDisplayUnreadCountEnabled() || appSettingsService.isDisplayAllCountEnabled()) {
CountingIdlingResourceSingleton.increment()
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
repository.reloadBadges() repository.reloadBadges()
CountingIdlingResourceSingleton.decrement()
} }
} }
} }
@@ -523,13 +566,17 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
} }
val searchItem = menu.findItem(R.id.action_search) val searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem.getActionView() as SearchView val searchView = searchItem.actionView as SearchView
searchView.setOnQueryTextListener(this) searchView.setOnQueryTextListener(this)
return true return true
} }
private fun needsConfirmation(titleRes: Int, messageRes: Int, doFn: () -> Unit) { private fun needsConfirmation(
titleRes: Int,
messageRes: Int,
doFn: () -> Unit,
) {
AlertDialog.Builder(this@HomeActivity) AlertDialog.Builder(this@HomeActivity)
.setMessage(messageRes) .setMessage(messageRes)
.setTitle(titleRes) .setTitle(titleRes)
@@ -541,45 +588,55 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.issue_tracker -> {
baseContext.openUrlInBrowser(AppSettingsService.trackerUrl)
return true
}
R.id.action_filter -> { R.id.action_filter -> {
val filterSheetFragment = FilterSheetFragment() val filterSheetFragment = FilterSheetFragment()
filterSheetFragment.show(supportFragmentManager, FilterSheetFragment.TAG) filterSheetFragment.show(supportFragmentManager, FilterSheetFragment.TAG)
return true return true
} }
R.id.refresh -> { R.id.refresh -> {
needsConfirmation(R.string.menu_home_refresh, R.string.refresh_dialog_message) { needsConfirmation(R.string.menu_home_refresh, R.string.refresh_dialog_message) {
Toast.makeText(this, R.string.refresh_in_progress, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.refresh_in_progress, Toast.LENGTH_SHORT).show()
CountingIdlingResourceSingleton.increment()
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
val updatedRemote = repository.updateRemote() val updatedRemote = repository.updateRemote()
if (updatedRemote) { if (updatedRemote) {
Toast.makeText( Toast.makeText(
this@HomeActivity, this@HomeActivity,
R.string.refresh_success_response, Toast.LENGTH_LONG R.string.refresh_success_response,
Toast.LENGTH_LONG,
) )
.show() .show()
} else { } else {
Toast.makeText( Toast.makeText(
this@HomeActivity, this@HomeActivity,
R.string.refresh_failer_message, R.string.refresh_failer_message,
Toast.LENGTH_SHORT Toast.LENGTH_SHORT,
).show() ).show()
} }
CountingIdlingResourceSingleton.decrement()
} }
} }
return true return true
} }
R.id.readAll -> { R.id.readAll -> {
if (elementsShown == ItemType.UNREAD) { if (elementsShown == ItemType.UNREAD) {
needsConfirmation(R.string.readAll, R.string.markall_dialog_message) { needsConfirmation(R.string.readAll, R.string.markall_dialog_message) {
binding.swipeRefreshLayout.isRefreshing = true binding.swipeRefreshLayout.isRefreshing = true
CountingIdlingResourceSingleton.increment()
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
val success = repository.markAllAsRead(items) val success = repository.markAllAsRead(items)
if (success) { if (success) {
Toast.makeText( Toast.makeText(
this@HomeActivity, this@HomeActivity,
R.string.all_posts_read, R.string.all_posts_read,
Toast.LENGTH_SHORT Toast.LENGTH_SHORT,
).show() ).show()
tabNewBadge.removeBadge() tabNewBadge.removeBadge()
@@ -588,18 +645,24 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
Toast.makeText( Toast.makeText(
this@HomeActivity, this@HomeActivity,
R.string.all_posts_not_read, R.string.all_posts_not_read,
Toast.LENGTH_SHORT Toast.LENGTH_SHORT,
).show() ).show()
} }
handleListResult() handleListResult()
binding.swipeRefreshLayout.isRefreshing = false binding.swipeRefreshLayout.isRefreshing = false
CountingIdlingResourceSingleton.decrement()
} }
} }
} }
return true return true
} }
R.id.action_disconnect -> { R.id.action_disconnect -> {
needsConfirmation(R.string.confirm_disconnect_title, R.string.confirm_disconnect_description) { needsConfirmation(
R.string.confirm_disconnect_title,
R.string.confirm_disconnect_description
) {
runBlocking { runBlocking {
repository.logout() repository.logout()
} }
@@ -609,14 +672,17 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
} }
return true return true
} }
R.id.action_settings -> { R.id.action_settings -> {
settingsLauncher.launch(Intent(this, SettingsActivity::class.java)) settingsLauncher.launch(Intent(this, SettingsActivity::class.java))
return true return true
} }
R.id.action_sources -> { R.id.action_sources -> {
startActivity(Intent(this, SourcesActivity::class.java)) startActivity(Intent(this, SourcesActivity::class.java))
return true return true
} }
else -> return super.onOptionsItemSelected(item) else -> return super.onOptionsItemSelected(item)
} }
} }
@@ -635,20 +701,29 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
private fun handleRecurringTask() { private fun handleRecurringTask() {
if (appSettingsService.isPeriodicRefreshEnabled()) { if (appSettingsService.isPeriodicRefreshEnabled()) {
val myConstraints = Constraints.Builder() val myConstraints =
Constraints.Builder()
.setRequiresBatteryNotLow(true) .setRequiresBatteryNotLow(true)
.setRequiresCharging(appSettingsService.isRefreshWhenChargingOnlyEnabled()) .setRequiresCharging(appSettingsService.isRefreshWhenChargingOnlyEnabled())
.setRequiresStorageNotLow(true) .setRequiresStorageNotLow(true)
.build() .build()
val backgroundWork = val backgroundWork =
PeriodicWorkRequestBuilder<LoadingWorker>(appSettingsService.getRefreshMinutes(), TimeUnit.MINUTES) PeriodicWorkRequestBuilder<LoadingWorker>(
appSettingsService.getRefreshMinutes(),
TimeUnit.MINUTES
)
.setConstraints(myConstraints) .setConstraints(myConstraints)
.addTag("selfoss-loading") .addTag("selfoss-loading")
.build() .build()
WorkManager.getInstance(baseContext).enqueueUniquePeriodicWork("selfoss-loading", ExistingPeriodicWorkPolicy.KEEP, backgroundWork) WorkManager.getInstance(
baseContext,
).enqueueUniquePeriodicWork(
"selfoss-loading",
ExistingPeriodicWorkPolicy.KEEP,
backgroundWork
)
} }
} }
} }
@@ -32,24 +32,41 @@ class ImageActivity : AppCompatActivity() {
binding.pager.adapter = ScreenSlidePagerAdapter(this) binding.pager.adapter = ScreenSlidePagerAdapter(this)
binding.pager.setCurrentItem(position, false) binding.pager.setCurrentItem(position, false)
val transitionListener = object : MotionLayout.TransitionListener { val transitionListener =
override fun onTransitionStarted(motionLayout: MotionLayout?, startId: Int, endId: Int) { object : MotionLayout.TransitionListener {
// Nothing override fun onTransitionStarted(
} motionLayout: MotionLayout?,
startId: Int,
override fun onTransitionChange(motionLayout: MotionLayout?, startId: Int, endId: Int, progress: Float endId: Int,
) { ) {
// Nothing // Nothing
} }
override fun onTransitionCompleted(motionLayout: MotionLayout?, currentId: Int) { override fun onTransitionChange(
motionLayout: MotionLayout?,
startId: Int,
endId: Int,
progress: Float,
) {
// Nothing
}
override fun onTransitionCompleted(
motionLayout: MotionLayout?,
currentId: Int,
) {
if (motionLayout?.currentState == binding.root.endState) { if (motionLayout?.currentState == binding.root.endState) {
onBackPressedDispatcher.onBackPressed() onBackPressedDispatcher.onBackPressed()
overridePendingTransition(0, 0) overridePendingTransition(0, 0)
} }
} }
override fun onTransitionTrigger(motionLayout: MotionLayout?, triggerId: Int, positive: Boolean, progress: Float) { override fun onTransitionTrigger(
motionLayout: MotionLayout?,
triggerId: Int,
positive: Boolean,
progress: Float,
) {
// Nothing // Nothing
} }
} }
@@ -68,7 +85,6 @@ class ImageActivity : AppCompatActivity() {
} }
private inner class ScreenSlidePagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) { private inner class ScreenSlidePagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
override fun getItemCount(): Int = allImages.size override fun getItemCount(): Int = allImages.size
override fun createFragment(position: Int): Fragment = ImageFragment.newInstance(allImages[position]) override fun createFragment(position: Int): Fragment = ImageFragment.newInstance(allImages[position])
@@ -4,6 +4,7 @@ import android.animation.Animator
import android.animation.AnimatorListenerAdapter import android.animation.AnimatorListenerAdapter
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.text.TextUtils import android.text.TextUtils
import android.view.Menu import android.view.Menu
@@ -16,6 +17,7 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import bou.amine.apps.readerforselfossv2.android.databinding.ActivityLoginBinding import bou.amine.apps.readerforselfossv2.android.databinding.ActivityLoginBinding
import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton
import bou.amine.apps.readerforselfossv2.android.utils.isBaseUrlInvalid import bou.amine.apps.readerforselfossv2.android.utils.isBaseUrlInvalid
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.service.AppSettingsService import bou.amine.apps.readerforselfossv2.service.AppSettingsService
@@ -28,9 +30,7 @@ import org.kodein.di.DIAware
import org.kodein.di.android.closestDI import org.kodein.di.android.closestDI
import org.kodein.di.instance import org.kodein.di.instance
class LoginActivity : AppCompatActivity(), DIAware { class LoginActivity : AppCompatActivity(), DIAware {
private var inValidCount: Int = 0 private var inValidCount: Int = 0
private var isWithLogin = false private var isWithLogin = false
@@ -40,7 +40,6 @@ class LoginActivity : AppCompatActivity(), DIAware {
private val repository: Repository by instance() private val repository: Repository by instance()
private val appSettingsService: AppSettingsService by instance() private val appSettingsService: AppSettingsService by instance()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -69,7 +68,6 @@ class LoginActivity : AppCompatActivity(), DIAware {
} }
private fun handleActions() { private fun handleActions() {
binding.passwordView.setOnEditorActionListener( binding.passwordView.setOnEditorActionListener(
TextView.OnEditorActionListener { _, id, _ -> TextView.OnEditorActionListener { _, id, _ ->
if (id == R.id.loginView || id == EditorInfo.IME_NULL) { if (id == R.id.loginView || id == EditorInfo.IME_NULL) {
@@ -77,7 +75,7 @@ class LoginActivity : AppCompatActivity(), DIAware {
return@OnEditorActionListener true return@OnEditorActionListener true
} }
false false
} },
) )
binding.signInButton.setOnClickListener { attemptLogin() } binding.signInButton.setOnClickListener { attemptLogin() }
@@ -98,16 +96,21 @@ class LoginActivity : AppCompatActivity(), DIAware {
alertDialog.setMessage(getString(R.string.base_url_error)) alertDialog.setMessage(getString(R.string.base_url_error))
alertDialog.setButton( alertDialog.setButton(
AlertDialog.BUTTON_NEUTRAL, AlertDialog.BUTTON_NEUTRAL,
"OK" "OK",
) { dialog, _ -> dialog.dismiss() } ) { dialog, _ -> dialog.dismiss() }
alertDialog.show() alertDialog.show()
} }
} }
private fun goToMain() { private fun goToMain() {
CountingIdlingResourceSingleton.increment()
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
repository.updateApiInformation() repository.updateApiInformation()
ACRA.errorReporter.putCustomData("SELFOSS_API_VERSION", appSettingsService.getApiVersion().toString()) ACRA.errorReporter.putCustomData(
"SELFOSS_API_VERSION",
appSettingsService.getApiVersion().toString()
)
CountingIdlingResourceSingleton.decrement()
} }
val intent = Intent(this, HomeActivity::class.java) val intent = Intent(this, HomeActivity::class.java)
startActivity(intent) startActivity(intent)
@@ -123,7 +126,6 @@ class LoginActivity : AppCompatActivity(), DIAware {
} }
private fun attemptLogin() { private fun attemptLogin() {
// Reset errors. // Reset errors.
binding.urlView.error = null binding.urlView.error = null
binding.loginView.error = null binding.loginView.error = null
@@ -143,33 +145,40 @@ class LoginActivity : AppCompatActivity(), DIAware {
repository.refreshLoginInformation(url, login, password) repository.refreshLoginInformation(url, login, password)
CountingIdlingResourceSingleton.increment()
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
try {
repository.updateApiInformation() repository.updateApiInformation()
val result = repository.login() } catch (e: Exception) {
if (result) { if (e.message?.startsWith("No transformation found") == true) {
val (errorFetching, displaySelfossOnly) = repository.shouldBeSelfossInstance()
if (!errorFetching && !displaySelfossOnly) {
goToMain()
} else {
if (displaySelfossOnly) {
Toast.makeText( Toast.makeText(
applicationContext, applicationContext,
R.string.application_selfoss_only, R.string.application_selfoss_only,
Toast.LENGTH_LONG Toast.LENGTH_LONG,
).show() ).show()
preferenceError()
showProgress(false)
} }
}
val result = repository.login()
if (result) {
val errorFetching = repository.checkIfFetchFails()
if (!errorFetching) {
goToMain()
} else {
preferenceError() preferenceError()
} }
} else { } else {
preferenceError() preferenceError()
} }
showProgress(false) showProgress(false)
CountingIdlingResourceSingleton.decrement()
} }
} }
private fun failLoginDetails( private fun failLoginDetails(
password: String, password: String,
login: String login: String,
) { ) {
var lastFocusedView: View? = null var lastFocusedView: View? = null
var cancel = false var cancel = false
@@ -202,7 +211,7 @@ class LoginActivity : AppCompatActivity(), DIAware {
alertDialog.setMessage(getString(R.string.text_wrong_url)) alertDialog.setMessage(getString(R.string.text_wrong_url))
alertDialog.setButton( alertDialog.setButton(
AlertDialog.BUTTON_NEUTRAL, AlertDialog.BUTTON_NEUTRAL,
"OK" "OK",
) { dialog, _ -> dialog.dismiss() } ) { dialog, _ -> dialog.dismiss() }
alertDialog.show() alertDialog.show()
inValidCount = 0 inValidCount = 0
@@ -211,7 +220,10 @@ class LoginActivity : AppCompatActivity(), DIAware {
maybeCancelAndFocusView(cancel, focusView) maybeCancelAndFocusView(cancel, focusView)
} }
private fun maybeCancelAndFocusView(cancel: Boolean, focusView: View?) { private fun maybeCancelAndFocusView(
cancel: Boolean,
focusView: View?,
) {
if (cancel) { if (cancel) {
focusView?.requestFocus() focusView?.requestFocus()
} }
@@ -225,12 +237,13 @@ class LoginActivity : AppCompatActivity(), DIAware {
.animate() .animate()
.setDuration(shortAnimTime.toLong()) .setDuration(shortAnimTime.toLong())
.alpha( .alpha(
if (show) 0F else 1F if (show) 0F else 1F,
).setListener(object : AnimatorListenerAdapter() { ).setListener(
object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) { override fun onAnimationEnd(animation: Animator) {
binding.loginForm.visibility = if (show) View.GONE else View.VISIBLE binding.loginForm.visibility = if (show) View.GONE else View.VISIBLE
} }
} },
) )
binding.loginProgress.visibility = if (show) View.VISIBLE else View.GONE binding.loginProgress.visibility = if (show) View.VISIBLE else View.GONE
@@ -238,12 +251,13 @@ class LoginActivity : AppCompatActivity(), DIAware {
.animate() .animate()
.setDuration(shortAnimTime.toLong()) .setDuration(shortAnimTime.toLong())
.alpha( .alpha(
if (show) 1F else 0F if (show) 1F else 0F,
).setListener(object : AnimatorListenerAdapter() { ).setListener(
object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) { override fun onAnimationEnd(animation: Animator) {
binding.loginProgress.visibility = if (show) View.VISIBLE else View.GONE binding.loginProgress.visibility = if (show) View.VISIBLE else View.GONE
} }
} },
) )
} }
@@ -254,13 +268,25 @@ class LoginActivity : AppCompatActivity(), DIAware {
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) { return when (item.itemId) {
R.id.issue_tracker -> {
val browserIntent =
Intent(Intent.ACTION_VIEW, Uri.parse(AppSettingsService.trackerUrl))
startActivity(browserIntent)
return true
}
R.id.about -> { R.id.about -> {
LibsBuilder() LibsBuilder()
.withAboutIconShown(true) .withAboutIconShown(true)
.withAboutVersionShown(true) .withAboutVersionShown(true)
.withAboutSpecial2("Bug reports")
.withAboutSpecial2Description(AppSettingsService.trackerUrl)
.withAboutSpecial1("Project Page")
.withAboutSpecial1Description(AppSettingsService.sourceUrl)
.start(this) .start(this)
true true
} }
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
} }
@@ -8,7 +8,6 @@ import bou.amine.apps.readerforselfossv2.android.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater) binding = ActivityMainBinding.inflate(layoutInflater)
@@ -10,6 +10,7 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
import androidx.multidex.MultiDexApplication import androidx.multidex.MultiDexApplication
import bou.amine.apps.readerforselfossv2.DI.networkModule import bou.amine.apps.readerforselfossv2.DI.networkModule
import bou.amine.apps.readerforselfossv2.android.testing.TestingHelper
import bou.amine.apps.readerforselfossv2.android.viewmodel.AppViewModel import bou.amine.apps.readerforselfossv2.android.viewmodel.AppViewModel
import bou.amine.apps.readerforselfossv2.dao.DriverFactory import bou.amine.apps.readerforselfossv2.dao.DriverFactory
import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB
@@ -29,21 +30,25 @@ import org.acra.config.toast
import org.acra.data.StringFormat import org.acra.data.StringFormat
import org.acra.ktx.initAcra import org.acra.ktx.initAcra
import org.acra.sender.HttpSender import org.acra.sender.HttpSender
import org.kodein.di.* import org.kodein.di.DI
import org.kodein.di.DIAware
import org.kodein.di.bind
import org.kodein.di.instance
import org.kodein.di.singleton
class MyApp : MultiDexApplication(), DIAware { class MyApp : MultiDexApplication(), DIAware {
override val di by DI.lazy { override val di by DI.lazy {
bind<AppSettingsService>() with singleton { AppSettingsService(ACRA.isACRASenderServiceProcess()) } bind<AppSettingsService>() with singleton { AppSettingsService(ACRA.isACRASenderServiceProcess() || TestingHelper().isUnitTest()) }
import(networkModule) import(networkModule)
bind<DriverFactory>() with singleton { DriverFactory(applicationContext) } bind<DriverFactory>() with singleton { DriverFactory(applicationContext) }
bind<ReaderForSelfossDB>() with singleton { ReaderForSelfossDB(driverFactory.createDriver()) } bind<ReaderForSelfossDB>() with singleton { ReaderForSelfossDB(driverFactory.createDriver()) }
bind<Repository>() with singleton { bind<Repository>() with
singleton {
Repository( Repository(
instance(), instance(),
instance(), instance(),
isConnectionAvailable, isConnectionAvailable,
instance() instance(),
) )
} }
bind<ConnectivityStatus>() with singleton { ConnectivityStatus(applicationContext) } bind<ConnectivityStatus>() with singleton { ConnectivityStatus(applicationContext) }
@@ -70,13 +75,14 @@ class MyApp : MultiDexApplication(), DIAware {
ProcessLifecycleOwner.get().lifecycle.addObserver( ProcessLifecycleOwner.get().lifecycle.addObserver(
AppLifeCycleObserver( AppLifeCycleObserver(
connectivityStatus, connectivityStatus,
repository repository,
) ),
) )
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
viewModel.networkAvailableProvider.collect { networkAvailable -> viewModel.networkAvailableProvider.collect { networkAvailable ->
val toastMessage = if (networkAvailable) { val toastMessage =
if (networkAvailable) {
repository.handleDBActions() repository.handleDBActions()
R.string.network_connectivity_retrieved R.string.network_connectivity_retrieved
} else { } else {
@@ -86,7 +92,7 @@ class MyApp : MultiDexApplication(), DIAware {
Toast.makeText( Toast.makeText(
applicationContext, applicationContext,
toastMessage, toastMessage,
Toast.LENGTH_SHORT Toast.LENGTH_SHORT,
).show() ).show()
} }
} }
@@ -100,7 +106,8 @@ class MyApp : MultiDexApplication(), DIAware {
initAcra { initAcra {
reportFormat = StringFormat.JSON reportFormat = StringFormat.JSON
reportContent = listOf( reportContent =
listOf(
ReportField.REPORT_ID, ReportField.REPORT_ID,
ReportField.INSTALLATION_ID, ReportField.INSTALLATION_ID,
ReportField.APP_VERSION_CODE, ReportField.APP_VERSION_CODE,
@@ -121,7 +128,7 @@ class MyApp : MultiDexApplication(), DIAware {
ReportField.USER_COMMENT, ReportField.USER_COMMENT,
ReportField.USER_CRASH_DATE, ReportField.USER_CRASH_DATE,
ReportField.USER_EMAIL, ReportField.USER_EMAIL,
ReportField.CUSTOM_DATA ReportField.CUSTOM_DATA,
) )
toast { toast {
// required // required
@@ -130,7 +137,7 @@ class MyApp : MultiDexApplication(), DIAware {
} }
httpSender { httpSender {
uri = uri =
"https://bugs.amine-louveau.fr/report" /*best guess, you may need to adjust this*/ "https://bugs.amine-bouabdallaoui.fr/report" // best guess, you may need to adjust this
basicAuthLogin = "qMEscjj89Gwt6cPR" basicAuthLogin = "qMEscjj89Gwt6cPR"
basicAuthPassword = "Yo58QFlGzFaWlBzP" basicAuthPassword = "Yo58QFlGzFaWlBzP"
httpMethod = HttpSender.Method.POST httpMethod = HttpSender.Method.POST
@@ -148,10 +155,11 @@ class MyApp : MultiDexApplication(), DIAware {
val newItemsChannelname = getString(R.string.new_items_channel_sync) val newItemsChannelname = getString(R.string.new_items_channel_sync)
val newItemsChannelimportance = NotificationManager.IMPORTANCE_DEFAULT val newItemsChannelimportance = NotificationManager.IMPORTANCE_DEFAULT
val newItemsChannelmChannel = NotificationChannel( val newItemsChannelmChannel =
NotificationChannel(
AppSettingsService.newItemsChannelId, AppSettingsService.newItemsChannelId,
newItemsChannelname, newItemsChannelname,
newItemsChannelimportance newItemsChannelimportance,
) )
notificationManager.createNotificationChannel(mChannel) notificationManager.createNotificationChannel(mChannel)
@@ -163,9 +171,11 @@ class MyApp : MultiDexApplication(), DIAware {
val oldHandler = Thread.getDefaultUncaughtExceptionHandler() val oldHandler = Thread.getDefaultUncaughtExceptionHandler()
Thread.setDefaultUncaughtExceptionHandler { thread, e -> Thread.setDefaultUncaughtExceptionHandler { thread, e ->
if (e is NoClassDefFoundError && e.stackTrace.asList().any { if (e is NoClassDefFoundError &&
e.stackTrace.asList().any {
it.toString().contains("android.view.ViewDebug") it.toString().contains("android.view.ViewDebug")
}) { }
) {
// Nothing // Nothing
} else { } else {
oldHandler.uncaughtException(thread, e) oldHandler.uncaughtException(thread, e)
@@ -175,9 +185,8 @@ class MyApp : MultiDexApplication(), DIAware {
class AppLifeCycleObserver( class AppLifeCycleObserver(
val connectivityStatus: ConnectivityStatus, val connectivityStatus: ConnectivityStatus,
val repository: Repository val repository: Repository,
) : DefaultLifecycleObserver { ) : DefaultLifecycleObserver {
override fun onResume(owner: LifecycleOwner) { override fun onResume(owner: LifecycleOwner) {
super.onResume(owner) super.onResume(owner)
repository.connectionMonitored = true repository.connectionMonitored = true
@@ -23,7 +23,6 @@ import org.kodein.di.android.closestDI
import org.kodein.di.instance import org.kodein.di.instance
class ReaderActivity : AppCompatActivity(), DIAware { class ReaderActivity : AppCompatActivity(), DIAware {
private var currentItem: Int = 0 private var currentItem: Int = 0
private lateinit var toolbarMenu: Menu private lateinit var toolbarMenu: Menu
@@ -102,15 +101,15 @@ class ReaderActivity : AppCompatActivity(), DIAware {
private inner class ScreenSlidePagerAdapter(fa: FragmentActivity) : private inner class ScreenSlidePagerAdapter(fa: FragmentActivity) :
FragmentStateAdapter(fa) { FragmentStateAdapter(fa) {
override fun getItemCount(): Int = allItems.size override fun getItemCount(): Int = allItems.size
override fun createFragment(position: Int): Fragment = override fun createFragment(position: Int): Fragment = ArticleFragment.newInstance(allItems[position])
ArticleFragment.newInstance(allItems[position])
} }
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { override fun onKeyDown(
keyCode: Int,
event: KeyEvent?,
): Boolean {
return when (keyCode) { return when (keyCode) {
KeyEvent.KEYCODE_VOLUME_DOWN -> { KeyEvent.KEYCODE_VOLUME_DOWN -> {
val currentFragment = val currentFragment =
@@ -152,10 +151,8 @@ class ReaderActivity : AppCompatActivity(), DIAware {
canFavorite() canFavorite()
} }
binding.pager.registerOnPageChangeCallback( binding.pager.registerOnPageChangeCallback(
object : ViewPager2.OnPageChangeCallback() { object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) { override fun onPageSelected(position: Int) {
super.onPageSelected(position) super.onPageSelected(position)
@@ -166,7 +163,7 @@ class ReaderActivity : AppCompatActivity(), DIAware {
} }
readItem(allItems[position]) readItem(allItems[position])
} }
} },
) )
} }
@@ -8,6 +8,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import bou.amine.apps.readerforselfossv2.android.adapters.SourcesListAdapter import bou.amine.apps.readerforselfossv2.android.adapters.SourcesListAdapter
import bou.amine.apps.readerforselfossv2.android.databinding.ActivitySourcesBinding import bou.amine.apps.readerforselfossv2.android.databinding.ActivitySourcesBinding
import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton
import bou.amine.apps.readerforselfossv2.model.SelfossModel import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -18,7 +19,6 @@ import org.kodein.di.android.closestDI
import org.kodein.di.instance import org.kodein.di.instance
class SourcesActivity : AppCompatActivity(), DIAware { class SourcesActivity : AppCompatActivity(), DIAware {
private lateinit var binding: ActivitySourcesBinding private lateinit var binding: ActivitySourcesBinding
override val di by closestDI() override val di by closestDI()
@@ -37,7 +37,8 @@ class SourcesActivity : AppCompatActivity(), DIAware {
supportActionBar?.setDisplayShowHomeEnabled(true) supportActionBar?.setDisplayShowHomeEnabled(true)
binding.fab.rippleColor = resources.getColor(R.color.colorAccentDark) binding.fab.rippleColor = resources.getColor(R.color.colorAccentDark)
binding.fab.backgroundTintList = ColorStateList.valueOf(resources.getColor(R.color.colorAccent)) binding.fab.backgroundTintList =
ColorStateList.valueOf(resources.getColor(R.color.colorAccent))
} }
override fun onStop() { override fun onStop() {
@@ -54,12 +55,15 @@ class SourcesActivity : AppCompatActivity(), DIAware {
binding.recyclerView.setHasFixedSize(true) binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.layoutManager = mLayoutManager binding.recyclerView.layoutManager = mLayoutManager
CountingIdlingResourceSingleton.increment()
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
val response = repository.getSourcesDetails() val response = repository.getSourcesDetails()
if (response.isNotEmpty()) { if (response.isNotEmpty()) {
items = response items = response
val mAdapter = SourcesListAdapter( val mAdapter =
this@SourcesActivity, items SourcesListAdapter(
this@SourcesActivity,
items,
) )
binding.recyclerView.adapter = mAdapter binding.recyclerView.adapter = mAdapter
mAdapter.notifyDataSetChanged() mAdapter.notifyDataSetChanged()
@@ -67,9 +71,10 @@ class SourcesActivity : AppCompatActivity(), DIAware {
Toast.makeText( Toast.makeText(
this@SourcesActivity, this@SourcesActivity,
R.string.cant_get_sources, R.string.cant_get_sources,
Toast.LENGTH_SHORT Toast.LENGTH_SHORT,
).show() ).show()
} }
CountingIdlingResourceSingleton.decrement()
} }
binding.fab.setOnClickListener { binding.fab.setOnClickListener {
@@ -21,9 +21,7 @@ import org.kodein.di.DIAware
import org.kodein.di.android.closestDI import org.kodein.di.android.closestDI
import org.kodein.di.instance import org.kodein.di.instance
class UpsertSourceActivity : AppCompatActivity(), DIAware { class UpsertSourceActivity : AppCompatActivity(), DIAware {
private var existingSource: SelfossModel.SourceDetail? = null private var existingSource: SelfossModel.SourceDetail? = null
private var mSpoutsValue: String? = null private var mSpoutsValue: String? = null
@@ -58,7 +56,6 @@ class UpsertSourceActivity : AppCompatActivity(), DIAware {
supportActionBar?.setDisplayShowHomeEnabled(true) supportActionBar?.setDisplayShowHomeEnabled(true)
supportActionBar?.title = resources.getString(title) supportActionBar?.title = resources.getString(title)
maybeGetDetailsFromIntentSharing(intent) maybeGetDetailsFromIntentSharing(intent)
binding.saveBtn.setOnClickListener { binding.saveBtn.setOnClickListener {
@@ -88,8 +85,14 @@ class UpsertSourceActivity : AppCompatActivity(), DIAware {
private fun handleSpoutsSpinner() { private fun handleSpoutsSpinner() {
val spoutsKV = HashMap<String, String>() val spoutsKV = HashMap<String, String>()
binding.spoutsSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { binding.spoutsSpinner.onItemSelectedListener =
override fun onItemSelected(adapterView: AdapterView<*>, view: View?, i: Int, l: Long) { object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
adapterView: AdapterView<*>,
view: View?,
i: Int,
l: Long,
) {
if (view != null) { if (view != null) {
val spoutName = (view as TextView).text.toString() val spoutName = (view as TextView).text.toString()
mSpoutsValue = spoutsKV[spoutName] mSpoutsValue = spoutsKV[spoutName]
@@ -101,12 +104,11 @@ class UpsertSourceActivity : AppCompatActivity(), DIAware {
} }
} }
fun handleSpoutFailure(networkIssue: Boolean = false) { fun handleSpoutFailure(networkIssue: Boolean = false) {
Toast.makeText( Toast.makeText(
this@UpsertSourceActivity, this@UpsertSourceActivity,
if (networkIssue) R.string.cant_get_spouts_no_network else R.string.cant_get_spouts, if (networkIssue) R.string.cant_get_spouts_no_network else R.string.cant_get_spouts,
Toast.LENGTH_SHORT Toast.LENGTH_SHORT,
).show() ).show()
binding.progress.visibility = View.GONE binding.progress.visibility = View.GONE
} }
@@ -127,7 +129,7 @@ class UpsertSourceActivity : AppCompatActivity(), DIAware {
ArrayAdapter( ArrayAdapter(
this@UpsertSourceActivity, this@UpsertSourceActivity,
android.R.layout.simple_spinner_item, android.R.layout.simple_spinner_item,
itemsStrings itemsStrings,
) )
spinnerArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) spinnerArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
binding.spoutsSpinner.adapter = spinnerArrayAdapter binding.spoutsSpinner.adapter = spinnerArrayAdapter
@@ -144,9 +146,7 @@ class UpsertSourceActivity : AppCompatActivity(), DIAware {
} }
} }
private fun maybeGetDetailsFromIntentSharing( private fun maybeGetDetailsFromIntentSharing(intent: Intent) {
intent: Intent
) {
if (Intent.ACTION_SEND == intent.action && "text/plain" == intent.type) { if (Intent.ACTION_SEND == intent.action && "text/plain" == intent.type) {
binding.sourceUri.setText(intent.getStringExtra(Intent.EXTRA_TEXT)) binding.sourceUri.setText(intent.getStringExtra(Intent.EXTRA_TEXT))
binding.nameInput.setText(intent.getStringExtra(Intent.EXTRA_TITLE)) binding.nameInput.setText(intent.getStringExtra(Intent.EXTRA_TITLE))
@@ -172,13 +172,14 @@ class UpsertSourceActivity : AppCompatActivity(), DIAware {
} }
else -> { else -> {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
val successfullyAddedSource = if (existingSource != null) { val successfullyAddedSource =
if (existingSource != null) {
repository.updateSource( repository.updateSource(
existingSource!!.id, existingSource!!.id,
binding.nameInput.text.toString(), binding.nameInput.text.toString(),
url, url,
mSpoutsValue!!, mSpoutsValue!!,
binding.tags.text.toString() binding.tags.text.toString(),
) )
} else { } else {
repository.createSource( repository.createSource(
@@ -194,7 +195,7 @@ class UpsertSourceActivity : AppCompatActivity(), DIAware {
Toast.makeText( Toast.makeText(
this@UpsertSourceActivity, this@UpsertSourceActivity,
R.string.cant_create_source, R.string.cant_create_source,
Toast.LENGTH_SHORT Toast.LENGTH_SHORT,
).show() ).show()
} }
} }
@@ -1,7 +1,6 @@
package bou.amine.apps.readerforselfossv2.android.adapters package bou.amine.apps.readerforselfossv2.android.adapters
import android.app.Activity import android.app.Activity
import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@@ -10,10 +9,10 @@ import androidx.recyclerview.widget.RecyclerView
import bou.amine.apps.readerforselfossv2.android.R import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.databinding.CardItemBinding import bou.amine.apps.readerforselfossv2.android.databinding.CardItemBinding
import bou.amine.apps.readerforselfossv2.android.utils.LinkOnTouchListener import bou.amine.apps.readerforselfossv2.android.utils.LinkOnTouchListener
import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName
import bou.amine.apps.readerforselfossv2.android.utils.glide.bitmapCenterCrop import bou.amine.apps.readerforselfossv2.android.utils.glide.bitmapCenterCrop
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularDrawable import bou.amine.apps.readerforselfossv2.android.utils.glide.circularDrawable
import bou.amine.apps.readerforselfossv2.android.utils.openInBrowserAsNewTask import bou.amine.apps.readerforselfossv2.android.utils.openItemUrlInBrowserAsNewTask
import bou.amine.apps.readerforselfossv2.android.utils.openItemUrl
import bou.amine.apps.readerforselfossv2.android.utils.shareLink import bou.amine.apps.readerforselfossv2.android.utils.shareLink
import bou.amine.apps.readerforselfossv2.model.SelfossModel import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
@@ -31,10 +30,10 @@ import org.kodein.di.instance
class ItemCardAdapter( class ItemCardAdapter(
override val app: Activity, override val app: Activity,
override var items: ArrayList<SelfossModel.Item>, override val items: ArrayList<SelfossModel.Item>,
override val updateItems: (ArrayList<SelfossModel.Item>) -> Unit override val updateHomeItems: (ArrayList<SelfossModel.Item>) -> Unit,
) : ItemsAdapter<ItemCardAdapter.ViewHolder>() { ) : ItemsAdapter<ItemCardAdapter.ViewHolder>() {
private val c: Context = app.baseContext override lateinit var binding: CardItemBinding
private val imageMaxHeight: Int = private val imageMaxHeight: Int =
c.resources.getDimension(R.dimen.card_image_max_height).toInt() c.resources.getDimension(R.dimen.card_image_max_height).toInt()
@@ -42,15 +41,50 @@ class ItemCardAdapter(
override val repository: Repository by instance() override val repository: Repository by instance()
override val appSettingsService: AppSettingsService by instance() override val appSettingsService: AppSettingsService by instance()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(
val binding = CardItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) parent: ViewGroup,
viewType: Int,
): ViewHolder {
binding = CardItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding) return ViewHolder(binding)
} }
override fun onBindViewHolder(holder: ViewHolder, position: Int) { private fun handleClickListeners(holderBinding: CardItemBinding, position: Int) {
holderBinding.favButton.setOnClickListener {
val item = items[position]
if (item.starred) {
CoroutineScope(Dispatchers.IO).launch {
repository.unstarr(item)
}
binding.favButton.isSelected = false
} else {
CoroutineScope(Dispatchers.IO).launch {
repository.starr(item)
}
binding.favButton.isSelected = true
}
}
binding.shareBtn.setOnClickListener {
val item = items[position]
c.shareLink(item.getLinkDecoded(), item.title.getHtmlDecoded())
}
binding.browserBtn.setOnClickListener {
c.openItemUrlInBrowserAsNewTask(items[position])
}
}
override fun onBindViewHolder(
holder: ViewHolder,
position: Int,
) {
with(holder) { with(holder) {
val itm = items[position] val itm = items[position]
handleClickListeners(binding, position)
handleLinkOpening(binding, position)
binding.favButton.isSelected = itm.starred binding.favButton.isSelected = itm.starred
if (appSettingsService.getPublicAccess()) { if (appSettingsService.getPublicAccess()) {
binding.favButton.visibility = View.GONE binding.favButton.visibility = View.GONE
@@ -62,7 +96,12 @@ class ItemCardAdapter(
binding.title.setLinkTextColor(c.resources.getColor(R.color.colorAccent)) binding.title.setLinkTextColor(c.resources.getColor(R.color.colorAccent))
binding.sourceTitleAndDate.text = itm.sourceAuthorAndDate() binding.sourceTitleAndDate.text = try {
itm.sourceAuthorAndDate()
} catch (e: Exception) {
e.sendSilentlyWithAcraWithName("ItemCardAdapter parse date")
itm.sourceAuthorOnly()
}
if (!appSettingsService.isFullHeightCardsEnabled()) { if (!appSettingsService.isFullHeightCardsEnabled()) {
binding.itemImage.maxHeight = imageMaxHeight binding.itemImage.maxHeight = imageMaxHeight
@@ -86,53 +125,5 @@ class ItemCardAdapter(
} }
} }
override fun getItemCount(): Int { inner class ViewHolder(val binding: CardItemBinding) : RecyclerView.ViewHolder(binding.root)
return items.size
}
inner class ViewHolder(val binding: CardItemBinding) : RecyclerView.ViewHolder(binding.root) {
init {
handleClickListeners()
handleLinkOpening()
}
private fun handleClickListeners() {
binding.favButton.setOnClickListener {
val item = items[bindingAdapterPosition]
if (item.starred) {
CoroutineScope(Dispatchers.IO).launch {
repository.unstarr(item)
}
binding.favButton.isSelected = false
} else {
CoroutineScope(Dispatchers.IO).launch {
repository.starr(item)
}
binding.favButton.isSelected = true
}
}
binding.shareBtn.setOnClickListener {
val item = items[bindingAdapterPosition]
c.shareLink(item.getLinkDecoded(), item.title.getHtmlDecoded())
}
binding.browserBtn.setOnClickListener {
c.openInBrowserAsNewTask(items[bindingAdapterPosition])
}
}
private fun handleLinkOpening() {
binding.root.setOnClickListener {
repository.setReaderItems(items)
c.openItemUrl(
bindingAdapterPosition,
items[bindingAdapterPosition].getLinkDecoded(),
appSettingsService.isArticleViewerEnabled(),
app
)
}
}
}
} }
@@ -1,15 +1,14 @@
package bou.amine.apps.readerforselfossv2.android.adapters package bou.amine.apps.readerforselfossv2.android.adapters
import android.app.Activity import android.app.Activity
import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import bou.amine.apps.readerforselfossv2.android.R import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.databinding.ListItemBinding import bou.amine.apps.readerforselfossv2.android.databinding.ListItemBinding
import bou.amine.apps.readerforselfossv2.android.utils.LinkOnTouchListener import bou.amine.apps.readerforselfossv2.android.utils.LinkOnTouchListener
import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularDrawable import bou.amine.apps.readerforselfossv2.android.utils.glide.circularDrawable
import bou.amine.apps.readerforselfossv2.android.utils.openItemUrl
import bou.amine.apps.readerforselfossv2.model.SelfossModel import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.service.AppSettingsService import bou.amine.apps.readerforselfossv2.service.AppSettingsService
@@ -22,34 +21,46 @@ import org.kodein.di.instance
class ItemListAdapter( class ItemListAdapter(
override val app: Activity, override val app: Activity,
override var items: ArrayList<SelfossModel.Item>, override val items: ArrayList<SelfossModel.Item>,
override val updateItems: (ArrayList<SelfossModel.Item>) -> Unit override val updateHomeItems: (ArrayList<SelfossModel.Item>) -> Unit,
) : ItemsAdapter<ItemListAdapter.ViewHolder>() { ) : ItemsAdapter<ItemListAdapter.ViewHolder>() {
private val c: Context = app.baseContext override lateinit var binding: ListItemBinding
override val di: DI by closestDI(app) override val di: DI by closestDI(app)
override val repository: Repository by instance() override val repository: Repository by instance()
override val appSettingsService: AppSettingsService by instance() override val appSettingsService: AppSettingsService by instance()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(
val binding = ListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) parent: ViewGroup,
viewType: Int,
): ViewHolder {
binding = ListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding) return ViewHolder(binding)
} }
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(
holder: ViewHolder,
position: Int,
) {
with(holder) { with(holder) {
val itm = items[position] val itm = items[position]
handleLinkOpening(binding, position)
binding.title.text = itm.title.getHtmlDecoded() binding.title.text = itm.title.getHtmlDecoded()
binding.title.setOnTouchListener(LinkOnTouchListener()) binding.title.setOnTouchListener(LinkOnTouchListener())
binding.title.setLinkTextColor(c.resources.getColor(R.color.colorAccent)) binding.title.setLinkTextColor(c.resources.getColor(R.color.colorAccent))
binding.sourceTitleAndDate.text = itm.sourceAuthorAndDate() binding.sourceTitleAndDate.text = try {
itm.sourceAuthorAndDate()
} catch (e: Exception) {
e.sendSilentlyWithAcraWithName("ItemListAdapter parse date")
itm.sourceAuthorOnly()
}
if (itm.getThumbnail(repository.baseUrl).isEmpty()) { if (itm.getThumbnail(repository.baseUrl).isEmpty()) {
if (itm.getIcon(repository.baseUrl).isEmpty()) { if (itm.getIcon(repository.baseUrl).isEmpty()) {
binding.itemImage.setBackgroundAndText(itm.sourcetitle.getHtmlDecoded()) binding.itemImage.setBackgroundAndText(itm.sourcetitle.getHtmlDecoded())
} else { } else {
@@ -61,24 +72,5 @@ class ItemListAdapter(
} }
} }
override fun getItemCount(): Int = items.size inner class ViewHolder(val binding: ListItemBinding) : RecyclerView.ViewHolder(binding.root)
inner class ViewHolder(val binding: ListItemBinding) : RecyclerView.ViewHolder(binding.root) {
init {
handleLinkOpening()
}
private fun handleLinkOpening() {
binding.root.setOnClickListener {
repository.setReaderItems(items)
c.openItemUrl(
bindingAdapterPosition,
items[bindingAdapterPosition].getLinkDecoded(),
appSettingsService.isArticleViewerEnabled(),
app
)
}
}
}
} }
@@ -1,10 +1,13 @@
package bou.amine.apps.readerforselfossv2.android.adapters package bou.amine.apps.readerforselfossv2.android.adapters
import android.app.Activity import android.app.Activity
import android.content.Context
import android.graphics.Color import android.graphics.Color
import android.widget.TextView import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import bou.amine.apps.readerforselfossv2.android.R import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.utils.openItemUrl
import bou.amine.apps.readerforselfossv2.model.SelfossModel import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.service.AppSettingsService import bou.amine.apps.readerforselfossv2.service.AppSettingsService
@@ -16,24 +19,32 @@ import kotlinx.coroutines.launch
import org.kodein.di.DIAware import org.kodein.di.DIAware
abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapter<VH>(), DIAware { abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapter<VH>(), DIAware {
abstract var items: ArrayList<SelfossModel.Item> abstract val items: ArrayList<SelfossModel.Item>
abstract val repository: Repository abstract val repository: Repository
abstract val binding: ViewBinding
abstract val appSettingsService: AppSettingsService abstract val appSettingsService: AppSettingsService
abstract val app: Activity abstract val app: Activity
abstract val updateItems: (ArrayList<SelfossModel.Item>) -> Unit abstract val updateHomeItems: (ArrayList<SelfossModel.Item>) -> Unit
protected val c: Context get() = app.baseContext
fun updateAllItems(items: ArrayList<SelfossModel.Item>) { fun updateAllItems(items: ArrayList<SelfossModel.Item>) {
this.items = items this.items.clear()
this.items.addAll(items)
updateHomeItems(items)
notifyDataSetChanged() notifyDataSetChanged()
updateItems(this.items)
} }
private fun unmarkSnackbar(item: SelfossModel.Item, position: Int) { private fun unmarkSnackbar(
val s = Snackbar item: SelfossModel.Item,
position: Int,
) {
val s =
Snackbar
.make( .make(
app.findViewById(R.id.coordLayout), app.findViewById(R.id.coordLayout),
R.string.marked_as_read, R.string.marked_as_read,
Snackbar.LENGTH_LONG Snackbar.LENGTH_LONG,
) )
.setAction(R.string.undo_string) { .setAction(R.string.undo_string) {
unreadItemAtIndex(item, position, false) unreadItemAtIndex(item, position, false)
@@ -45,12 +56,16 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
s.show() s.show()
} }
private fun markSnackbar(item: SelfossModel.Item, position: Int) { private fun markSnackbar(
val s = Snackbar item: SelfossModel.Item,
position: Int,
) {
val s =
Snackbar
.make( .make(
app.findViewById(R.id.coordLayout), app.findViewById(R.id.coordLayout),
R.string.marked_as_unread, R.string.marked_as_unread,
Snackbar.LENGTH_LONG Snackbar.LENGTH_LONG,
) )
.setAction(R.string.undo_string) { .setAction(R.string.undo_string) {
readItemAtIndex(item, position, false) readItemAtIndex(item, position, false)
@@ -62,6 +77,18 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
s.show() s.show()
} }
protected fun handleLinkOpening(holderBinding: ViewBinding, position: Int) {
holderBinding.root.setOnClickListener {
repository.setReaderItems(items)
c.openItemUrl(
position,
items[position].getLinkDecoded(),
appSettingsService.isArticleViewerEnabled(),
app,
)
}
}
fun handleItemAtIndex(position: Int) { fun handleItemAtIndex(position: Int) {
if (items[position].unread) { if (items[position].unread) {
readItemAtIndex(items[position], position) readItemAtIndex(items[position], position)
@@ -70,14 +97,19 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
} }
} }
private fun readItemAtIndex(item: SelfossModel.Item, position: Int, showSnackbar: Boolean = true) { private fun readItemAtIndex(
item: SelfossModel.Item,
position: Int,
showSnackbar: Boolean = true,
) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
repository.markAsRead(item) repository.markAsRead(item)
} }
if (repository.displayedItems == ItemType.UNREAD) { if (repository.displayedItems == ItemType.UNREAD) {
items.remove(item) items.remove(item)
notifyItemRemoved(position) notifyItemRemoved(position)
updateItems(items) notifyItemRangeChanged(position, itemCount)
updateHomeItems(items)
} else { } else {
notifyItemChanged(position) notifyItemChanged(position)
} }
@@ -86,10 +118,13 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
} }
} }
private fun unreadItemAtIndex(item: SelfossModel.Item, position: Int, showSnackbar: Boolean = true) { private fun unreadItemAtIndex(
item: SelfossModel.Item,
position: Int,
showSnackbar: Boolean = true,
) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
repository.unmarkAsRead(item) repository.unmarkAsRead(item)
} }
notifyItemChanged(position) notifyItemChanged(position)
if (showSnackbar) { if (showSnackbar) {
@@ -97,18 +132,21 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
} }
} }
fun addItemAtIndex(item: SelfossModel.Item, position: Int) { fun addItemAtIndex(
item: SelfossModel.Item,
position: Int,
) {
items.add(position, item) items.add(position, item)
notifyItemInserted(position) notifyItemInserted(position)
updateItems(items) updateHomeItems(items)
} }
fun addItemsAtEnd(newItems: List<SelfossModel.Item>) { fun addItemsAtEnd(newItems: List<SelfossModel.Item>) {
val oldSize = items.size val oldSize = items.size
items.addAll(newItems) items.addAll(newItems)
notifyItemRangeInserted(oldSize, newItems.size) notifyItemRangeInserted(oldSize, newItems.size)
updateItems(items) updateHomeItems(items)
}
} override fun getItemCount(): Int = items.size
} }
@@ -28,7 +28,7 @@ import org.kodein.di.instance
class SourcesListAdapter( class SourcesListAdapter(
private val app: Activity, private val app: Activity,
private val items: ArrayList<SelfossModel.SourceDetail> private val items: ArrayList<SelfossModel.SourceDetail>,
) : RecyclerView.Adapter<SourcesListAdapter.ViewHolder>(), DIAware { ) : RecyclerView.Adapter<SourcesListAdapter.ViewHolder>(), DIAware {
private val c: Context = app.baseContext private val c: Context = app.baseContext
private lateinit var binding: SourceListItemBinding private lateinit var binding: SourceListItemBinding
@@ -36,14 +36,47 @@ class SourcesListAdapter(
override val di: DI by closestDI(app) override val di: DI by closestDI(app)
private val repository: Repository by instance() private val repository: Repository by instance()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int,
): ViewHolder {
binding = SourceListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) binding = SourceListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding.root) return ViewHolder(binding.root)
} }
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(
holder: ViewHolder,
position: Int,
) {
val itm = items[position] val itm = items[position]
val deleteBtn: Button = holder.mView.findViewById(R.id.deleteBtn)
deleteBtn.setOnClickListener {
val (id, title) = items[position]
CoroutineScope(Dispatchers.IO).launch {
val successfullyDeletedSource = repository.deleteSource(id, title)
if (successfullyDeletedSource) {
items.removeAt(position)
notifyItemRemoved(position)
notifyItemRangeChanged(position, itemCount)
} else {
Toast.makeText(
app,
R.string.can_delete_source,
Toast.LENGTH_SHORT,
).show()
}
}
}
holder.mView.setOnClickListener {
val source = items[position]
repository.setSelectedSource(source)
app.startActivity(Intent(app, UpsertSourceActivity::class.java))
}
if (itm.getIcon(repository.baseUrl).isEmpty()) { if (itm.getIcon(repository.baseUrl).isEmpty()) {
binding.itemImage.setBackgroundAndText(itm.title.getHtmlDecoded()) binding.itemImage.setBackgroundAndText(itm.title.getHtmlDecoded())
} else { } else {
@@ -66,41 +99,5 @@ class SourcesListAdapter(
override fun getItemCount(): Int = items.size override fun getItemCount(): Int = items.size
inner class ViewHolder(private val mView: ConstraintLayout) : RecyclerView.ViewHolder(mView) { inner class ViewHolder(val mView: ConstraintLayout) : RecyclerView.ViewHolder(mView)
init {
handleClickListeners()
}
private fun handleClickListeners() {
val deleteBtn: Button = mView.findViewById(R.id.deleteBtn)
deleteBtn.setOnClickListener {
val (id, title) = items[bindingAdapterPosition]
CoroutineScope(Dispatchers.IO).launch {
val successfullyDeletedSource = repository.deleteSource(id, title)
if (successfullyDeletedSource) {
items.removeAt(bindingAdapterPosition)
notifyItemRemoved(bindingAdapterPosition)
notifyItemRangeChanged(bindingAdapterPosition, itemCount)
} else {
Toast.makeText(
app,
R.string.can_delete_source,
Toast.LENGTH_SHORT
).show()
}
}
}
mView.setOnClickListener {
val source = items[bindingAdapterPosition]
repository.setSelectedSource(source)
app.startActivity(Intent(app, UpsertSourceActivity::class.java))
}
}
}
} }
@@ -26,16 +26,15 @@ import org.kodein.di.instance
import java.util.* import java.util.*
import kotlin.concurrent.schedule import kotlin.concurrent.schedule
class LoadingWorker(val context: Context, params: WorkerParameters) : Worker(context, params), class LoadingWorker(val context: Context, params: WorkerParameters) :
Worker(context, params),
DIAware { DIAware {
override val di by lazy { (applicationContext as MyApp).di } override val di by lazy { (applicationContext as MyApp).di }
private val repository: Repository by instance() private val repository: Repository by instance()
private val appSettingsService: AppSettingsService by instance() private val appSettingsService: AppSettingsService by instance()
override fun doWork(): Result { override fun doWork(): Result {
if (appSettingsService.isPeriodicRefreshEnabled() && isNetworkAccessible(context)) { if (appSettingsService.isPeriodicRefreshEnabled() && isNetworkAccessible(context)) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val notificationManager = val notificationManager =
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@@ -67,19 +66,19 @@ class LoadingWorker(val context: Context, params: WorkerParameters) : Worker(con
private fun handleNewItemsNotification( private fun handleNewItemsNotification(
newItems: List<SelfossModel.Item>?, newItems: List<SelfossModel.Item>?,
notificationManager: NotificationManager notificationManager: NotificationManager,
) { ) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val apiItems = newItems.orEmpty() val apiItems = newItems.orEmpty()
val newSize = apiItems.filter { it.unread }.size val newSize = apiItems.filter { it.unread }.size
if (newSize > 0) { if (newSize > 0) {
val intent =
val intent = Intent(context, MainActivity::class.java).apply { Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
} }
val pflags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { val pflags =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE PendingIntent.FLAG_IMMUTABLE
} else { } else {
0 0
@@ -90,14 +89,14 @@ class LoadingWorker(val context: Context, params: WorkerParameters) : Worker(con
val newItemsNotification = val newItemsNotification =
NotificationCompat.Builder( NotificationCompat.Builder(
applicationContext, applicationContext,
AppSettingsService.newItemsChannelId AppSettingsService.newItemsChannelId,
) )
.setContentTitle(context.getString(R.string.new_items_notification_title)) .setContentTitle(context.getString(R.string.new_items_notification_title))
.setContentText( .setContentText(
context.getString( context.getString(
R.string.new_items_notification_text, R.string.new_items_notification_text,
newSize newSize,
) ),
) )
.setPriority(PRIORITY_DEFAULT) .setPriority(PRIORITY_DEFAULT)
.setChannelId(AppSettingsService.newItemsChannelId) .setChannelId(AppSettingsService.newItemsChannelId)
@@ -1,16 +1,20 @@
package bou.amine.apps.readerforselfossv2.android.fragments package bou.amine.apps.readerforselfossv2.android.fragments
import android.content.ActivityNotFoundException
import android.content.Intent import android.content.Intent
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.content.res.TypedArray import android.content.res.TypedArray
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Typeface import android.graphics.Typeface
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.util.TypedValue import android.util.TypedValue
import android.view.* import android.view.GestureDetector
import android.view.InflateException
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.webkit.WebResourceResponse import android.webkit.WebResourceResponse
import android.webkit.WebSettings import android.webkit.WebSettings
import android.webkit.WebView import android.webkit.WebView
@@ -25,10 +29,11 @@ import bou.amine.apps.readerforselfossv2.android.databinding.FragmentArticleBind
import bou.amine.apps.readerforselfossv2.android.model.ParecelableItem import bou.amine.apps.readerforselfossv2.android.model.ParecelableItem
import bou.amine.apps.readerforselfossv2.android.model.toModel import bou.amine.apps.readerforselfossv2.android.model.toModel
import bou.amine.apps.readerforselfossv2.android.model.toParcelable import bou.amine.apps.readerforselfossv2.android.model.toParcelable
import bou.amine.apps.readerforselfossv2.android.sendSilentlyWithAcraWithName import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName
import bou.amine.apps.readerforselfossv2.android.utils.glide.getBitmapInputStream import bou.amine.apps.readerforselfossv2.android.utils.glide.getBitmapInputStream
import bou.amine.apps.readerforselfossv2.android.utils.isUrlValid import bou.amine.apps.readerforselfossv2.android.utils.isUrlValid
import bou.amine.apps.readerforselfossv2.android.utils.openInBrowserAsNewTask import bou.amine.apps.readerforselfossv2.android.utils.openItemUrlInBrowserAsNewTask
import bou.amine.apps.readerforselfossv2.android.utils.openUrlInBrowser
import bou.amine.apps.readerforselfossv2.android.utils.shareLink import bou.amine.apps.readerforselfossv2.android.utils.shareLink
import bou.amine.apps.readerforselfossv2.model.MercuryModel import bou.amine.apps.readerforselfossv2.model.MercuryModel
import bou.amine.apps.readerforselfossv2.model.SelfossModel import bou.amine.apps.readerforselfossv2.model.SelfossModel
@@ -47,17 +52,16 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.acra.ktx.sendSilentlyWithAcra
import org.kodein.di.DI import org.kodein.di.DI
import org.kodein.di.DIAware import org.kodein.di.DIAware
import org.kodein.di.android.x.closestDI import org.kodein.di.android.x.closestDI
import org.kodein.di.instance import org.kodein.di.instance
import java.net.MalformedURLException import java.net.MalformedURLException
import java.net.SocketTimeoutException
import java.net.URL import java.net.URL
import java.util.* import java.util.Locale
import java.util.concurrent.ExecutionException import java.util.concurrent.ExecutionException
private const val IMAGE_JPG = "image/jpg" private const val IMAGE_JPG = "image/jpg"
class ArticleFragment : Fragment(), DIAware { class ArticleFragment : Fragment(), DIAware {
@@ -84,7 +88,6 @@ class ArticleFragment : Fragment(), DIAware {
private val mercuryApi: MercuryApi by instance() private val mercuryApi: MercuryApi by instance()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -96,16 +99,26 @@ class ArticleFragment : Fragment(), DIAware {
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?,
): View { ): View {
try { try {
binding = FragmentArticleBinding.inflate(inflater, container, false) binding = FragmentArticleBinding.inflate(inflater, container, false)
try {
url = item.getLinkDecoded() url = item.getLinkDecoded()
} catch (e: Exception) {
e.sendSilentlyWithAcra()
}
contentText = item.content contentText = item.content
contentTitle = item.title.getHtmlDecoded() contentTitle = item.title.getHtmlDecoded()
contentImage = item.getThumbnail(repository.baseUrl) contentImage = item.getThumbnail(repository.baseUrl)
contentSource = item.sourceAuthorAndDate() contentSource = try {
item.sourceAuthorAndDate()
} catch (e: Exception) {
e.sendSilentlyWithAcraWithName("Article Fragment parse date")
item.sourceAuthorOnly()
}
allImages = item.getImages() allImages = item.getImages()
fontSize = appSettingsService.getFontSize() fontSize = appSettingsService.getFontSize()
@@ -146,23 +159,24 @@ class ArticleFragment : Fragment(), DIAware {
if (floatingToolbar.isShowing) floatingToolbar.hide() else fab.show() if (floatingToolbar.isShowing) floatingToolbar.hide() else fab.show()
} }
} }
} },
) )
} catch (e: InflateException) { } catch (e: InflateException) {
e.sendSilentlyWithAcraWithName("webview not available") e.sendSilentlyWithAcraWithName("webview not available")
if (context != null) { try {
AlertDialog.Builder(requireContext()) AlertDialog.Builder(requireContext())
.setMessage(requireContext().getString(R.string.webview_dialog_issue_message)) .setMessage(requireContext().getString(R.string.webview_dialog_issue_message))
.setTitle(requireContext().getString(R.string.webview_dialog_issue_title)) .setTitle(requireContext().getString(R.string.webview_dialog_issue_title))
.setPositiveButton( .setPositiveButton(
android.R.string.ok android.R.string.ok,
) { _, _ -> ) { _, _ ->
appSettingsService.disableArticleViewer() appSettingsService.disableArticleViewer()
requireActivity().finish() requireActivity().finish()
} }
.create() .create()
.show() .show()
} catch (e: IllegalStateException) {
e.sendSilentlyWithAcraWithName("Context required is null")
} }
} }
@@ -210,17 +224,18 @@ class ArticleFragment : Fragment(), DIAware {
override fun onItemClick(item: MenuItem) { override fun onItemClick(item: MenuItem) {
when (item.itemId) { when (item.itemId) {
R.id.share_action -> requireActivity().shareLink(url, contentTitle) R.id.share_action -> requireActivity().shareLink(url, contentTitle)
R.id.open_action -> requireActivity().openInBrowserAsNewTask(this@ArticleFragment.item) R.id.open_action -> requireActivity().openItemUrlInBrowserAsNewTask(this@ArticleFragment.item)
R.id.unread_action -> if (context != null) { R.id.unread_action ->
try {
if (this@ArticleFragment.item.unread) { if (this@ArticleFragment.item.unread) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
repository.markAsRead(this@ArticleFragment.item) repository.markAsRead(this@ArticleFragment.item)
} }
this@ArticleFragment.item.unread = false this@ArticleFragment.item.unread = false
Toast.makeText( Toast.makeText(
context, requireContext(),
R.string.marked_as_read, R.string.marked_as_read,
Toast.LENGTH_LONG Toast.LENGTH_LONG,
).show() ).show()
} else { } else {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
@@ -230,10 +245,13 @@ class ArticleFragment : Fragment(), DIAware {
Toast.makeText( Toast.makeText(
context, context,
R.string.marked_as_unread, R.string.marked_as_unread,
Toast.LENGTH_LONG Toast.LENGTH_LONG,
).show() ).show()
} }
} catch (e: IllegalStateException) {
e.sendSilentlyWithAcraWithName("Context required is null")
} }
else -> Unit else -> Unit
} }
} }
@@ -241,13 +259,14 @@ class ArticleFragment : Fragment(), DIAware {
override fun onItemLongClick(item: MenuItem?) { override fun onItemLongClick(item: MenuItem?) {
// We do nothing // We do nothing
} }
} },
) )
return floatingToolbar return floatingToolbar
} }
private fun refreshAlignment() { private fun refreshAlignment() {
textAlignment = when (appSettingsService.getActiveAllignment()) { textAlignment =
when (appSettingsService.getActiveAllignment()) {
1 -> "justify" 1 -> "justify"
2 -> "left" 2 -> "left"
else -> "justify" else -> "justify"
@@ -265,10 +284,7 @@ class ArticleFragment : Fragment(), DIAware {
} else { } else {
openInBrowserAfterFailing() openInBrowserAfterFailing()
} }
} catch (e: SocketTimeoutException) {
openInBrowserAfterFailing()
} catch (e: Exception) { } catch (e: Exception) {
e.sendSilentlyWithAcraWithName("getContentFromMercury > $url")
openInBrowserAfterFailing() openInBrowserAfterFailing()
} }
} }
@@ -288,7 +304,7 @@ class ArticleFragment : Fragment(), DIAware {
contentText = data.content.orEmpty() contentText = data.content.orEmpty()
htmlToWebview() htmlToWebview()
handleLeadImage(data?.lead_image_url) handleLeadImage(data.lead_image_url)
binding.nestedScrollView.scrollTo(0, 0) binding.nestedScrollView.scrollTo(0, 0)
binding.progressBar.visibility = View.GONE binding.progressBar.visibility = View.GONE
@@ -302,7 +318,7 @@ class ArticleFragment : Fragment(), DIAware {
.with(requireContext()) .with(requireContext())
.asBitmap() .asBitmap()
.load( .load(
lead_image_url lead_image_url,
) )
.apply(RequestOptions.fitCenterTransform()) .apply(RequestOptions.fitCenterTransform())
.into(binding.imageView) .into(binding.imageView)
@@ -312,15 +328,15 @@ class ArticleFragment : Fragment(), DIAware {
} }
private fun handleImageLoading() { private fun handleImageLoading() {
binding.webcontent.webViewClient = object : WebViewClient() { binding.webcontent.webViewClient =
object : WebViewClient() {
@Deprecated("Deprecated in Java") @Deprecated("Deprecated in Java")
override fun shouldOverrideUrlLoading(view: WebView?, url: String): Boolean { override fun shouldOverrideUrlLoading(
view: WebView?,
url: String,
): Boolean {
return if (context != null && url.isUrlValid() && binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) { return if (context != null && url.isUrlValid() && binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
try { requireContext().openUrlInBrowser(url)
requireContext().startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
} catch (e: ActivityNotFoundException) {
e.sendSilentlyWithAcraWithName("activityNotFound > $url")
}
true true
} else { } else {
false false
@@ -328,18 +344,23 @@ class ArticleFragment : Fragment(), DIAware {
} }
@Deprecated("Deprecated in Java") @Deprecated("Deprecated in Java")
override fun shouldInterceptRequest(view: WebView, url: String): WebResourceResponse? { override fun shouldInterceptRequest(
view: WebView,
url: String,
): WebResourceResponse? {
val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL) val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL)
if (url.lowercase(Locale.US).contains(".jpg") || url.lowercase(Locale.US) if (url.lowercase(Locale.US).contains(".jpg") ||
url.lowercase(Locale.US)
.contains(".jpeg") .contains(".jpeg")
) { ) {
try { try {
val image = val image =
Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get() Glide.with(view).asBitmap().apply(glideOptions).load(url).submit()
.get()
return WebResourceResponse( return WebResourceResponse(
IMAGE_JPG, IMAGE_JPG,
"UTF-8", "UTF-8",
getBitmapInputStream(image, Bitmap.CompressFormat.JPEG) getBitmapInputStream(image, Bitmap.CompressFormat.JPEG),
) )
} catch (e: ExecutionException) { } catch (e: ExecutionException) {
// Do nothing // Do nothing
@@ -347,11 +368,12 @@ class ArticleFragment : Fragment(), DIAware {
} else if (url.lowercase(Locale.US).contains(".png")) { } else if (url.lowercase(Locale.US).contains(".png")) {
try { try {
val image = val image =
Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get() Glide.with(view).asBitmap().apply(glideOptions).load(url).submit()
.get()
return WebResourceResponse( return WebResourceResponse(
IMAGE_JPG, IMAGE_JPG,
"UTF-8", "UTF-8",
getBitmapInputStream(image, Bitmap.CompressFormat.PNG) getBitmapInputStream(image, Bitmap.CompressFormat.PNG),
) )
} catch (e: ExecutionException) { } catch (e: ExecutionException) {
// Do nothing // Do nothing
@@ -359,11 +381,12 @@ class ArticleFragment : Fragment(), DIAware {
} else if (url.lowercase(Locale.US).contains(".webp")) { } else if (url.lowercase(Locale.US).contains(".webp")) {
try { try {
val image = val image =
Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get() Glide.with(view).asBitmap().apply(glideOptions).load(url).submit()
.get()
return WebResourceResponse( return WebResourceResponse(
IMAGE_JPG, IMAGE_JPG,
"UTF-8", "UTF-8",
getBitmapInputStream(image, Bitmap.CompressFormat.WEBP) getBitmapInputStream(image, Bitmap.CompressFormat.WEBP),
) )
} catch (e: ExecutionException) { } catch (e: ExecutionException) {
// Do nothing // Do nothing
@@ -376,11 +399,10 @@ class ArticleFragment : Fragment(), DIAware {
} }
private fun htmlToWebview() { private fun htmlToWebview() {
if (context != null) { try {
val attrs: IntArray = intArrayOf(android.R.attr.fontFamily) val attrs: IntArray = intArrayOf(android.R.attr.fontFamily)
val a: TypedArray = requireContext().obtainStyledAttributes(resId, attrs) val a: TypedArray = requireContext().obtainStyledAttributes(resId, attrs)
binding.webcontent.settings.standardFontFamily = a.getString(0) binding.webcontent.settings.standardFontFamily = a.getString(0)
binding.webcontent.visibility = View.VISIBLE binding.webcontent.visibility = View.VISIBLE
@@ -397,11 +419,14 @@ class ArticleFragment : Fragment(), DIAware {
handleImageLoading() handleImageLoading()
val gestureDetector = val gestureDetector =
GestureDetector(activity, object : GestureDetector.SimpleOnGestureListener() { GestureDetector(
activity,
object : GestureDetector.SimpleOnGestureListener() {
override fun onSingleTapUp(e: MotionEvent): Boolean { override fun onSingleTapUp(e: MotionEvent): Boolean {
return performClick() return performClick()
} }
}) },
)
binding.webcontent.setOnTouchListener { _, event -> gestureDetector.onTouchEvent(event) } binding.webcontent.setOnTouchListener { _, event -> gestureDetector.onTouchEvent(event) }
@@ -417,18 +442,20 @@ class ArticleFragment : Fragment(), DIAware {
e.sendSilentlyWithAcraWithName("htmlToWebview > $url") e.sendSilentlyWithAcraWithName("htmlToWebview > $url")
} }
val fontName = when (font) { val fontName =
when (font) {
getString(R.string.open_sans_font_id) -> "Open Sans" getString(R.string.open_sans_font_id) -> "Open Sans"
getString(R.string.roboto_font_id) -> "Roboto" getString(R.string.roboto_font_id) -> "Roboto"
getString(R.string.source_code_pro_font_id) -> "Source Code Pro" getString(R.string.source_code_pro_font_id) -> "Source Code Pro"
else -> "" else -> ""
} }
val fontLinkAndStyle = if (font.isNotEmpty()) { val fontLinkAndStyle =
if (font.isNotEmpty()) {
"""<link href="https://fonts.googleapis.com/css?family=${ """<link href="https://fonts.googleapis.com/css?family=${
fontName.replace( fontName.replace(
" ", " ",
"+" "+",
) )
}" rel="stylesheet"> }" rel="stylesheet">
|<style> |<style>
@@ -457,7 +484,7 @@ class ArticleFragment : Fragment(), DIAware {
| color: ${ | color: ${
String.format( String.format(
"#%06X", "#%06X",
0xFFFFFF and resources.getColor(R.color.colorAccent) 0xFFFFFF and resources.getColor(R.color.colorAccent),
) )
} !important; } !important;
| } | }
@@ -473,7 +500,7 @@ class ArticleFragment : Fragment(), DIAware {
| background-color: ${ | background-color: ${
String.format( String.format(
"#%06X", "#%06X",
0xFFFFFF and colorSurface.data 0xFFFFFF and colorSurface.data,
) )
}; };
| } | }
@@ -481,13 +508,13 @@ class ArticleFragment : Fragment(), DIAware {
| background-color: ${ | background-color: ${
String.format( String.format(
"#%06X", "#%06X",
0xFFFFFF and colorSurface.data 0xFFFFFF and colorSurface.data,
) )
} !important; } !important;
| border-color: ${ | border-color: ${
String.format( String.format(
"#%06X", "#%06X",
0xFFFFFF and colorSurface.data 0xFFFFFF and colorSurface.data,
) )
} !important; } !important;
| padding: 0 !important; | padding: 0 !important;
@@ -502,7 +529,7 @@ class ArticleFragment : Fragment(), DIAware {
| background-color: ${ | background-color: ${
String.format( String.format(
"#%06X", "#%06X",
0xFFFFFF and colorSurface.data 0xFFFFFF and colorSurface.data,
) )
}; };
| } | }
@@ -511,11 +538,14 @@ class ArticleFragment : Fragment(), DIAware {
|</head> |</head>
|<body> |<body>
| $contentText | $contentText
|</body>""".trimMargin(), |</body>
""".trimMargin(),
"text/html", "text/html",
"utf-8", "utf-8",
null null,
) )
} catch (e: IllegalStateException) {
e.sendSilentlyWithAcraWithName("Context required is null")
} }
} }
@@ -531,20 +561,17 @@ class ArticleFragment : Fragment(), DIAware {
private fun openInBrowserAfterFailing() { private fun openInBrowserAfterFailing() {
binding.progressBar.visibility = View.GONE binding.progressBar.visibility = View.GONE
if (context != null) { try {
requireContext().openInBrowserAsNewTask(this@ArticleFragment.item) requireContext().openItemUrlInBrowserAsNewTask(this@ArticleFragment.item)
} else { } catch (e: IllegalStateException) {
Exception("openInBrowserAfterFailing context is null").sendSilentlyWithAcraWithName("openInBrowserAfterFailing > $context") e.sendSilentlyWithAcraWithName("Context required is null")
} }
} }
companion object { companion object {
private const val ARG_ITEMS = "items" private const val ARG_ITEMS = "items"
fun newInstance( fun newInstance(item: SelfossModel.Item): ArticleFragment {
item: SelfossModel.Item
): ArticleFragment {
val fragment = ArticleFragment() val fragment = ArticleFragment()
val args = Bundle() val args = Bundle()
args.putParcelable(ARG_ITEMS, item.toParcelable()) args.putParcelable(ARG_ITEMS, item.toParcelable())
@@ -554,10 +581,11 @@ class ArticleFragment : Fragment(), DIAware {
} }
fun performClick(): Boolean { fun performClick(): Boolean {
if (allImages != null && (binding.webcontent.hitTestResult.type == WebView.HitTestResult.IMAGE_TYPE || if (allImages != null && (
binding.webcontent.hitTestResult.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) binding.webcontent.hitTestResult.type == WebView.HitTestResult.IMAGE_TYPE ||
binding.webcontent.hitTestResult.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE
)
) { ) {
val position: Int = allImages.indexOf(binding.webcontent.hitTestResult.extra) val position: Int = allImages.indexOf(binding.webcontent.hitTestResult.extra)
val intent = Intent(activity, ImageActivity::class.java) val intent = Intent(activity, ImageActivity::class.java)
@@ -568,6 +596,4 @@ class ArticleFragment : Fragment(), DIAware {
} }
return false return false
} }
} }
@@ -15,8 +15,9 @@ import android.view.ViewGroup
import bou.amine.apps.readerforselfossv2.android.HomeActivity import bou.amine.apps.readerforselfossv2.android.HomeActivity
import bou.amine.apps.readerforselfossv2.android.R import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.databinding.FilterFragmentBinding import bou.amine.apps.readerforselfossv2.android.databinding.FilterFragmentBinding
import bou.amine.apps.readerforselfossv2.android.sendSilentlyWithAcraWithName import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.utils.getColorHexCode
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
import bou.amine.apps.readerforselfossv2.utils.getIcon import bou.amine.apps.readerforselfossv2.utils.getIcon
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
@@ -32,9 +33,7 @@ import org.kodein.di.DIAware
import org.kodein.di.android.x.closestDI import org.kodein.di.android.x.closestDI
import org.kodein.di.instance import org.kodein.di.instance
class FilterSheetFragment : BottomSheetDialogFragment(), DIAware { class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
private lateinit var binding: FilterFragmentBinding private lateinit var binding: FilterFragmentBinding
override val di: DI by closestDI() override val di: DI by closestDI()
private val repository: Repository by instance() private val repository: Repository by instance()
@@ -44,29 +43,26 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?,
): View { ): View {
binding = binding =
FilterFragmentBinding.inflate( FilterFragmentBinding.inflate(
inflater, inflater,
container, container,
false false,
) )
val context: Context? = context try {
if (context == null) {
dismiss()
Exception("FilterSheetFragment context is null").sendSilentlyWithAcraWithName("FilterSheetFragment > onCreateView")
} else {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
handleTagChips(context) handleTagChips(requireContext())
handleSourceChips(context) handleSourceChips(requireContext())
binding.progressBar2.visibility = GONE binding.progressBar2.visibility = GONE
binding.filterView.visibility = VISIBLE binding.filterView.visibility = VISIBLE
} }
} catch (e: IllegalStateException) {
dismiss()
e.sendSilentlyWithAcraWithName("FilterSheetFragment > onCreateView")
} }
binding.floatingActionButton2.setOnClickListener { binding.floatingActionButton2.setOnClickListener {
@@ -77,9 +73,7 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
return binding.root return binding.root
} }
private suspend fun handleSourceChips( private suspend fun handleSourceChips(context: Context) {
context: Context
) {
val sourceGroup = binding.sourcesGroup val sourceGroup = binding.sourcesGroup
repository.getSourcesDetailsOrStats().forEachIndexed { _, source -> repository.getSourcesDetailsOrStats().forEachIndexed { _, source ->
@@ -88,10 +82,11 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
Glide.with(context) Glide.with(context)
.load(source.getIcon(repository.baseUrl)) .load(source.getIcon(repository.baseUrl))
.into(object : ViewTarget<Chip?, Drawable?>(c) { .into(
object : ViewTarget<Chip?, Drawable?>(c) {
override fun onResourceReady( override fun onResourceReady(
resource: Drawable, resource: Drawable,
transition: Transition<in Drawable?>? transition: Transition<in Drawable?>?,
) { ) {
try { try {
c.chipIcon = resource c.chipIcon = resource
@@ -99,8 +94,8 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
e.sendSilentlyWithAcraWithName("sources > onResourceReady") e.sendSilentlyWithAcraWithName("sources > onResourceReady")
} }
} }
},
}) )
c.text = source.title.getHtmlDecoded() c.text = source.title.getHtmlDecoded()
@@ -121,7 +116,6 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
repository.setTagFilter(null) repository.setTagFilter(null)
} }
if (repository.sourceFilter.value?.equals(source) == true) { if (repository.sourceFilter.value?.equals(source) == true) {
c.isCloseIconVisible = true c.isCloseIconVisible = true
selectedChip = c selectedChip = c
@@ -137,9 +131,7 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
} }
} }
private suspend fun handleTagChips( private suspend fun handleTagChips(context: Context) {
context: Context,
) {
val tagGroup = binding.tagsGroup val tagGroup = binding.tagsGroup
val tags = repository.getTags() val tags = repository.getTags()
@@ -152,10 +144,11 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
if (tag.color.isNotEmpty()) { if (tag.color.isNotEmpty()) {
try { try {
val gd = GradientDrawable() val gd = GradientDrawable()
val gdColor = try { val gdColor =
Color.parseColor(tag.color) try {
Color.parseColor(tag.getColorHexCode())
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
e.sendSilentlyWithAcraWithName("color issue " + tag.color) e.sendSilentlyWithAcraWithName("color issue " + tag.color + " / " + tag.getColorHexCode())
resources.getColor(R.color.colorPrimary) resources.getColor(R.color.colorPrimary)
} }
gd.setColor(gdColor) gd.setColor(gdColor)
@@ -197,6 +190,4 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
companion object { companion object {
const val TAG = "FilterModalBottomSheet" const val TAG = "FilterModalBottomSheet"
} }
} }
@@ -11,7 +11,6 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
class ImageFragment : Fragment() { class ImageFragment : Fragment() {
private lateinit var imageUrl: String private lateinit var imageUrl: String
private val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL) private val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL)
private var _binding: FragmentImageBinding? = null private var _binding: FragmentImageBinding? = null
@@ -23,7 +22,11 @@ class ImageFragment : Fragment() {
imageUrl = requireArguments().getString("imageUrl")!! imageUrl = requireArguments().getString("imageUrl")!!
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
_binding = FragmentImageBinding.inflate(inflater, container, false) _binding = FragmentImageBinding.inflate(inflater, container, false)
val view = binding?.root val view = binding?.root
@@ -45,9 +48,7 @@ class ImageFragment : Fragment() {
companion object { companion object {
private const val ARG_IMAGE = "imageUrl" private const val ARG_IMAGE = "imageUrl"
fun newInstance( fun newInstance(imageUrl: String): ImageFragment {
imageUrl : String
): ImageFragment {
val fragment = ImageFragment() val fragment = ImageFragment()
val args = Bundle() val args = Bundle()
args.putString(ARG_IMAGE, imageUrl) args.putString(ARG_IMAGE, imageUrl)
@@ -2,7 +2,7 @@ package bou.amine.apps.readerforselfossv2.android.model
import android.content.Context import android.content.Context
import android.webkit.URLUtil import android.webkit.URLUtil
import bou.amine.apps.readerforselfossv2.android.sendSilentlyWithAcraWithName import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName
import bou.amine.apps.readerforselfossv2.model.SelfossModel import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.utils.getImages import bou.amine.apps.readerforselfossv2.utils.getImages
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
@@ -14,7 +14,6 @@ fun SelfossModel.Item.preloadImages(context: Context) : Boolean {
val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL).timeout(10000) val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL).timeout(10000)
try { try {
for (url in imageUrls) { for (url in imageUrls) {
if (URLUtil.isValidUrl(url)) { if (URLUtil.isValidUrl(url)) {
@@ -17,8 +17,9 @@ fun SelfossModel.Item.toParcelable() : ParecelableItem =
this.link, this.link,
this.sourcetitle, this.sourcetitle,
this.tags.joinToString(","), this.tags.joinToString(","),
this.author this.author,
) )
fun ParecelableItem.toModel(): SelfossModel.Item = fun ParecelableItem.toModel(): SelfossModel.Item =
SelfossModel.Item( SelfossModel.Item(
this.id, this.id,
@@ -32,8 +33,9 @@ fun ParecelableItem.toModel() : SelfossModel.Item =
this.link, this.link,
this.sourcetitle, this.sourcetitle,
this.tags.split(","), this.tags.split(","),
this.author this.author,
) )
data class ParecelableItem( data class ParecelableItem(
val id: Int, val id: Int,
val datetime: String, val datetime: String,
@@ -46,13 +48,14 @@ data class ParecelableItem(
val link: String, val link: String,
val sourcetitle: String, val sourcetitle: String,
val tags: String, val tags: String,
val author: String? val author: String?,
) : Parcelable { ) : Parcelable {
companion object { companion object {
@JvmField @JvmField
val CREATOR: Parcelable.Creator<ParecelableItem> = object : Parcelable.Creator<ParecelableItem> { val CREATOR: Parcelable.Creator<ParecelableItem> =
object : Parcelable.Creator<ParecelableItem> {
override fun createFromParcel(source: Parcel): ParecelableItem = ParecelableItem(source) override fun createFromParcel(source: Parcel): ParecelableItem = ParecelableItem(source)
override fun newArray(size: Int): Array<ParecelableItem?> = arrayOfNulls(size) override fun newArray(size: Int): Array<ParecelableItem?> = arrayOfNulls(size)
} }
} }
@@ -69,12 +72,15 @@ data class ParecelableItem(
link = source.readString().orEmpty(), link = source.readString().orEmpty(),
sourcetitle = source.readString().orEmpty(), sourcetitle = source.readString().orEmpty(),
tags = source.readString().orEmpty(), tags = source.readString().orEmpty(),
author = source.readString().orEmpty() author = source.readString().orEmpty(),
) )
override fun describeContents() = 0 override fun describeContents() = 0
override fun writeToParcel(dest: Parcel, flags: Int) { override fun writeToParcel(
dest: Parcel,
flags: Int,
) {
dest.writeInt(id) dest.writeInt(id)
dest.writeString(datetime) dest.writeString(datetime)
dest.writeString(title) dest.writeString(title)
@@ -1,7 +1,5 @@
package bou.amine.apps.readerforselfossv2.android.settings package bou.amine.apps.readerforselfossv2.android.settings
import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.text.Editable import android.text.Editable
import android.text.InputFilter import android.text.InputFilter
@@ -16,7 +14,8 @@ import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import bou.amine.apps.readerforselfossv2.android.R import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.databinding.ActivitySettingsBinding import bou.amine.apps.readerforselfossv2.android.databinding.ActivitySettingsBinding
import bou.amine.apps.readerforselfossv2.android.sendSilentlyWithAcraWithName import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName
import bou.amine.apps.readerforselfossv2.android.utils.openUrlInBrowser
import bou.amine.apps.readerforselfossv2.service.AppSettingsService import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import com.mikepenz.aboutlibraries.LibsBuilder import com.mikepenz.aboutlibraries.LibsBuilder
import org.kodein.di.DIAware import org.kodein.di.DIAware
@@ -24,8 +23,10 @@ import org.kodein.di.android.closestDI
private const val TITLE_TAG = "settingsActivityTitle" private const val TITLE_TAG = "settingsActivityTitle"
class SettingsActivity : AppCompatActivity(), class SettingsActivity :
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback, DIAware { AppCompatActivity(),
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback,
DIAware {
override val di by closestDI() override val di by closestDI()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -72,13 +73,14 @@ class SettingsActivity : AppCompatActivity(),
override fun onPreferenceStartFragment( override fun onPreferenceStartFragment(
caller: PreferenceFragmentCompat, caller: PreferenceFragmentCompat,
pref: Preference pref: Preference,
): Boolean { ): Boolean {
// Instantiate the new Fragment // Instantiate the new Fragment
val args = pref.extras val args = pref.extras
val fragment = supportFragmentManager.fragmentFactory.instantiate( val fragment =
supportFragmentManager.fragmentFactory.instantiate(
classLoader, classLoader,
pref.fragment.toString() pref.fragment.toString(),
).apply { ).apply {
arguments = args arguments = args
setTargetFragment(caller, 0) setTargetFragment(caller, 0)
@@ -94,15 +96,22 @@ class SettingsActivity : AppCompatActivity(),
} }
class MainPreferenceFragment : PreferenceFragmentCompat() { class MainPreferenceFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(
savedInstanceState: Bundle?,
rootKey: String?,
) {
setPreferencesFromResource(R.xml.pref_main, rootKey) setPreferencesFromResource(R.xml.pref_main, rootKey)
preferenceManager.findPreference<Preference>("currentMode")?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> preferenceManager.findPreference<Preference>("currentMode")?.onPreferenceChangeListener =
AppCompatDelegate.setDefaultNightMode(newValue.toString().toInt()) // ListPreference Only takes string-arrays ¯\_(ツ)_/¯ Preference.OnPreferenceChangeListener { _, newValue ->
AppCompatDelegate.setDefaultNightMode(
newValue.toString().toInt()
) // ListPreference Only takes string-arrays ¯\_(ツ)_/¯
true true
} }
preferenceManager.findPreference<Preference>("action_about")?.onPreferenceClickListener = Preference.OnPreferenceClickListener { _ -> preferenceManager.findPreference<Preference>("action_about")?.onPreferenceClickListener =
Preference.OnPreferenceClickListener { _ ->
context?.let { context?.let {
LibsBuilder() LibsBuilder()
.withAboutIconShown(true) .withAboutIconShown(true)
@@ -115,42 +124,66 @@ class SettingsActivity : AppCompatActivity(),
} }
class GeneralPreferenceFragment : PreferenceFragmentCompat() { class GeneralPreferenceFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(
savedInstanceState: Bundle?,
rootKey: String?,
) {
setPreferencesFromResource(R.xml.pref_general, rootKey) setPreferencesFromResource(R.xml.pref_general, rootKey)
val editTextPreference = preferenceManager.findPreference<EditTextPreference>("prefer_api_items_number") val editTextPreference =
preferenceManager.findPreference<EditTextPreference>("prefer_api_items_number")
editTextPreference?.setOnBindEditTextListener { editText -> editTextPreference?.setOnBindEditTextListener { editText ->
editText.inputType = InputType.TYPE_CLASS_NUMBER editText.inputType = InputType.TYPE_CLASS_NUMBER
editText.filters = arrayOf( editText.filters =
arrayOf(
InputFilter { source, _, _, dest, _, _ -> InputFilter { source, _, _, dest, _, _ ->
try { try {
val input: Int = (dest.toString() + source.toString()).toInt() val input: Int = (dest.toString() + source.toString()).toInt()
if (input in 1..200) return@InputFilter null if (input in 1..200) return@InputFilter null
} catch (nfe: NumberFormatException) { } catch (nfe: NumberFormatException) {
nfe.sendSilentlyWithAcraWithName("GeneralPreferenceFragment") Toast.makeText(
Toast.makeText(activity, R.string.items_number_should_be_number, Toast.LENGTH_LONG).show() activity,
R.string.items_number_should_be_number,
Toast.LENGTH_LONG
).show()
} }
"" ""
} },
) )
} }
} }
} }
class ArticleViewerPreferenceFragment : PreferenceFragmentCompat() { class ArticleViewerPreferenceFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(
savedInstanceState: Bundle?,
rootKey: String?,
) {
setPreferencesFromResource(R.xml.pref_viewer, rootKey) setPreferencesFromResource(R.xml.pref_viewer, rootKey)
val fontSize = preferenceManager.findPreference<EditTextPreference>("reader_font_size") val fontSize = preferenceManager.findPreference<EditTextPreference>("reader_font_size")
fontSize?.setOnBindEditTextListener { editText -> fontSize?.setOnBindEditTextListener { editText ->
editText.inputType = InputType.TYPE_CLASS_NUMBER editText.inputType = InputType.TYPE_CLASS_NUMBER
editText.addTextChangedListener { object : TextWatcher { editText.addTextChangedListener {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) { object : TextWatcher {
override fun beforeTextChanged(
charSequence: CharSequence,
i: Int,
i1: Int,
i2: Int,
) {
// We do nothing // We do nothing
} }
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
override fun onTextChanged(
charSequence: CharSequence,
i: Int,
i1: Int,
i2: Int,
) {
// We do nothing // We do nothing
} }
override fun afterTextChanged(editable: Editable) { override fun afterTextChanged(editable: Editable) {
try { try {
editText.textSize = editable.toString().toInt().toFloat() editText.textSize = editable.toString().toInt().toFloat()
@@ -158,8 +191,10 @@ class SettingsActivity : AppCompatActivity(),
e.sendSilentlyWithAcraWithName("ArticleViewerPreferenceFragment > afterTextChanged") e.sendSilentlyWithAcraWithName("ArticleViewerPreferenceFragment > afterTextChanged")
} }
} }
} } }
editText.filters = arrayOf( }
editText.filters =
arrayOf(
InputFilter { source, _, _, dest, _, _ -> InputFilter { source, _, _, dest, _, _ ->
try { try {
val input = (dest.toString() + source.toString()).toInt() val input = (dest.toString() + source.toString()).toInt()
@@ -168,57 +203,74 @@ class SettingsActivity : AppCompatActivity(),
nfe.sendSilentlyWithAcraWithName("ArticleViewerPreferenceFragment > filters") nfe.sendSilentlyWithAcraWithName("ArticleViewerPreferenceFragment > filters")
} }
"" ""
} },
) )
} }
} }
} }
class OfflinePreferenceFragment : PreferenceFragmentCompat() { class OfflinePreferenceFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(
savedInstanceState: Bundle?,
rootKey: String?,
) {
setPreferencesFromResource(R.xml.pref_offline, rootKey) setPreferencesFromResource(R.xml.pref_offline, rootKey)
} }
} }
class ThemePreferenceFragment : PreferenceFragmentCompat() { class ThemePreferenceFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(
savedInstanceState: Bundle?,
rootKey: String?,
) {
setPreferencesFromResource(R.xml.pref_theme, rootKey) setPreferencesFromResource(R.xml.pref_theme, rootKey)
preferenceManager.findPreference<Preference>("currentMode")?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> preferenceManager.findPreference<Preference>("currentMode")?.onPreferenceChangeListener =
AppCompatDelegate.setDefaultNightMode(newValue.toString().toInt()) // ListPreference Only takes string-arrays ¯\_(ツ)_/¯ Preference.OnPreferenceChangeListener { _, newValue ->
AppCompatDelegate.setDefaultNightMode(
newValue.toString().toInt()
) // ListPreference Only takes string-arrays ¯\_(ツ)_/¯
true true
} }
} }
} }
class LinksPreferenceFragment : PreferenceFragmentCompat() { class LinksPreferenceFragment : PreferenceFragmentCompat() {
private fun openUrl(uri: Uri?) { private fun openUrl(url: String) {
val browserIntent = Intent(Intent.ACTION_VIEW, uri) context?.openUrlInBrowser(url)
startActivity(browserIntent)
} }
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(
savedInstanceState: Bundle?,
rootKey: String?,
) {
setPreferencesFromResource(R.xml.pref_links, rootKey) setPreferencesFromResource(R.xml.pref_links, rootKey)
preferenceManager.findPreference<Preference>("trackerLink")?.onPreferenceClickListener = Preference.OnPreferenceClickListener { preferenceManager.findPreference<Preference>("trackerLink")?.onPreferenceClickListener =
openUrl(Uri.parse(AppSettingsService.trackerUrl)) Preference.OnPreferenceClickListener {
openUrl(AppSettingsService.trackerUrl)
true true
} }
preferenceManager.findPreference<Preference>("sourceLink")?.onPreferenceClickListener = Preference.OnPreferenceClickListener { preferenceManager.findPreference<Preference>("sourceLink")?.onPreferenceClickListener =
openUrl(Uri.parse(AppSettingsService.sourceUrl)) Preference.OnPreferenceClickListener {
openUrl(AppSettingsService.sourceUrl)
false false
} }
preferenceManager.findPreference<Preference>("translation")?.onPreferenceClickListener = Preference.OnPreferenceClickListener { preferenceManager.findPreference<Preference>("translation")?.onPreferenceClickListener =
openUrl(Uri.parse(AppSettingsService.translationUrl)) Preference.OnPreferenceClickListener {
openUrl(AppSettingsService.translationUrl)
false false
} }
} }
} }
class ExperimentalPreferenceFragment : PreferenceFragmentCompat() { class ExperimentalPreferenceFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(
savedInstanceState: Bundle?,
rootKey: String?,
) {
setPreferencesFromResource(R.xml.pref_experimental, rootKey) setPreferencesFromResource(R.xml.pref_experimental, rootKey)
} }
} }
@@ -0,0 +1,21 @@
package bou.amine.apps.readerforselfossv2.android.testing
import androidx.test.espresso.idling.CountingIdlingResource
object CountingIdlingResourceSingleton {
private const val RESOURCE = "GLOBAL"
@JvmField
val countingIdlingResource = CountingIdlingResource(RESOURCE)
fun increment() {
countingIdlingResource.increment()
}
fun decrement() {
if (!countingIdlingResource.isIdleNow) {
countingIdlingResource.decrement()
}
}
}
@@ -0,0 +1,19 @@
package bou.amine.apps.readerforselfossv2.android.testing
import android.os.Build
class TestingHelper {
fun isUnitTest(): Boolean {
var device = Build.DEVICE
var product = Build.PRODUCT
if (device == null) {
device = ""
}
if (product == null) {
product = ""
}
return device == "robolectric" && product == "robolectric"
}
}
@@ -5,7 +5,10 @@ import android.content.Intent
import bou.amine.apps.readerforselfossv2.android.R import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.utils.toStringUriWithHttp import bou.amine.apps.readerforselfossv2.utils.toStringUriWithHttp
fun Context.shareLink(itemUrl: String, itemTitle: String) { fun Context.shareLink(
itemUrl: String,
itemTitle: String,
) {
val sendIntent = Intent() val sendIntent = Intent()
sendIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK sendIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
sendIntent.action = Intent.ACTION_SEND sendIntent.action = Intent.ACTION_SEND
@@ -15,7 +18,7 @@ fun Context.shareLink(itemUrl: String, itemTitle: String) {
startActivity( startActivity(
Intent.createChooser( Intent.createChooser(
sendIntent, sendIntent,
getString(R.string.share) getString(R.string.share),
).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) ).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
) )
} }
@@ -12,16 +12,19 @@ import bou.amine.apps.readerforselfossv2.android.model.toTextDrawableString
import com.google.android.material.imageview.ShapeableImageView import com.google.android.material.imageview.ShapeableImageView
import kotlin.math.abs import kotlin.math.abs
class CircleImageView @JvmOverloads constructor( class CircleImageView
@JvmOverloads
constructor(
context: Context, context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = 0 defStyleAttr: Int = 0,
) : RelativeLayout(context, attrs, defStyleAttr) { ) : RelativeLayout(context, attrs, defStyleAttr) {
val view: View val view: View
val imageView: ShapeableImageView val imageView: ShapeableImageView
val textView: TextView val textView: TextView
private val colorScheme = listOf( private val colorScheme =
listOf(
-0x1a8c8d, -0x1a8c8d,
-0xf9d6e, -0xf9d6e,
-0x459738, -0x459738,
@@ -38,7 +41,7 @@ class CircleImageView @JvmOverloads constructor(
-0x2ab1, -0x2ab1,
-0x48b3, -0x48b3,
-0x5e7781, -0x5e7781,
-0x6f5b52 -0x6f5b52,
) )
init { init {
@@ -1,6 +1,7 @@
package bou.amine.apps.readerforselfossv2.android.utils package bou.amine.apps.readerforselfossv2.android.utils
import android.app.Activity import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
@@ -17,18 +18,18 @@ import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.utils.toStringUriWithHttp import bou.amine.apps.readerforselfossv2.utils.toStringUriWithHttp
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
fun Context.openItemUrl( fun Context.openItemUrl(
currentItem: Int, currentItem: Int,
linkDecoded: String, linkDecoded: String,
articleViewer: Boolean, articleViewer: Boolean,
app: Activity app: Activity,
) { ) {
if (!linkDecoded.isUrlValid()) { if (!linkDecoded.isUrlValid()) {
Toast.makeText( Toast.makeText(
this, this,
this.getString(R.string.cant_open_invalid_url), this.getString(R.string.cant_open_invalid_url),
Toast.LENGTH_LONG Toast.LENGTH_LONG,
).show() ).show()
} else { } else {
if (articleViewer) { if (articleViewer) {
@@ -36,10 +37,7 @@ fun Context.openItemUrl(
intent.putExtra("currentItem", currentItem) intent.putExtra("currentItem", currentItem)
app.startActivity(intent) app.startActivity(intent)
} else { } else {
val intent = Intent(Intent.ACTION_VIEW) this.openUrlInBrowserAsNewTask(linkDecoded)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
intent.data = Uri.parse(linkDecoded.toStringUriWithHttp())
startActivity(intent)
} }
} }
} }
@@ -58,15 +56,38 @@ fun String.isBaseUrlInvalid(): Boolean {
return !(Patterns.WEB_URL.matcher(this).matches() && existsAndEndsWithSlash) return !(Patterns.WEB_URL.matcher(this).matches() && existsAndEndsWithSlash)
} }
fun Context.openInBrowserAsNewTask(i: SelfossModel.Item) { fun Context.openItemUrlInBrowserAsNewTask(i: SelfossModel.Item) {
this.openUrlInBrowserAsNewTask(i.getLinkDecoded().toStringUriWithHttp())
}
fun Context.openUrlInBrowserAsNewTask(url: String) {
val intent = Intent(Intent.ACTION_VIEW) val intent = Intent(Intent.ACTION_VIEW)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
intent.data = Uri.parse(i.getLinkDecoded().toStringUriWithHttp()) intent.data = Uri.parse(url)
startActivity(intent) this.mayBeStartActivity(intent)
}
fun Context.openUrlInBrowser(url: String) {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(url)
this.mayBeStartActivity(intent)
}
fun Context.mayBeStartActivity(intent: Intent) {
try {
this.startActivity(intent)
} catch (e: ActivityNotFoundException) {
Toast.makeText(this, getString(R.string.no_browser), Toast.LENGTH_SHORT).show()
}
} }
class LinkOnTouchListener : View.OnTouchListener { class LinkOnTouchListener : View.OnTouchListener {
override fun onTouch(v: View?, event: MotionEvent?): Boolean { override fun onTouch(
v: View?,
event: MotionEvent?,
): Boolean {
var ret = false var ret = false
val widget: TextView = v as TextView val widget: TextView = v as TextView
val text: CharSequence = widget.text val text: CharSequence = widget.text
@@ -1,4 +1,4 @@
package bou.amine.apps.readerforselfossv2.android package bou.amine.apps.readerforselfossv2.android.utils.acra
import org.acra.ACRA import org.acra.ACRA
import org.acra.ktx.sendSilentlyWithAcra import org.acra.ktx.sendSilentlyWithAcra
@@ -0,0 +1,29 @@
package bou.amine.apps.readerforselfossv2.android.utils.acra
import android.content.Context
import android.os.DeadSystemException
import com.google.auto.service.AutoService
import org.acra.builder.ReportBuilder
import org.acra.config.CoreConfiguration
import org.acra.config.ReportingAdministrator
import org.acra.data.CrashReportData
@AutoService(ReportingAdministrator::class)
class AcraReportingAdministrator : ReportingAdministrator {
override fun shouldStartCollecting(
context: Context,
config: CoreConfiguration,
reportBuilder: ReportBuilder
): Boolean {
return reportBuilder.exception !is DeadSystemException && (reportBuilder.exception != null && reportBuilder.exception!!::class.simpleName != "CannotDeliverBroadcastException")
}
override fun shouldSendReport(
context: Context,
config: CoreConfiguration,
crashReportData: CrashReportData
): Boolean {
return crashReportData.get("BRAND") != "redroid"
}
}
@@ -8,5 +8,4 @@ fun TextBadgeItem.removeBadge(): TextBadgeItem {
return this return this
} }
fun TextBadgeItem.maybeShow(): TextBadgeItem = fun TextBadgeItem.maybeShow(): TextBadgeItem = if (this.isHidden) this.show() else this
if (this.isHidden) this.show() else this
@@ -10,14 +10,19 @@ import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.InputStream import java.io.InputStream
fun Context.bitmapCenterCrop(url: String, iv: ImageView) = fun Context.bitmapCenterCrop(
Glide.with(this) url: String,
iv: ImageView,
) = Glide.with(this)
.asBitmap() .asBitmap()
.load(url) .load(url)
.apply(RequestOptions.centerCropTransform()) .apply(RequestOptions.centerCropTransform())
.into(iv) .into(iv)
fun Context.circularDrawable(url: String, view: CircleImageView) { fun Context.circularDrawable(
url: String,
view: CircleImageView,
) {
view.textView.text = "" view.textView.text = ""
Glide.with(this) Glide.with(this)
@@ -25,7 +30,10 @@ fun Context.circularDrawable(url: String, view: CircleImageView) {
.into(view.imageView) .into(view.imageView)
} }
fun getBitmapInputStream(bitmap:Bitmap,compressFormat: Bitmap.CompressFormat): InputStream { fun getBitmapInputStream(
bitmap: Bitmap,
compressFormat: Bitmap.CompressFormat,
): InputStream {
val byteArrayOutputStream = ByteArrayOutputStream() val byteArrayOutputStream = ByteArrayOutputStream()
bitmap.compress(compressFormat, 80, byteArrayOutputStream) bitmap.compress(compressFormat, 80, byteArrayOutputStream)
val bitmapData: ByteArray = byteArrayOutputStream.toByteArray() val bitmapData: ByteArray = byteArrayOutputStream.toByteArray()
@@ -19,7 +19,8 @@ class AppViewModel(private val repository: Repository) : ViewModel() {
if (isConnected && !wasConnected && repository.connectionMonitored) { if (isConnected && !wasConnected && repository.connectionMonitored) {
_networkAvailableProvider.emit(true) _networkAvailableProvider.emit(true)
wasConnected = true wasConnected = true
} else if (!isConnected && wasConnected && repository.connectionMonitored){ } else if (!isConnected && wasConnected && repository.connectionMonitored)
{
_networkAvailableProvider.emit(false) _networkAvailableProvider.emit(false)
wasConnected = false wasConnected = false
} }
@@ -23,6 +23,7 @@
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical" android:orientation="vertical"
android:padding="@dimen/activity_horizontal_margin"> android:padding="@dimen/activity_horizontal_margin">
<!-- Login progress --> <!-- Login progress -->