Compare commits
	
		
			34 Commits
		
	
	
		
			v124010031
			...
			v124123641
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| d81ced3964 | |||
| fbafece1fa | |||
| cbed8f07cb | |||
| f54fcc3ba1 | |||
| 
						 | 
					aad93ef722 | ||
| 
						 | 
					9e83af0302 | ||
| 
						 | 
					24b86e66b4 | ||
| 
						 | 
					641c444061 | ||
| 0902c61544 | |||
| 
						 | 
					6790152a0b | ||
| 
						 | 
					46d1ba418e | ||
| 
						 | 
					436373d0ad | ||
| 
						 | 
					5b9b51c02d | ||
| b81abe384a | |||
| 
						 | 
					851f862dbe | ||
| 
						 | 
					8d7e302af8 | ||
| 236e1cca90 | |||
| 3a33cb4510 | |||
| 0bf9ca9a49 | |||
| 
						 | 
					61e0087894 | ||
| 
						 | 
					1ec05d9913 | ||
| 
						 | 
					859bd91bbb | ||
| 
						 | 
					204b736c53 | ||
| 
						 | 
					f24609c143 | ||
| 
						 | 
					b94d7dc537 | ||
| 
						 | 
					41910cc4cd | ||
| 
						 | 
					db166ca9d4 | ||
| 
						 | 
					db0d5a4a85 | ||
| 
						 | 
					3bc0d7cf95 | ||
| 
						 | 
					8f464d95fd | ||
| 
						 | 
					5ccd6a3368 | ||
| 
						 | 
					cdbded246e | ||
| 
						 | 
					750c7758bd | ||
| 
						 | 
					22f8b14ecd | 
							
								
								
									
										170
									
								
								.drone.yml
									
									
									
									
									
								
							
							
						
						
									
										170
									
								
								.drone.yml
									
									
									
									
									
								
							@@ -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 'shared/**/*.kt' 'androidApp/**/*.kt' '!shared/build' || true
 | 
			
		||||
      - echo "---------------------------------------------------------"
 | 
			
		||||
      - echo "Detecting..."
 | 
			
		||||
      - ./detekt-cli-1.23.1/bin/detekt-cli --all-rules --excludes '**/shared/build/**/*.kt' || 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-bouabdallaoui.fr
 | 
			
		||||
      username: ubuntu
 | 
			
		||||
      key:
 | 
			
		||||
        from_secret: privateKey
 | 
			
		||||
      port: 22
 | 
			
		||||
      target: /home/ubuntu/
 | 
			
		||||
      source: version.txt
 | 
			
		||||
 | 
			
		||||
  - name: deploy
 | 
			
		||||
    image: appleboy/drone-ssh
 | 
			
		||||
    settings:
 | 
			
		||||
      host: amine-bouabdallaoui.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-bouabdallaoui.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-bouabdallaoui.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
 | 
			
		||||
							
								
								
									
										10
									
								
								.gitea/workflows/assets/docker-compose.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								.gitea/workflows/assets/docker-compose.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
version: '3'
 | 
			
		||||
services:
 | 
			
		||||
  selfoss:
 | 
			
		||||
    container_name: selfoss
 | 
			
		||||
    image: rsprta/selfoss
 | 
			
		||||
    network_mode: "host"
 | 
			
		||||
    ports:
 | 
			
		||||
      - "8888:8888"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										24
									
								
								.gitea/workflows/common_build.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								.gitea/workflows/common_build.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
			
		||||
name: Build
 | 
			
		||||
on:
 | 
			
		||||
  workflow_call:
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  BuildAndTest:
 | 
			
		||||
    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'
 | 
			
		||||
      - name: Setup Android SDK
 | 
			
		||||
        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 test --stacktrace
 | 
			
		||||
							
								
								
									
										126
									
								
								.gitea/workflows/on_merge_on_release.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								.gitea/workflows/on_merge_on_release.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,126 @@
 | 
			
		||||
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'
 | 
			
		||||
      - 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
 | 
			
		||||
							
								
								
									
										26
									
								
								.gitea/workflows/on_pr.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								.gitea/workflows/on_pr.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
			
		||||
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' # See 'Supported distributions' for available options
 | 
			
		||||
          java-version: '17'
 | 
			
		||||
      - 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:
 | 
			
		||||
    uses: ./.gitea/workflows/common_build.yml
 | 
			
		||||
							
								
								
									
										9
									
								
								.gitea/workflows/on_push.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								.gitea/workflows/on_push.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
name: Check master code
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    branches:
 | 
			
		||||
      - master
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  build:
 | 
			
		||||
    uses: ./.gitea/workflows/common_build.yml
 | 
			
		||||
							
								
								
									
										44
									
								
								.gitea/workflows/on_push_coverage.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								.gitea/workflows/on_push_coverage.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,44 @@
 | 
			
		||||
name: Check master code
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    branches:
 | 
			
		||||
      - master
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  coverage:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
        with:
 | 
			
		||||
          fetch-depth: 0
 | 
			
		||||
      - name: Fetch tags
 | 
			
		||||
        run: git fetch --tags -p
 | 
			
		||||
      - 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
 | 
			
		||||
      - 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: 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
 | 
			
		||||
							
								
								
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -320,4 +320,7 @@ fabric.properties
 | 
			
		||||
# End of https://www.toptal.com/developers/gitignore/api/gradle,kotlin,androidstudio,android,xcode,swift
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
crowdin.properties
 | 
			
		||||
crowdin.properties
 | 
			
		||||
 | 
			
		||||
.kotlin/
 | 
			
		||||
build-cache/
 | 
			
		||||
							
								
								
									
										77
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										77
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -1,3 +1,80 @@
 | 
			
		||||
**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.
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
# ReaderForSelfoss-multiplatform [](https://build.amine-bouabdallaoui.fr/Louvorg/ReaderForSelfoss-multiplatform)
 | 
			
		||||
# ReaderForSelfoss-multiplatform [](https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderForSelfoss-multiplatform/actions?workflow=on_push.yml&actor=0&status=0)
 | 
			
		||||
 | 
			
		||||
[](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>
 | 
			
		||||
 | 
			
		||||
## 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 ?
 | 
			
		||||
 | 
			
		||||
<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>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								androidApp/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								androidApp/.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -1 +1,2 @@
 | 
			
		||||
/build
 | 
			
		||||
.kotlin/
 | 
			
		||||
@@ -84,6 +84,7 @@ android {
 | 
			
		||||
 | 
			
		||||
        // tests
 | 
			
		||||
        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
 | 
			
		||||
        testInstrumentationRunnerArguments["clearPackageData"] = "true"
 | 
			
		||||
    }
 | 
			
		||||
    packaging {
 | 
			
		||||
        resources {
 | 
			
		||||
@@ -107,6 +108,13 @@ android {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    namespace = "bou.amine.apps.readerforselfossv2.android"
 | 
			
		||||
    testOptions {
 | 
			
		||||
        animationsDisabled = true
 | 
			
		||||
        execution = "ANDROIDX_TEST_ORCHESTRATOR"
 | 
			
		||||
        unitTests {
 | 
			
		||||
            isIncludeAndroidResources = true
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -184,9 +192,18 @@ dependencies {
 | 
			
		||||
    testImplementation("io.mockk:mockk:1.12.0")
 | 
			
		||||
    testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1")
 | 
			
		||||
    implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
 | 
			
		||||
    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-toast:$acraVersion")
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
tasks.withType<Test> {
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,90 @@
 | 
			
		||||
package bou.amine.apps.readerforselfossv2.android
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import androidx.annotation.ArrayRes
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
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())
 | 
			
		||||
    Thread.sleep(10000)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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())))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,137 @@
 | 
			
		||||
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
 | 
			
		||||
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()
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
        openActionBarOverflowOrOptionsMenu(
 | 
			
		||||
            ApplicationProvider.getApplicationContext<Context>()
 | 
			
		||||
        )
 | 
			
		||||
        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())
 | 
			
		||||
 | 
			
		||||
        openActionBarOverflowOrOptionsMenu(
 | 
			
		||||
            ApplicationProvider.getApplicationContext<Context>()
 | 
			
		||||
        )
 | 
			
		||||
        onView(withText(R.string.readAll)).perform(click())
 | 
			
		||||
        onView(withText(R.string.markall_dialog_message)).check(matches(isDisplayed()))
 | 
			
		||||
        onView(isRoot()).perform(ViewActions.pressBack())
 | 
			
		||||
        openActionBarOverflowOrOptionsMenu(
 | 
			
		||||
            ApplicationProvider.getApplicationContext<Context>()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        onView(withText(R.string.menu_home_sources)).perform(click())
 | 
			
		||||
        onView(withId(R.id.fab)).check(matches(isDisplayed()))
 | 
			
		||||
        onView(isRoot()).perform(ViewActions.pressBack())
 | 
			
		||||
        openActionBarOverflowOrOptionsMenu(
 | 
			
		||||
            ApplicationProvider.getApplicationContext<Context>()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        onView(withText(R.string.title_activity_settings)).perform(click())
 | 
			
		||||
        onView(withText(R.string.pref_header_general)).check(matches(isDisplayed()))
 | 
			
		||||
        onView(isRoot()).perform(ViewActions.pressBack())
 | 
			
		||||
        openActionBarOverflowOrOptionsMenu(
 | 
			
		||||
            ApplicationProvider.getApplicationContext<Context>()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        onView(withText(R.string.menu_home_refresh)).perform(click())
 | 
			
		||||
        onView(withText(R.string.refresh_dialog_message)).check(matches(isDisplayed()))
 | 
			
		||||
        onView(isRoot()).perform(ViewActions.pressBack())
 | 
			
		||||
        openActionBarOverflowOrOptionsMenu(
 | 
			
		||||
            ApplicationProvider.getApplicationContext<Context>()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        /*onView(withText(R.string.issue_tracker_link)).perform(click())
 | 
			
		||||
        onView(withText(R.string.markall_dialog_message)).check(matches(isDisplayed()))
 | 
			
		||||
        onView(isRoot()).perform(ViewActions.pressBack())
 | 
			
		||||
        openActionBarOverflowOrOptionsMenu(
 | 
			
		||||
            ApplicationProvider.getApplicationContext<Context>()
 | 
			
		||||
        )*/
 | 
			
		||||
 | 
			
		||||
        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("172.17.0.1: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,172 @@
 | 
			
		||||
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.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(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,104 @@
 | 
			
		||||
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.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()
 | 
			
		||||
        openActionBarOverflowOrOptionsMenu(
 | 
			
		||||
            ApplicationProvider.getApplicationContext<Context>()
 | 
			
		||||
        )
 | 
			
		||||
        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,65 @@
 | 
			
		||||
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.action.ViewActions.scrollCompletelyTo
 | 
			
		||||
import androidx.test.espresso.action.ViewActions.typeTextIntoFocusedView
 | 
			
		||||
import androidx.test.espresso.assertion.ViewAssertions.matches
 | 
			
		||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
 | 
			
		||||
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.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()
 | 
			
		||||
        openActionBarOverflowOrOptionsMenu(
 | 
			
		||||
            ApplicationProvider.getApplicationContext<Context>()
 | 
			
		||||
        )
 | 
			
		||||
        onView(withText(R.string.menu_home_sources))
 | 
			
		||||
            .perform(click())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun addSource() {
 | 
			
		||||
        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("https://lorem-rss.herokuapp.com/feed?unit=year&interval=1&length=10"))
 | 
			
		||||
        onView(withId(R.id.tags))
 | 
			
		||||
            .perform(click()).perform(typeTextIntoFocusedView("tag1,tag2,tag3"))
 | 
			
		||||
        onView(withId(R.id.spoutsSpinner))
 | 
			
		||||
            .perform(click())
 | 
			
		||||
        onView(withText("RSS Feed"))
 | 
			
		||||
            .perform(scrollCompletelyTo())
 | 
			
		||||
            .perform(click())
 | 
			
		||||
        onView(withId(R.id.saveBtn))
 | 
			
		||||
            .perform(click())
 | 
			
		||||
        onView(withText(sourceName)).check(matches(isDisplayed()))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,101 @@
 | 
			
		||||
package bou.amine.apps.readerforselfossv2.android
 | 
			
		||||
 | 
			
		||||
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.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)
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
@@ -13,7 +13,11 @@ import androidx.appcompat.app.AppCompatActivity
 | 
			
		||||
import androidx.appcompat.widget.SearchView
 | 
			
		||||
import androidx.core.view.doOnNextLayout
 | 
			
		||||
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.ExistingPeriodicWorkPolicy
 | 
			
		||||
import androidx.work.PeriodicWorkRequestBuilder
 | 
			
		||||
@@ -25,6 +29,7 @@ import bou.amine.apps.readerforselfossv2.android.background.LoadingWorker
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.databinding.ActivityHomeBinding
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.fragments.FilterSheetFragment
 | 
			
		||||
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.removeBadge
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
 | 
			
		||||
@@ -58,7 +63,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
			
		||||
    private lateinit var recyclerViewScrollListener: RecyclerView.OnScrollListener
 | 
			
		||||
    private lateinit var binding: ActivityHomeBinding
 | 
			
		||||
 | 
			
		||||
    private var recyclerAdapter: RecyclerView.Adapter<*>? = null
 | 
			
		||||
    private var recyclerAdapter: ItemsAdapter<out RecyclerView.ViewHolder>? = null
 | 
			
		||||
 | 
			
		||||
    private var fromTabShortcut: Boolean = false
 | 
			
		||||
 | 
			
		||||
@@ -80,7 +85,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
			
		||||
        repository.offlineOverride = intent.getBooleanExtra("startOffline", false)
 | 
			
		||||
 | 
			
		||||
        if (fromTabShortcut) {
 | 
			
		||||
            elementsShown = ItemType.fromInt(intent.getIntExtra("shortcutTab", ItemType.UNREAD.position))
 | 
			
		||||
            elementsShown =
 | 
			
		||||
                ItemType.fromInt(intent.getIntExtra("shortcutTab", ItemType.UNREAD.position))
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        setContentView(view)
 | 
			
		||||
@@ -92,8 +98,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
			
		||||
        handleSwipeRefreshLayout()
 | 
			
		||||
 | 
			
		||||
        if (appSettingsService.isItemCachingEnabled()) {
 | 
			
		||||
            CountingIdlingResourceSingleton.increment()
 | 
			
		||||
            CoroutineScope(Dispatchers.Main).launch {
 | 
			
		||||
                repository.tryToCacheItemsAndGetNewOnes()
 | 
			
		||||
                CountingIdlingResourceSingleton.decrement()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@@ -107,9 +115,11 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
			
		||||
        binding.swipeRefreshLayout.setOnRefreshListener {
 | 
			
		||||
            repository.offlineOverride = false
 | 
			
		||||
            lastFetchDone = false
 | 
			
		||||
            CountingIdlingResourceSingleton.increment()
 | 
			
		||||
            CoroutineScope(Dispatchers.Main).launch {
 | 
			
		||||
                getElementsAccordingToTab()
 | 
			
		||||
                binding.swipeRefreshLayout.isRefreshing = false
 | 
			
		||||
                CountingIdlingResourceSingleton.decrement()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -270,9 +280,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
			
		||||
        handleGDPRDialog(appSettingsService.settings.getBoolean("GDPR_shown", false))
 | 
			
		||||
 | 
			
		||||
        handleRecurringTask()
 | 
			
		||||
 | 
			
		||||
        CountingIdlingResourceSingleton.increment()
 | 
			
		||||
        CoroutineScope(Dispatchers.Main).launch {
 | 
			
		||||
            repository.handleDBActions()
 | 
			
		||||
            CountingIdlingResourceSingleton.decrement()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        getElementsAccordingToTab()
 | 
			
		||||
@@ -311,6 +322,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
			
		||||
                        )
 | 
			
		||||
                    binding.recyclerView.layoutManager = layoutManager
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            is GridLayoutManager ->
 | 
			
		||||
                if (appSettingsService.isCardViewEnabled()) {
 | 
			
		||||
                    layoutManager =
 | 
			
		||||
@@ -322,6 +334,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
			
		||||
                        StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
 | 
			
		||||
                    binding.recyclerView.layoutManager = layoutManager
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            else ->
 | 
			
		||||
                if (currentManager == null) {
 | 
			
		||||
                    if (!appSettingsService.isCardViewEnabled()) {
 | 
			
		||||
@@ -358,12 +371,14 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
			
		||||
                            } else {
 | 
			
		||||
                                layoutManager.scrollToPositionWithOffset(0, 0)
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                        is GridLayoutManager ->
 | 
			
		||||
                            if (layoutManager.findFirstCompletelyVisibleItemPosition() == 0) {
 | 
			
		||||
                                getElementsAccordingToTab()
 | 
			
		||||
                            } else {
 | 
			
		||||
                                layoutManager.scrollToPositionWithOffset(0, 0)
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                        else -> Unit
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
@@ -416,6 +431,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
			
		||||
                manager.findLastCompletelyVisibleItemPositions(
 | 
			
		||||
                    null,
 | 
			
		||||
                ).last()
 | 
			
		||||
 | 
			
		||||
            is GridLayoutManager -> manager.findLastCompletelyVisibleItemPosition()
 | 
			
		||||
            else -> 0
 | 
			
		||||
        }
 | 
			
		||||
@@ -444,6 +460,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
			
		||||
        appendResults: Boolean,
 | 
			
		||||
        itemType: ItemType,
 | 
			
		||||
    ) {
 | 
			
		||||
        CountingIdlingResourceSingleton.increment()
 | 
			
		||||
        CoroutineScope(Dispatchers.Main).launch {
 | 
			
		||||
            binding.swipeRefreshLayout.isRefreshing = true
 | 
			
		||||
            repository.displayedItems = itemType
 | 
			
		||||
@@ -455,6 +472,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
			
		||||
                }
 | 
			
		||||
            binding.swipeRefreshLayout.isRefreshing = false
 | 
			
		||||
            handleListResult()
 | 
			
		||||
            CountingIdlingResourceSingleton.decrement()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -465,8 +483,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
			
		||||
                when (oldManager) {
 | 
			
		||||
                    is StaggeredGridLayoutManager ->
 | 
			
		||||
                        oldManager.findFirstCompletelyVisibleItemPositions(null).last()
 | 
			
		||||
 | 
			
		||||
                    is GridLayoutManager ->
 | 
			
		||||
                        oldManager.findFirstCompletelyVisibleItemPosition()
 | 
			
		||||
 | 
			
		||||
                    else -> 0
 | 
			
		||||
                }
 | 
			
		||||
        }
 | 
			
		||||
@@ -498,7 +518,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
			
		||||
            }
 | 
			
		||||
            binding.recyclerView.adapter = recyclerAdapter
 | 
			
		||||
        } else {
 | 
			
		||||
            (recyclerAdapter as ItemsAdapter<*>).updateAllItems(items)
 | 
			
		||||
            recyclerAdapter!!.updateAllItems(items)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        reloadBadges()
 | 
			
		||||
@@ -507,8 +527,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
			
		||||
 | 
			
		||||
    private fun reloadBadges() {
 | 
			
		||||
        if (appSettingsService.isDisplayUnreadCountEnabled() || appSettingsService.isDisplayAllCountEnabled()) {
 | 
			
		||||
            CountingIdlingResourceSingleton.increment()
 | 
			
		||||
            CoroutineScope(Dispatchers.IO).launch {
 | 
			
		||||
                repository.reloadBadges()
 | 
			
		||||
                CountingIdlingResourceSingleton.decrement()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@@ -544,7 +566,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val searchItem = menu.findItem(R.id.action_search)
 | 
			
		||||
        val searchView = searchItem.getActionView() as SearchView
 | 
			
		||||
        val searchView = searchItem.actionView as SearchView
 | 
			
		||||
        searchView.setOnQueryTextListener(this)
 | 
			
		||||
 | 
			
		||||
        return true
 | 
			
		||||
@@ -567,18 +589,22 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
			
		||||
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
 | 
			
		||||
        when (item.itemId) {
 | 
			
		||||
            R.id.issue_tracker -> {
 | 
			
		||||
                val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(AppSettingsService.trackerUrl))
 | 
			
		||||
                val browserIntent =
 | 
			
		||||
                    Intent(Intent.ACTION_VIEW, Uri.parse(AppSettingsService.trackerUrl))
 | 
			
		||||
                startActivity(browserIntent)
 | 
			
		||||
                return true
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            R.id.action_filter -> {
 | 
			
		||||
                val filterSheetFragment = FilterSheetFragment()
 | 
			
		||||
                filterSheetFragment.show(supportFragmentManager, FilterSheetFragment.TAG)
 | 
			
		||||
                return true
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            R.id.refresh -> {
 | 
			
		||||
                needsConfirmation(R.string.menu_home_refresh, R.string.refresh_dialog_message) {
 | 
			
		||||
                    Toast.makeText(this, R.string.refresh_in_progress, Toast.LENGTH_SHORT).show()
 | 
			
		||||
                    CountingIdlingResourceSingleton.increment()
 | 
			
		||||
                    CoroutineScope(Dispatchers.Main).launch {
 | 
			
		||||
                        val updatedRemote = repository.updateRemote()
 | 
			
		||||
                        if (updatedRemote) {
 | 
			
		||||
@@ -595,15 +621,17 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
			
		||||
                                Toast.LENGTH_SHORT,
 | 
			
		||||
                            ).show()
 | 
			
		||||
                        }
 | 
			
		||||
                        CountingIdlingResourceSingleton.decrement()
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                return true
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            R.id.readAll -> {
 | 
			
		||||
                if (elementsShown == ItemType.UNREAD) {
 | 
			
		||||
                    needsConfirmation(R.string.readAll, R.string.markall_dialog_message) {
 | 
			
		||||
                        binding.swipeRefreshLayout.isRefreshing = true
 | 
			
		||||
 | 
			
		||||
                        CountingIdlingResourceSingleton.increment()
 | 
			
		||||
                        CoroutineScope(Dispatchers.Main).launch {
 | 
			
		||||
                            val success = repository.markAllAsRead(items)
 | 
			
		||||
                            if (success) {
 | 
			
		||||
@@ -624,13 +652,19 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
			
		||||
                            }
 | 
			
		||||
                            handleListResult()
 | 
			
		||||
                            binding.swipeRefreshLayout.isRefreshing = false
 | 
			
		||||
                            CountingIdlingResourceSingleton.decrement()
 | 
			
		||||
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                return true
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            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 {
 | 
			
		||||
                        repository.logout()
 | 
			
		||||
                    }
 | 
			
		||||
@@ -640,14 +674,17 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
			
		||||
                }
 | 
			
		||||
                return true
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            R.id.action_settings -> {
 | 
			
		||||
                settingsLauncher.launch(Intent(this, SettingsActivity::class.java))
 | 
			
		||||
                return true
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            R.id.action_sources -> {
 | 
			
		||||
                startActivity(Intent(this, SourcesActivity::class.java))
 | 
			
		||||
                return true
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            else -> return super.onOptionsItemSelected(item)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@@ -674,14 +711,21 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
			
		||||
                    .build()
 | 
			
		||||
 | 
			
		||||
            val backgroundWork =
 | 
			
		||||
                PeriodicWorkRequestBuilder<LoadingWorker>(appSettingsService.getRefreshMinutes(), TimeUnit.MINUTES)
 | 
			
		||||
                PeriodicWorkRequestBuilder<LoadingWorker>(
 | 
			
		||||
                    appSettingsService.getRefreshMinutes(),
 | 
			
		||||
                    TimeUnit.MINUTES
 | 
			
		||||
                )
 | 
			
		||||
                    .setConstraints(myConstraints)
 | 
			
		||||
                    .addTag("selfoss-loading")
 | 
			
		||||
                    .build()
 | 
			
		||||
 | 
			
		||||
            WorkManager.getInstance(
 | 
			
		||||
                baseContext,
 | 
			
		||||
            ).enqueueUniquePeriodicWork("selfoss-loading", ExistingPeriodicWorkPolicy.KEEP, backgroundWork)
 | 
			
		||||
            ).enqueueUniquePeriodicWork(
 | 
			
		||||
                "selfoss-loading",
 | 
			
		||||
                ExistingPeriodicWorkPolicy.KEEP,
 | 
			
		||||
                backgroundWork
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@@ -17,6 +17,7 @@ import androidx.appcompat.app.AlertDialog
 | 
			
		||||
import androidx.appcompat.app.AppCompatActivity
 | 
			
		||||
import androidx.appcompat.app.AppCompatDelegate
 | 
			
		||||
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.repository.Repository
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
 | 
			
		||||
@@ -102,9 +103,14 @@ class LoginActivity : AppCompatActivity(), DIAware {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun goToMain() {
 | 
			
		||||
        CountingIdlingResourceSingleton.increment()
 | 
			
		||||
        CoroutineScope(Dispatchers.Main).launch {
 | 
			
		||||
            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)
 | 
			
		||||
        startActivity(intent)
 | 
			
		||||
@@ -139,6 +145,7 @@ class LoginActivity : AppCompatActivity(), DIAware {
 | 
			
		||||
 | 
			
		||||
        repository.refreshLoginInformation(url, login, password)
 | 
			
		||||
 | 
			
		||||
        CountingIdlingResourceSingleton.increment()
 | 
			
		||||
        CoroutineScope(Dispatchers.Main).launch {
 | 
			
		||||
            try {
 | 
			
		||||
                repository.updateApiInformation()
 | 
			
		||||
@@ -165,6 +172,7 @@ class LoginActivity : AppCompatActivity(), DIAware {
 | 
			
		||||
                preferenceError()
 | 
			
		||||
            }
 | 
			
		||||
            showProgress(false)
 | 
			
		||||
            CountingIdlingResourceSingleton.decrement()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -261,20 +269,25 @@ class LoginActivity : AppCompatActivity(), DIAware {
 | 
			
		||||
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
 | 
			
		||||
        return when (item.itemId) {
 | 
			
		||||
            R.id.issue_tracker -> {
 | 
			
		||||
                val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(AppSettingsService.trackerUrl))
 | 
			
		||||
                val browserIntent =
 | 
			
		||||
                    Intent(Intent.ACTION_VIEW, Uri.parse(AppSettingsService.trackerUrl))
 | 
			
		||||
                startActivity(browserIntent)
 | 
			
		||||
                return true
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            R.id.about -> {
 | 
			
		||||
                LibsBuilder()
 | 
			
		||||
                    .withAboutIconShown(true)
 | 
			
		||||
                    .withAboutVersionShown(true)
 | 
			
		||||
                    .withAboutSpecial2("Bug reports").withAboutSpecial2Description(AppSettingsService.trackerUrl)
 | 
			
		||||
                    .withAboutSpecial1("Project Page").withAboutSpecial1Description(AppSettingsService.sourceUrl)
 | 
			
		||||
                    .withAboutSpecial2("Bug reports")
 | 
			
		||||
                    .withAboutSpecial2Description(AppSettingsService.trackerUrl)
 | 
			
		||||
                    .withAboutSpecial1("Project Page")
 | 
			
		||||
                    .withAboutSpecial1Description(AppSettingsService.sourceUrl)
 | 
			
		||||
                    .start(this)
 | 
			
		||||
                true
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            else -> super.onOptionsItemSelected(item)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@@ -10,6 +10,7 @@ import androidx.lifecycle.LifecycleOwner
 | 
			
		||||
import androidx.lifecycle.ProcessLifecycleOwner
 | 
			
		||||
import androidx.multidex.MultiDexApplication
 | 
			
		||||
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.dao.DriverFactory
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB
 | 
			
		||||
@@ -29,23 +30,27 @@ import org.acra.config.toast
 | 
			
		||||
import org.acra.data.StringFormat
 | 
			
		||||
import org.acra.ktx.initAcra
 | 
			
		||||
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 {
 | 
			
		||||
    override val di by DI.lazy {
 | 
			
		||||
        bind<AppSettingsService>() with singleton { AppSettingsService(ACRA.isACRASenderServiceProcess()) }
 | 
			
		||||
        bind<AppSettingsService>() with singleton { AppSettingsService(ACRA.isACRASenderServiceProcess() || TestingHelper().isUnitTest()) }
 | 
			
		||||
        import(networkModule)
 | 
			
		||||
        bind<DriverFactory>() with singleton { DriverFactory(applicationContext) }
 | 
			
		||||
        bind<ReaderForSelfossDB>() with singleton { ReaderForSelfossDB(driverFactory.createDriver()) }
 | 
			
		||||
        bind<Repository>() with
 | 
			
		||||
            singleton {
 | 
			
		||||
                Repository(
 | 
			
		||||
                    instance(),
 | 
			
		||||
                    instance(),
 | 
			
		||||
                    isConnectionAvailable,
 | 
			
		||||
                    instance(),
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
                singleton {
 | 
			
		||||
                    Repository(
 | 
			
		||||
                        instance(),
 | 
			
		||||
                        instance(),
 | 
			
		||||
                        isConnectionAvailable,
 | 
			
		||||
                        instance(),
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
        bind<ConnectivityStatus>() with singleton { ConnectivityStatus(applicationContext) }
 | 
			
		||||
        bind<AppViewModel>() with singleton { AppViewModel(repository = instance()) }
 | 
			
		||||
    }
 | 
			
		||||
@@ -194,4 +199,4 @@ class MyApp : MultiDexApplication(), DIAware {
 | 
			
		||||
            super.onPause(owner)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@@ -8,6 +8,7 @@ import androidx.appcompat.app.AppCompatActivity
 | 
			
		||||
import androidx.recyclerview.widget.LinearLayoutManager
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.adapters.SourcesListAdapter
 | 
			
		||||
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.repository.Repository
 | 
			
		||||
import kotlinx.coroutines.CoroutineScope
 | 
			
		||||
@@ -36,7 +37,8 @@ class SourcesActivity : AppCompatActivity(), DIAware {
 | 
			
		||||
        supportActionBar?.setDisplayShowHomeEnabled(true)
 | 
			
		||||
 | 
			
		||||
        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() {
 | 
			
		||||
@@ -53,6 +55,7 @@ class SourcesActivity : AppCompatActivity(), DIAware {
 | 
			
		||||
        binding.recyclerView.setHasFixedSize(true)
 | 
			
		||||
        binding.recyclerView.layoutManager = mLayoutManager
 | 
			
		||||
 | 
			
		||||
        CountingIdlingResourceSingleton.increment()
 | 
			
		||||
        CoroutineScope(Dispatchers.Main).launch {
 | 
			
		||||
            val response = repository.getSourcesDetails()
 | 
			
		||||
            if (response.isNotEmpty()) {
 | 
			
		||||
@@ -71,10 +74,11 @@ class SourcesActivity : AppCompatActivity(), DIAware {
 | 
			
		||||
                    Toast.LENGTH_SHORT,
 | 
			
		||||
                ).show()
 | 
			
		||||
            }
 | 
			
		||||
            CountingIdlingResourceSingleton.decrement()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        binding.fab.setOnClickListener {
 | 
			
		||||
            startActivity(Intent(this@SourcesActivity, UpsertSourceActivity::class.java))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
package bou.amine.apps.readerforselfossv2.android.adapters
 | 
			
		||||
 | 
			
		||||
import android.app.Activity
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
@@ -9,11 +8,11 @@ import android.widget.ImageView.ScaleType
 | 
			
		||||
import androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.R
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.databinding.CardItemBinding
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.sendSilentlyWithAcraWithName
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.utils.LinkOnTouchListener
 | 
			
		||||
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.openInBrowserAsNewTask
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.utils.openItemUrl
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.utils.shareLink
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.repository.Repository
 | 
			
		||||
@@ -31,10 +30,10 @@ import org.kodein.di.instance
 | 
			
		||||
 | 
			
		||||
class ItemCardAdapter(
 | 
			
		||||
    override val app: Activity,
 | 
			
		||||
    override var items: ArrayList<SelfossModel.Item>,
 | 
			
		||||
    override val updateItems: (ArrayList<SelfossModel.Item>) -> Unit,
 | 
			
		||||
    override val items: ArrayList<SelfossModel.Item>,
 | 
			
		||||
    override val updateHomeItems: (ArrayList<SelfossModel.Item>) -> Unit,
 | 
			
		||||
) : ItemsAdapter<ItemCardAdapter.ViewHolder>() {
 | 
			
		||||
    private val c: Context = app.baseContext
 | 
			
		||||
    override lateinit var binding: CardItemBinding
 | 
			
		||||
    private val imageMaxHeight: Int =
 | 
			
		||||
        c.resources.getDimension(R.dimen.card_image_max_height).toInt()
 | 
			
		||||
 | 
			
		||||
@@ -46,10 +45,36 @@ class ItemCardAdapter(
 | 
			
		||||
        parent: ViewGroup,
 | 
			
		||||
        viewType: Int,
 | 
			
		||||
    ): ViewHolder {
 | 
			
		||||
        val binding = CardItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
 | 
			
		||||
        binding = CardItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
 | 
			
		||||
        return ViewHolder(binding)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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.openInBrowserAsNewTask(items[position])
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onBindViewHolder(
 | 
			
		||||
        holder: ViewHolder,
 | 
			
		||||
        position: Int,
 | 
			
		||||
@@ -57,6 +82,9 @@ class ItemCardAdapter(
 | 
			
		||||
        with(holder) {
 | 
			
		||||
            val itm = items[position]
 | 
			
		||||
 | 
			
		||||
            handleClickListeners(binding, position)
 | 
			
		||||
            handleLinkOpening(binding, position)
 | 
			
		||||
 | 
			
		||||
            binding.favButton.isSelected = itm.starred
 | 
			
		||||
            if (appSettingsService.getPublicAccess()) {
 | 
			
		||||
                binding.favButton.visibility = View.GONE
 | 
			
		||||
@@ -68,7 +96,12 @@ class ItemCardAdapter(
 | 
			
		||||
 | 
			
		||||
            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()) {
 | 
			
		||||
                binding.itemImage.maxHeight = imageMaxHeight
 | 
			
		||||
@@ -92,52 +125,5 @@ class ItemCardAdapter(
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getItemCount(): Int {
 | 
			
		||||
        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,
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    inner class ViewHolder(val binding: CardItemBinding) : RecyclerView.ViewHolder(binding.root)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,15 +1,14 @@
 | 
			
		||||
package bou.amine.apps.readerforselfossv2.android.adapters
 | 
			
		||||
 | 
			
		||||
import android.app.Activity
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.R
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.databinding.ListItemBinding
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.sendSilentlyWithAcraWithName
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.utils.LinkOnTouchListener
 | 
			
		||||
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.repository.Repository
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
 | 
			
		||||
@@ -22,10 +21,10 @@ import org.kodein.di.instance
 | 
			
		||||
 | 
			
		||||
class ItemListAdapter(
 | 
			
		||||
    override val app: Activity,
 | 
			
		||||
    override var items: ArrayList<SelfossModel.Item>,
 | 
			
		||||
    override val updateItems: (ArrayList<SelfossModel.Item>) -> Unit,
 | 
			
		||||
    override val items: ArrayList<SelfossModel.Item>,
 | 
			
		||||
    override val updateHomeItems: (ArrayList<SelfossModel.Item>) -> Unit,
 | 
			
		||||
) : ItemsAdapter<ItemListAdapter.ViewHolder>() {
 | 
			
		||||
    private val c: Context = app.baseContext
 | 
			
		||||
    override lateinit var binding: ListItemBinding
 | 
			
		||||
 | 
			
		||||
    override val di: DI by closestDI(app)
 | 
			
		||||
    override val repository: Repository by instance()
 | 
			
		||||
@@ -35,7 +34,7 @@ class ItemListAdapter(
 | 
			
		||||
        parent: ViewGroup,
 | 
			
		||||
        viewType: Int,
 | 
			
		||||
    ): ViewHolder {
 | 
			
		||||
        val binding = ListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
 | 
			
		||||
        binding = ListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
 | 
			
		||||
        return ViewHolder(binding)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -46,13 +45,20 @@ class ItemListAdapter(
 | 
			
		||||
        with(holder) {
 | 
			
		||||
            val itm = items[position]
 | 
			
		||||
 | 
			
		||||
            handleLinkOpening(binding, position)
 | 
			
		||||
 | 
			
		||||
            binding.title.text = itm.title.getHtmlDecoded()
 | 
			
		||||
 | 
			
		||||
            binding.title.setOnTouchListener(LinkOnTouchListener())
 | 
			
		||||
 | 
			
		||||
            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.getIcon(repository.baseUrl).isEmpty()) {
 | 
			
		||||
@@ -66,23 +72,5 @@ class ItemListAdapter(
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getItemCount(): Int = items.size
 | 
			
		||||
 | 
			
		||||
    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,
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    inner class ViewHolder(val binding: ListItemBinding) : RecyclerView.ViewHolder(binding.root)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,13 @@
 | 
			
		||||
package bou.amine.apps.readerforselfossv2.android.adapters
 | 
			
		||||
 | 
			
		||||
import android.app.Activity
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.graphics.Color
 | 
			
		||||
import android.widget.TextView
 | 
			
		||||
import androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
import androidx.viewbinding.ViewBinding
 | 
			
		||||
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.repository.Repository
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
 | 
			
		||||
@@ -16,16 +19,20 @@ import kotlinx.coroutines.launch
 | 
			
		||||
import org.kodein.di.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 binding: ViewBinding
 | 
			
		||||
    abstract val appSettingsService: AppSettingsService
 | 
			
		||||
    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>) {
 | 
			
		||||
        this.items = items
 | 
			
		||||
        this.items.clear()
 | 
			
		||||
        this.items.addAll(items)
 | 
			
		||||
        updateHomeItems(items)
 | 
			
		||||
        notifyDataSetChanged()
 | 
			
		||||
        updateItems(this.items)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun unmarkSnackbar(
 | 
			
		||||
@@ -70,6 +77,18 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
 | 
			
		||||
        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) {
 | 
			
		||||
        if (items[position].unread) {
 | 
			
		||||
            readItemAtIndex(items[position], position)
 | 
			
		||||
@@ -89,7 +108,8 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
 | 
			
		||||
        if (repository.displayedItems == ItemType.UNREAD) {
 | 
			
		||||
            items.remove(item)
 | 
			
		||||
            notifyItemRemoved(position)
 | 
			
		||||
            updateItems(items)
 | 
			
		||||
            notifyItemRangeChanged(position, itemCount)
 | 
			
		||||
            updateHomeItems(items)
 | 
			
		||||
        } else {
 | 
			
		||||
            notifyItemChanged(position)
 | 
			
		||||
        }
 | 
			
		||||
@@ -118,13 +138,15 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
 | 
			
		||||
    ) {
 | 
			
		||||
        items.add(position, item)
 | 
			
		||||
        notifyItemInserted(position)
 | 
			
		||||
        updateItems(items)
 | 
			
		||||
        updateHomeItems(items)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun addItemsAtEnd(newItems: List<SelfossModel.Item>) {
 | 
			
		||||
        val oldSize = items.size
 | 
			
		||||
        items.addAll(newItems)
 | 
			
		||||
        notifyItemRangeInserted(oldSize, newItems.size)
 | 
			
		||||
        updateItems(items)
 | 
			
		||||
        updateHomeItems(items)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getItemCount(): Int = items.size
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -50,6 +50,33 @@ class SourcesListAdapter(
 | 
			
		||||
    ) {
 | 
			
		||||
        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()) {
 | 
			
		||||
            binding.itemImage.setBackgroundAndText(itm.title.getHtmlDecoded())
 | 
			
		||||
        } else {
 | 
			
		||||
@@ -72,38 +99,5 @@ class SourcesListAdapter(
 | 
			
		||||
 | 
			
		||||
    override fun getItemCount(): Int = items.size
 | 
			
		||||
 | 
			
		||||
    inner class ViewHolder(private 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))
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    inner class ViewHolder(val mView: ConstraintLayout) : RecyclerView.ViewHolder(mView)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -52,7 +52,6 @@ import org.kodein.di.DIAware
 | 
			
		||||
import org.kodein.di.android.x.closestDI
 | 
			
		||||
import org.kodein.di.instance
 | 
			
		||||
import java.net.MalformedURLException
 | 
			
		||||
import java.net.SocketTimeoutException
 | 
			
		||||
import java.net.URL
 | 
			
		||||
import java.util.*
 | 
			
		||||
import java.util.concurrent.ExecutionException
 | 
			
		||||
@@ -103,7 +102,12 @@ class ArticleFragment : Fragment(), DIAware {
 | 
			
		||||
            contentText = item.content
 | 
			
		||||
            contentTitle = item.title.getHtmlDecoded()
 | 
			
		||||
            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()
 | 
			
		||||
 | 
			
		||||
            fontSize = appSettingsService.getFontSize()
 | 
			
		||||
@@ -264,10 +268,7 @@ class ArticleFragment : Fragment(), DIAware {
 | 
			
		||||
                } else {
 | 
			
		||||
                    openInBrowserAfterFailing()
 | 
			
		||||
                }
 | 
			
		||||
            } catch (e: SocketTimeoutException) {
 | 
			
		||||
                openInBrowserAfterFailing()
 | 
			
		||||
            } catch (e: Exception) {
 | 
			
		||||
                e.sendSilentlyWithAcraWithName("getContentFromMercury > $url")
 | 
			
		||||
                openInBrowserAfterFailing()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -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"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -54,19 +54,24 @@
 | 
			
		||||
                android:layout_width="0dp"
 | 
			
		||||
                android:layout_height="wrap_content"
 | 
			
		||||
                android:layout_margin="8dp"
 | 
			
		||||
                android:layout_marginStart="8dp"
 | 
			
		||||
                android:layout_marginTop="8dp"
 | 
			
		||||
                android:layout_marginEnd="8dp"
 | 
			
		||||
                android:textAlignment="viewStart"
 | 
			
		||||
                android:textColor="?android:textColorPrimary"
 | 
			
		||||
                android:textStyle="bold"
 | 
			
		||||
                app:layout_constraintEnd_toEndOf="parent"
 | 
			
		||||
                app:layout_constraintStart_toEndOf="@+id/sourceImage"
 | 
			
		||||
                app:layout_constraintTop_toTopOf="@+id/sourceImage"
 | 
			
		||||
                app:layout_constraintTop_toTopOf="parent"
 | 
			
		||||
                tools:text="Titre" />
 | 
			
		||||
 | 
			
		||||
            <TextView
 | 
			
		||||
                android:id="@+id/sourceTitleAndDate"
 | 
			
		||||
                android:layout_width="0dp"
 | 
			
		||||
                android:layout_height="wrap_content"
 | 
			
		||||
                android:layout_marginStart="8dp"
 | 
			
		||||
                android:layout_marginTop="8dp"
 | 
			
		||||
                android:layout_marginEnd="8dp"
 | 
			
		||||
                android:textAlignment="viewStart"
 | 
			
		||||
                android:textColor="?android:textColorPrimary"
 | 
			
		||||
                android:textSize="14sp"
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,8 @@
 | 
			
		||||
        android:layout_width="46dp"
 | 
			
		||||
        android:layout_height="46dp"
 | 
			
		||||
        android:layout_marginStart="8dp"
 | 
			
		||||
        android:layout_marginTop="8dp"
 | 
			
		||||
        android:layout_marginBottom="8dp"
 | 
			
		||||
        app:layout_constraintBottom_toBottomOf="parent"
 | 
			
		||||
        app:layout_constraintStart_toStartOf="parent"
 | 
			
		||||
        app:layout_constraintTop_toTopOf="parent" />
 | 
			
		||||
@@ -20,7 +22,7 @@
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        android:layout_marginStart="8dp"
 | 
			
		||||
        android:layout_marginTop="8dp"
 | 
			
		||||
        android:layout_marginEnd="16dp"
 | 
			
		||||
        android:layout_marginEnd="8dp"
 | 
			
		||||
        android:ellipsize="end"
 | 
			
		||||
        android:fontFamily="sans-serif"
 | 
			
		||||
        android:maxLines="3"
 | 
			
		||||
@@ -38,15 +40,17 @@
 | 
			
		||||
        android:layout_width="0dp"
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        android:layout_marginStart="8dp"
 | 
			
		||||
        android:layout_marginEnd="16dp"
 | 
			
		||||
        android:layout_marginTop="16dp"
 | 
			
		||||
        android:layout_marginEnd="8dp"
 | 
			
		||||
        android:layout_marginBottom="8dp"
 | 
			
		||||
        android:gravity="start"
 | 
			
		||||
        android:maxLines="1"
 | 
			
		||||
        android:textAlignment="viewStart"
 | 
			
		||||
        android:textColor="?android:textColorPrimary"
 | 
			
		||||
        android:textSize="14sp"
 | 
			
		||||
        app:layout_constraintBottom_toBottomOf="parent"
 | 
			
		||||
        app:layout_constraintEnd_toEndOf="parent"
 | 
			
		||||
        app:layout_constraintStart_toEndOf="@+id/itemImage"
 | 
			
		||||
        app:layout_constraintTop_toBottomOf="@+id/itemImage"
 | 
			
		||||
        app:layout_constraintTop_toBottomOf="@+id/title"
 | 
			
		||||
        tools:text="Google Actualité Il y a 5h" />
 | 
			
		||||
 | 
			
		||||
</androidx.constraintlayout.widget.ConstraintLayout>
 | 
			
		||||
@@ -0,0 +1,77 @@
 | 
			
		||||
package bou.amine.apps.readerforselfossv2.android.tests.robolectric
 | 
			
		||||
 | 
			
		||||
import android.widget.Button
 | 
			
		||||
import android.widget.EditText
 | 
			
		||||
import androidx.core.view.isVisible
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.LoginActivity
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.R
 | 
			
		||||
import com.google.android.material.switchmaterial.SwitchMaterial
 | 
			
		||||
import org.junit.Assert.assertEquals
 | 
			
		||||
import org.junit.Test
 | 
			
		||||
import org.junit.runner.RunWith
 | 
			
		||||
import org.robolectric.Robolectric
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@RunWith(RobotElectriqueRunnerclass::class)
 | 
			
		||||
class LoginActivityTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun login_shouldDisplay() {
 | 
			
		||||
        Robolectric.buildActivity(LoginActivity::class.java).use { controller ->
 | 
			
		||||
            controller.setup() // Moves the Activity to the RESUMED state
 | 
			
		||||
 | 
			
		||||
            val activity = controller.get()
 | 
			
		||||
            assert(activity.findViewById<EditText>(R.id.urlView).isVisible)
 | 
			
		||||
            assert(activity.findViewById<SwitchMaterial>(R.id.selfSigned).isVisible)
 | 
			
		||||
            assert(activity.findViewById<SwitchMaterial>(R.id.selfSigned).isChecked.not())
 | 
			
		||||
            assert(activity.findViewById<SwitchMaterial>(R.id.withLogin).isVisible)
 | 
			
		||||
            assert(activity.findViewById<SwitchMaterial>(R.id.withLogin).isChecked.not())
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun urlError() {
 | 
			
		||||
        Robolectric.buildActivity(LoginActivity::class.java).use { controller ->
 | 
			
		||||
            controller.setup() // Moves the Activity to the RESUMED state
 | 
			
		||||
            val activity = controller.get()
 | 
			
		||||
 | 
			
		||||
            val urlView = activity.findViewById<EditText>(R.id.urlView)
 | 
			
		||||
            urlView.setText("172.17.0.1:8888")
 | 
			
		||||
 | 
			
		||||
            activity.findViewById<Button>(R.id.signInButton).performClick()
 | 
			
		||||
 | 
			
		||||
            urlView.performClick()
 | 
			
		||||
            assertEquals(activity.getString(R.string.login_url_problem), urlView.error)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun multiError() {
 | 
			
		||||
        Robolectric.buildActivity(LoginActivity::class.java).use { controller ->
 | 
			
		||||
            controller.setup() // Moves the Activity to the RESUMED state
 | 
			
		||||
            val activity = controller.get()
 | 
			
		||||
 | 
			
		||||
            val signInButton = activity.findViewById<Button>(R.id.signInButton)
 | 
			
		||||
            repeat(3) { signInButton.performClick() }
 | 
			
		||||
 | 
			
		||||
            // Vérifie que l'avertissement est affiché
 | 
			
		||||
            assertEquals(activity.getString(R.string.text_wrong_url), dialogMessage())
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* @Test
 | 
			
		||||
     fun connect() {
 | 
			
		||||
         Robolectric.buildActivity(LoginActivity::class.java).use { controller ->
 | 
			
		||||
             controller.setup() // Moves the Activity to the RESUMED state
 | 
			
		||||
             val activity = controller.get()
 | 
			
		||||
             val signInButton = activity.findViewById<Button>(R.id.signInButton)
 | 
			
		||||
             val urlView = activity.findViewById<EditText>(R.id.urlView)
 | 
			
		||||
             urlView.setText("http://10.0.2.2:8888")
 | 
			
		||||
             signInButton.performClick()
 | 
			
		||||
 | 
			
		||||
             val expectedIntent = Intent(activity, HomeActivity::class.java)
 | 
			
		||||
             val actual = shadowOf(activity).nextStartedActivity
 | 
			
		||||
             assertEquals(expectedIntent.component, actual.component)
 | 
			
		||||
         }
 | 
			
		||||
     }*/
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,12 @@
 | 
			
		||||
package bou.amine.apps.readerforselfossv2.android.tests.robolectric
 | 
			
		||||
 | 
			
		||||
import org.robolectric.RobolectricTestRunner
 | 
			
		||||
import org.robolectric.annotation.Config
 | 
			
		||||
 | 
			
		||||
class RobotElectriqueRunnerclass(testClass: Class<*>?) :
 | 
			
		||||
    RobolectricTestRunner(testClass) {
 | 
			
		||||
 | 
			
		||||
    override fun buildGlobalConfig(): Config {
 | 
			
		||||
        return Config.Builder().setSdk(25, 30, 33).build()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,23 @@
 | 
			
		||||
package bou.amine.apps.readerforselfossv2.android.tests.robolectric
 | 
			
		||||
 | 
			
		||||
import android.view.Menu
 | 
			
		||||
import android.widget.TextView
 | 
			
		||||
import androidx.annotation.IdRes
 | 
			
		||||
import org.junit.Assert.assertTrue
 | 
			
		||||
import org.robolectric.shadows.ShadowDialog
 | 
			
		||||
 | 
			
		||||
fun dialogMessage(): String {
 | 
			
		||||
    val latestDialog = ShadowDialog.getLatestDialog()
 | 
			
		||||
    return latestDialog.findViewById<TextView>(android.R.id.message)?.text.toString()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun Menu.assertClickable(@IdRes id: Int) {
 | 
			
		||||
    this.assertVisible(id)
 | 
			
		||||
    val item = this.findItem(id)
 | 
			
		||||
    assertTrue(item.isEnabled)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun Menu.assertVisible(@IdRes id: Int) {
 | 
			
		||||
    val item = this.findItem(id)
 | 
			
		||||
    assertTrue(item.isVisible)
 | 
			
		||||
}
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
package bou.amine.apps.readerforselfossv2.repository
 | 
			
		||||
package bou.amine.apps.readerforselfossv2.tests.repository
 | 
			
		||||
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.dao.SOURCE
 | 
			
		||||
@@ -6,12 +6,22 @@ import bou.amine.apps.readerforselfossv2.dao.TAG
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.model.StatusAndData
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.model.SuccessResponse
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.repository.Repository
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.rest.SelfossApi
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.utils.ItemType
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.utils.toView
 | 
			
		||||
import io.mockk.*
 | 
			
		||||
import junit.framework.TestCase.*
 | 
			
		||||
import io.mockk.clearAllMocks
 | 
			
		||||
import io.mockk.coEvery
 | 
			
		||||
import io.mockk.coVerify
 | 
			
		||||
import io.mockk.every
 | 
			
		||||
import io.mockk.mockk
 | 
			
		||||
import io.mockk.verify
 | 
			
		||||
import junit.framework.TestCase.assertEquals
 | 
			
		||||
import junit.framework.TestCase.assertFalse
 | 
			
		||||
import junit.framework.TestCase.assertNotSame
 | 
			
		||||
import junit.framework.TestCase.assertSame
 | 
			
		||||
import junit.framework.TestCase.assertTrue
 | 
			
		||||
import kotlinx.coroutines.flow.MutableStateFlow
 | 
			
		||||
import kotlinx.coroutines.runBlocking
 | 
			
		||||
import org.junit.Assert.assertNotEquals
 | 
			
		||||
@@ -65,15 +75,19 @@ class RepositoryTest {
 | 
			
		||||
        every { appSettingsService.isUpdateSourcesEnabled() } returns false
 | 
			
		||||
 | 
			
		||||
        coEvery { api.apiInformation() } returns
 | 
			
		||||
            StatusAndData(
 | 
			
		||||
                success = true,
 | 
			
		||||
                data = SelfossModel.ApiInformation("2.19-ba1e8e3", "4.0.0", SelfossModel.ApiConfiguration(false, true)),
 | 
			
		||||
            )
 | 
			
		||||
                StatusAndData(
 | 
			
		||||
                    success = true,
 | 
			
		||||
                    data = SelfossModel.ApiInformation(
 | 
			
		||||
                        "2.19-ba1e8e3",
 | 
			
		||||
                        "4.0.0",
 | 
			
		||||
                        SelfossModel.ApiConfiguration(false, true)
 | 
			
		||||
                    ),
 | 
			
		||||
                )
 | 
			
		||||
        coEvery { api.stats() } returns
 | 
			
		||||
            StatusAndData(
 | 
			
		||||
                success = true,
 | 
			
		||||
                data = SelfossModel.Stats(NUMBER_ARTICLES, NUMBER_UNREAD, NUMBER_STARRED),
 | 
			
		||||
            )
 | 
			
		||||
                StatusAndData(
 | 
			
		||||
                    success = true,
 | 
			
		||||
                    data = SelfossModel.Stats(NUMBER_ARTICLES, NUMBER_UNREAD, NUMBER_STARRED),
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
        every { db.itemsQueries.deleteItemsWhereSource(any()) } returns Unit
 | 
			
		||||
        every { db.itemsQueries.items().executeAsList() } returns generateTestDBItems()
 | 
			
		||||
@@ -103,7 +117,7 @@ class RepositoryTest {
 | 
			
		||||
    fun get_api_4_date_with_api_1_version_stored() {
 | 
			
		||||
        every { appSettingsService.getApiVersion() } returns 1
 | 
			
		||||
        coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
 | 
			
		||||
            StatusAndData(success = true, data = generateTestApiItem())
 | 
			
		||||
                StatusAndData(success = true, data = generateTestApiItem())
 | 
			
		||||
        every { appSettingsService.updateApiVersion(any()) } returns Unit
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
@@ -119,10 +133,14 @@ class RepositoryTest {
 | 
			
		||||
    fun get_public_access() {
 | 
			
		||||
        every { appSettingsService.updatePublicAccess(any()) } returns Unit
 | 
			
		||||
        coEvery { api.apiInformation() } returns
 | 
			
		||||
            StatusAndData(
 | 
			
		||||
                success = true,
 | 
			
		||||
                data = SelfossModel.ApiInformation("2.19-ba1e8e3", "4.0.0", SelfossModel.ApiConfiguration(true, true)),
 | 
			
		||||
            )
 | 
			
		||||
                StatusAndData(
 | 
			
		||||
                    success = true,
 | 
			
		||||
                    data = SelfossModel.ApiInformation(
 | 
			
		||||
                        "2.19-ba1e8e3",
 | 
			
		||||
                        "4.0.0",
 | 
			
		||||
                        SelfossModel.ApiConfiguration(true, true)
 | 
			
		||||
                    ),
 | 
			
		||||
                )
 | 
			
		||||
        every { appSettingsService.getUserName() } returns ""
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
@@ -135,10 +153,14 @@ class RepositoryTest {
 | 
			
		||||
    fun get_public_access_username_not_empty() {
 | 
			
		||||
        every { appSettingsService.updatePublicAccess(any()) } returns Unit
 | 
			
		||||
        coEvery { api.apiInformation() } returns
 | 
			
		||||
            StatusAndData(
 | 
			
		||||
                success = true,
 | 
			
		||||
                data = SelfossModel.ApiInformation("2.19-ba1e8e3", "4.0.0", SelfossModel.ApiConfiguration(true, true)),
 | 
			
		||||
            )
 | 
			
		||||
                StatusAndData(
 | 
			
		||||
                    success = true,
 | 
			
		||||
                    data = SelfossModel.ApiInformation(
 | 
			
		||||
                        "2.19-ba1e8e3",
 | 
			
		||||
                        "4.0.0",
 | 
			
		||||
                        SelfossModel.ApiConfiguration(true, true)
 | 
			
		||||
                    ),
 | 
			
		||||
                )
 | 
			
		||||
        every { appSettingsService.getUserName() } returns "username"
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
@@ -151,10 +173,14 @@ class RepositoryTest {
 | 
			
		||||
    fun get_public_access_no_auth() {
 | 
			
		||||
        every { appSettingsService.updatePublicAccess(any()) } returns Unit
 | 
			
		||||
        coEvery { api.apiInformation() } returns
 | 
			
		||||
            StatusAndData(
 | 
			
		||||
                success = true,
 | 
			
		||||
                data = SelfossModel.ApiInformation("2.19-ba1e8e3", "4.0.0", SelfossModel.ApiConfiguration(true, false)),
 | 
			
		||||
            )
 | 
			
		||||
                StatusAndData(
 | 
			
		||||
                    success = true,
 | 
			
		||||
                    data = SelfossModel.ApiInformation(
 | 
			
		||||
                        "2.19-ba1e8e3",
 | 
			
		||||
                        "4.0.0",
 | 
			
		||||
                        SelfossModel.ApiConfiguration(true, false)
 | 
			
		||||
                    ),
 | 
			
		||||
                )
 | 
			
		||||
        every { appSettingsService.getUserName() } returns ""
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
@@ -167,10 +193,14 @@ class RepositoryTest {
 | 
			
		||||
    fun get_public_access_disabled() {
 | 
			
		||||
        every { appSettingsService.updatePublicAccess(any()) } returns Unit
 | 
			
		||||
        coEvery { api.apiInformation() } returns
 | 
			
		||||
            StatusAndData(
 | 
			
		||||
                success = true,
 | 
			
		||||
                data = SelfossModel.ApiInformation("2.19-ba1e8e3", "4.0.0", SelfossModel.ApiConfiguration(false, true)),
 | 
			
		||||
            )
 | 
			
		||||
                StatusAndData(
 | 
			
		||||
                    success = true,
 | 
			
		||||
                    data = SelfossModel.ApiInformation(
 | 
			
		||||
                        "2.19-ba1e8e3",
 | 
			
		||||
                        "4.0.0",
 | 
			
		||||
                        SelfossModel.ApiConfiguration(false, true)
 | 
			
		||||
                    ),
 | 
			
		||||
                )
 | 
			
		||||
        every { appSettingsService.getUserName() } returns ""
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
@@ -186,10 +216,10 @@ class RepositoryTest {
 | 
			
		||||
        val itemParameters = FakeItemParameters()
 | 
			
		||||
        itemParameters.datetime = "2021-04-23 11:45:32"
 | 
			
		||||
        coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
 | 
			
		||||
            StatusAndData(
 | 
			
		||||
                success = true,
 | 
			
		||||
                data = generateTestApiItem(itemParameters),
 | 
			
		||||
            )
 | 
			
		||||
                StatusAndData(
 | 
			
		||||
                    success = true,
 | 
			
		||||
                    data = generateTestApiItem(itemParameters),
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        runBlocking {
 | 
			
		||||
@@ -202,7 +232,7 @@ class RepositoryTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    fun get_newer_items() {
 | 
			
		||||
        coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
 | 
			
		||||
            StatusAndData(success = true, data = generateTestApiItem())
 | 
			
		||||
                StatusAndData(success = true, data = generateTestApiItem())
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        runBlocking {
 | 
			
		||||
@@ -217,7 +247,7 @@ class RepositoryTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    fun get_all_newer_items() {
 | 
			
		||||
        coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
 | 
			
		||||
            StatusAndData(success = true, data = generateTestApiItem())
 | 
			
		||||
                StatusAndData(success = true, data = generateTestApiItem())
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        repository.displayedItems = ItemType.ALL
 | 
			
		||||
@@ -233,7 +263,7 @@ class RepositoryTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    fun get_newer_starred_items() {
 | 
			
		||||
        coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
 | 
			
		||||
            StatusAndData(success = true, data = generateTestApiItem())
 | 
			
		||||
                StatusAndData(success = true, data = generateTestApiItem())
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        repository.displayedItems = ItemType.STARRED
 | 
			
		||||
@@ -272,8 +302,8 @@ class RepositoryTest {
 | 
			
		||||
        coEvery { db.itemsQueries.items().executeAsList() } returns generateTestDBItems(
 | 
			
		||||
            itemParameter1,
 | 
			
		||||
        ) +
 | 
			
		||||
            generateTestDBItems(itemParameter2) +
 | 
			
		||||
            generateTestDBItems(itemParameter3)
 | 
			
		||||
                generateTestDBItems(itemParameter2) +
 | 
			
		||||
                generateTestDBItems(itemParameter3)
 | 
			
		||||
 | 
			
		||||
        every { appSettingsService.isItemCachingEnabled() } returns true
 | 
			
		||||
 | 
			
		||||
@@ -300,8 +330,8 @@ class RepositoryTest {
 | 
			
		||||
        coEvery { db.itemsQueries.items().executeAsList() } returns generateTestDBItems(
 | 
			
		||||
            itemParameter1,
 | 
			
		||||
        ) +
 | 
			
		||||
            generateTestDBItems(itemParameter2) +
 | 
			
		||||
            generateTestDBItems(itemParameter3)
 | 
			
		||||
                generateTestDBItems(itemParameter2) +
 | 
			
		||||
                generateTestDBItems(itemParameter3)
 | 
			
		||||
 | 
			
		||||
        every { appSettingsService.isItemCachingEnabled() } returns true
 | 
			
		||||
 | 
			
		||||
@@ -330,7 +360,7 @@ class RepositoryTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    fun get_older_items() {
 | 
			
		||||
        coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
 | 
			
		||||
            StatusAndData(success = true, data = generateTestApiItem())
 | 
			
		||||
                StatusAndData(success = true, data = generateTestApiItem())
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        repository.items = ArrayList(generateTestApiItem())
 | 
			
		||||
@@ -346,7 +376,7 @@ class RepositoryTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    fun get_all_older_items() {
 | 
			
		||||
        coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
 | 
			
		||||
            StatusAndData(success = true, data = generateTestApiItem())
 | 
			
		||||
                StatusAndData(success = true, data = generateTestApiItem())
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        repository.items = ArrayList(generateTestApiItem())
 | 
			
		||||
@@ -363,7 +393,7 @@ class RepositoryTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    fun get_older_starred_items() {
 | 
			
		||||
        coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
 | 
			
		||||
            StatusAndData(success = true, data = generateTestApiItem())
 | 
			
		||||
                StatusAndData(success = true, data = generateTestApiItem())
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        repository.displayedItems = ItemType.STARRED
 | 
			
		||||
@@ -803,7 +833,7 @@ class RepositoryTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    fun create_source() {
 | 
			
		||||
        coEvery { api.createSourceForVersion(any(), any(), any(), any()) } returns
 | 
			
		||||
            SuccessResponse(true)
 | 
			
		||||
                SuccessResponse(true)
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        var response: Boolean
 | 
			
		||||
@@ -831,7 +861,7 @@ class RepositoryTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    fun create_source_but_response_fails() {
 | 
			
		||||
        coEvery { api.createSourceForVersion(any(), any(), any(), any()) } returns
 | 
			
		||||
            SuccessResponse(false)
 | 
			
		||||
                SuccessResponse(false)
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        var response: Boolean
 | 
			
		||||
@@ -859,7 +889,7 @@ class RepositoryTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    fun create_source_without_connection() {
 | 
			
		||||
        coEvery { api.createSourceForVersion(any(), any(), any(), any()) } returns
 | 
			
		||||
            SuccessResponse(true)
 | 
			
		||||
                SuccessResponse(true)
 | 
			
		||||
 | 
			
		||||
        initializeRepository(MutableStateFlow(false))
 | 
			
		||||
        var response: Boolean
 | 
			
		||||
@@ -932,10 +962,10 @@ class RepositoryTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    fun update_remote() {
 | 
			
		||||
        coEvery { api.update() } returns
 | 
			
		||||
            StatusAndData(
 | 
			
		||||
                success = true,
 | 
			
		||||
                data = "finished",
 | 
			
		||||
            )
 | 
			
		||||
                StatusAndData(
 | 
			
		||||
                    success = true,
 | 
			
		||||
                    data = "finished",
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        var response: Boolean
 | 
			
		||||
@@ -950,10 +980,10 @@ class RepositoryTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    fun update_remote_but_response_fails() {
 | 
			
		||||
        coEvery { api.update() } returns
 | 
			
		||||
            StatusAndData(
 | 
			
		||||
                success = false,
 | 
			
		||||
                data = "unallowed access",
 | 
			
		||||
            )
 | 
			
		||||
                StatusAndData(
 | 
			
		||||
                    success = false,
 | 
			
		||||
                    data = "unallowed access",
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        var response: Boolean
 | 
			
		||||
@@ -968,10 +998,10 @@ class RepositoryTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    fun update_remote_with_unallowed_access() {
 | 
			
		||||
        coEvery { api.update() } returns
 | 
			
		||||
            StatusAndData(
 | 
			
		||||
                success = true,
 | 
			
		||||
                data = "unallowed access",
 | 
			
		||||
            )
 | 
			
		||||
                StatusAndData(
 | 
			
		||||
                    success = true,
 | 
			
		||||
                    data = "unallowed access",
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        var response: Boolean
 | 
			
		||||
@@ -986,10 +1016,10 @@ class RepositoryTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    fun update_remote_without_connection() {
 | 
			
		||||
        coEvery { api.update() } returns
 | 
			
		||||
            StatusAndData(
 | 
			
		||||
                success = true,
 | 
			
		||||
                data = "undocumented...",
 | 
			
		||||
            )
 | 
			
		||||
                StatusAndData(
 | 
			
		||||
                    success = true,
 | 
			
		||||
                    data = "undocumented...",
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
        initializeRepository(MutableStateFlow(false))
 | 
			
		||||
        var response: Boolean
 | 
			
		||||
@@ -1079,11 +1109,11 @@ class RepositoryTest {
 | 
			
		||||
                any(),
 | 
			
		||||
            )
 | 
			
		||||
        } returnsMany
 | 
			
		||||
            listOf(
 | 
			
		||||
                StatusAndData(success = true, data = generateTestApiItem(itemParameter1)),
 | 
			
		||||
                StatusAndData(success = true, data = generateTestApiItem(itemParameter2)),
 | 
			
		||||
                StatusAndData(success = true, data = generateTestApiItem(itemParameter1)),
 | 
			
		||||
            )
 | 
			
		||||
                listOf(
 | 
			
		||||
                    StatusAndData(success = true, data = generateTestApiItem(itemParameter1)),
 | 
			
		||||
                    StatusAndData(success = true, data = generateTestApiItem(itemParameter2)),
 | 
			
		||||
                    StatusAndData(success = true, data = generateTestApiItem(itemParameter1)),
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        prepareSearch()
 | 
			
		||||
@@ -1097,7 +1127,7 @@ class RepositoryTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    fun cache_items_but_response_fails() {
 | 
			
		||||
        coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
 | 
			
		||||
            StatusAndData(success = false, data = generateTestApiItem())
 | 
			
		||||
                StatusAndData(success = false, data = generateTestApiItem())
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        prepareSearch()
 | 
			
		||||
@@ -1111,7 +1141,7 @@ class RepositoryTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    fun cache_items_without_connection() {
 | 
			
		||||
        coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
 | 
			
		||||
            StatusAndData(success = false, data = generateTestApiItem())
 | 
			
		||||
                StatusAndData(success = false, data = generateTestApiItem())
 | 
			
		||||
 | 
			
		||||
        initializeRepository(MutableStateFlow(false))
 | 
			
		||||
        prepareSearch()
 | 
			
		||||
@@ -1138,4 +1168,4 @@ class RepositoryTest {
 | 
			
		||||
        )
 | 
			
		||||
        repository.searchFilter = "search"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
package bou.amine.apps.readerforselfossv2.repository
 | 
			
		||||
package bou.amine.apps.readerforselfossv2.tests.repository
 | 
			
		||||
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.dao.ITEM
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
 | 
			
		||||
@@ -56,4 +56,4 @@ class FakeItemParameters {
 | 
			
		||||
    var sourcetitle = "La Chimica e la Società"
 | 
			
		||||
    var tags = "Chimica, Testing"
 | 
			
		||||
    var author = "Someone important"
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@@ -7,12 +7,12 @@ buildscript {
 | 
			
		||||
 | 
			
		||||
plugins {
 | 
			
		||||
    //trick: for the same plugin versions in all sub-modules
 | 
			
		||||
    id("com.android.application").version("8.1.2").apply(false)
 | 
			
		||||
    id("com.android.library").version("8.1.2").apply(false)
 | 
			
		||||
    id("org.jetbrains.kotlin.android").version("1.9.10").apply(false)
 | 
			
		||||
    kotlin("multiplatform").version("1.9.10").apply(false)
 | 
			
		||||
    id("com.android.application").version("8.7.3").apply(false)
 | 
			
		||||
    id("com.android.library").version("8.7.3").apply(false)
 | 
			
		||||
    id("org.jetbrains.kotlin.android").version("2.1.0").apply(false)
 | 
			
		||||
    kotlin("multiplatform").version("2.1.0").apply(false)
 | 
			
		||||
    id("com.mikepenz.aboutlibraries.plugin").version("10.5.1").apply(false)
 | 
			
		||||
    id("org.jetbrains.kotlinx.kover").version("0.6.1").apply(true)
 | 
			
		||||
    id("org.jetbrains.kotlinx.kover") version "0.9.0" apply true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
allprojects {
 | 
			
		||||
@@ -25,9 +25,10 @@ allprojects {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
tasks.register("clean", Delete::class) {
 | 
			
		||||
    delete(rootProject.buildDir)
 | 
			
		||||
    delete(layout.buildDirectory)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
koverMerged {
 | 
			
		||||
    enable()
 | 
			
		||||
dependencies {
 | 
			
		||||
    kover(project(":shared"))
 | 
			
		||||
    kover(project(":androidApp"))
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										0
									
								
								fastlane/metadata/android/en-US/changelogs/.gitkeep
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								fastlane/metadata/android/en-US/changelogs/.gitkeep
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
**v124113311**
 | 
			
		||||
 | 
			
		||||
- chore: update versions. (#165)
 | 
			
		||||
- chore: fastlane changelog.
 | 
			
		||||
- chore: fastlane fixes.
 | 
			
		||||
- Changelog for v124113301
 | 
			
		||||
@@ -0,0 +1,4 @@
 | 
			
		||||
**v124123421**
 | 
			
		||||
 | 
			
		||||
- fix: Trying to fix the serialization issue.
 | 
			
		||||
- Changelog for v124113311
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								fastlane/metadata/android/en-US/images/icon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								fastlane/metadata/android/en-US/images/icon.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 294 KiB  | 
@@ -1 +1 @@
 | 
			
		||||
A new RSS reader for <a href="http://selfoss.aditu.de/">selfoss</a>.
 | 
			
		||||
A new RSS reader for selfoss (http://selfoss.aditu.de/)
 | 
			
		||||
 
 | 
			
		||||
@@ -26,3 +26,4 @@ org.gradle.parallel=true
 | 
			
		||||
org.gradle.caching=true
 | 
			
		||||
ignoreGitVersion=false
 | 
			
		||||
kotlin.native.cacheKind.iosX64=none
 | 
			
		||||
org.gradle.configureondemand=true
 | 
			
		||||
							
								
								
									
										4
									
								
								gradle/wrapper/gradle-wrapper.properties
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								gradle/wrapper/gradle-wrapper.properties
									
									
									
									
										vendored
									
									
								
							@@ -1,6 +1,6 @@
 | 
			
		||||
#Thu Jul 13 11:41:19 CEST 2023
 | 
			
		||||
#Mon Nov 25 22:48:24 CET 2024
 | 
			
		||||
distributionBase=GRADLE_USER_HOME
 | 
			
		||||
distributionPath=wrapper/dists
 | 
			
		||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip
 | 
			
		||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
 | 
			
		||||
zipStoreBase=GRADLE_USER_HOME
 | 
			
		||||
zipStorePath=wrapper/dists
 | 
			
		||||
 
 | 
			
		||||
@@ -173,7 +173,7 @@
 | 
			
		||||
			);
 | 
			
		||||
			runOnlyForDeploymentPostprocessing = 0;
 | 
			
		||||
			shellPath = /bin/sh;
 | 
			
		||||
			shellScript = "cd \"$SRCROOT/..\"\n./gradlew :shared:embedAndSignAppleFrameworkForXcode\n";
 | 
			
		||||
			shellScript = "export JAVA_HOME=/Users/amine/.sdkman/candidates/java/17.0.8.1-jbr\ncd \"$SRCROOT/..\"\n./gradlew :shared:embedAndSignAppleFrameworkForXcode --stacktrace\n";
 | 
			
		||||
		};
 | 
			
		||||
/* End PBXShellScriptBuildPhase section */
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -52,6 +52,9 @@ kotlin {
 | 
			
		||||
 | 
			
		||||
                // Sql
 | 
			
		||||
                implementation(SqlDelight.runtime)
 | 
			
		||||
 | 
			
		||||
                // Sql
 | 
			
		||||
                implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.1")
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        val commonTest by getting {
 | 
			
		||||
@@ -80,10 +83,6 @@ kotlin {
 | 
			
		||||
        val iosArm64Main by getting
 | 
			
		||||
        // val iosSimulatorArm64Main by getting
 | 
			
		||||
        val iosMain by creating {
 | 
			
		||||
            dependsOn(commonMain)
 | 
			
		||||
            iosX64Main.dependsOn(this)
 | 
			
		||||
            iosArm64Main.dependsOn(this)
 | 
			
		||||
            // iosSimulatorArm64Main.dependsOn(this)
 | 
			
		||||
 | 
			
		||||
            dependencies {
 | 
			
		||||
                implementation(SqlDelight.native)
 | 
			
		||||
@@ -94,10 +93,6 @@ kotlin {
 | 
			
		||||
        val iosArm64Test by getting
 | 
			
		||||
        // val iosSimulatorArm64Test by getting
 | 
			
		||||
        val iosTest by creating {
 | 
			
		||||
            dependsOn(commonTest)
 | 
			
		||||
            iosX64Test.dependsOn(this)
 | 
			
		||||
            iosArm64Test.dependsOn(this)
 | 
			
		||||
            // iosSimulatorArm64Test.dependsOn(this)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,34 +1,13 @@
 | 
			
		||||
package bou.amine.apps.readerforselfossv2.utils
 | 
			
		||||
 | 
			
		||||
import android.text.format.DateUtils
 | 
			
		||||
import io.github.aakira.napier.Napier
 | 
			
		||||
import kotlinx.datetime.*
 | 
			
		||||
 | 
			
		||||
actual class DateUtils {
 | 
			
		||||
    actual companion object {
 | 
			
		||||
        // Possible formats are
 | 
			
		||||
        // yyyy-mm-dd hh:mm:ss format
 | 
			
		||||
        private val oldVersionFormat = "\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}(.()\\d*)?".toRegex()
 | 
			
		||||
 | 
			
		||||
        // yyyy-MM-dd'T'HH:mm:ss[.SSS]XXX (RFC3339)
 | 
			
		||||
        private val newVersionFormat = "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\+\\d{2}(:\\d{2})?".toRegex()
 | 
			
		||||
 | 
			
		||||
        // We may need to consider moving the formatting to platform specific code, even if the tests are doubled
 | 
			
		||||
        // For now, we handle this in a hacky way, because kotlin only accepts iso formats
 | 
			
		||||
        actual fun parseDate(dateString: String): Long {
 | 
			
		||||
            var isoDateString: String =
 | 
			
		||||
                if (dateString.matches(oldVersionFormat)) {
 | 
			
		||||
                    dateString.replace(" ", "T")
 | 
			
		||||
                } else if (dateString.matches(newVersionFormat)) {
 | 
			
		||||
                    dateString.split("+")[0]
 | 
			
		||||
                } else {
 | 
			
		||||
                    throw Exception("Unrecognized format for $dateString")
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            return LocalDateTime.parse(isoDateString).toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        actual fun parseRelativeDate(dateString: String): String {
 | 
			
		||||
            val date = parseDate(dateString)
 | 
			
		||||
            val date = dateString.toParsedDate()
 | 
			
		||||
 | 
			
		||||
            return " " +
 | 
			
		||||
                DateUtils.getRelativeTimeSpanString(
 | 
			
		||||
 
 | 
			
		||||
@@ -146,6 +146,14 @@ class SelfossModel {
 | 
			
		||||
            return txt
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        fun sourceAuthorOnly(): String {
 | 
			
		||||
            var txt = this.sourcetitle.getHtmlDecoded()
 | 
			
		||||
            if (!this.author.isNullOrBlank()) {
 | 
			
		||||
                txt += " (by ${this.author}) "
 | 
			
		||||
            }
 | 
			
		||||
            return txt
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        fun toggleStar(): Item {
 | 
			
		||||
            this.starred = !this.starred
 | 
			
		||||
            return this
 | 
			
		||||
@@ -183,7 +191,7 @@ class SelfossModel {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        override val descriptor: SerialDescriptor
 | 
			
		||||
            get() = PrimitiveSerialDescriptor("b", PrimitiveKind.BOOLEAN)
 | 
			
		||||
            get() = PrimitiveSerialDescriptor("BooleanOrIntForSomeSelfossVersions", PrimitiveKind.BOOLEAN)
 | 
			
		||||
 | 
			
		||||
        override fun serialize(
 | 
			
		||||
            encoder: Encoder,
 | 
			
		||||
 
 | 
			
		||||
@@ -74,7 +74,7 @@ class Repository(
 | 
			
		||||
                dbItems = dbItems.filter { it.sourcetitle == sourceFilter.value!!.title }
 | 
			
		||||
            }
 | 
			
		||||
            val itemsList = ArrayList(dbItems.map { it.toView() })
 | 
			
		||||
            itemsList.sortByDescending { DateUtils.parseDate(it.datetime) }
 | 
			
		||||
            itemsList.sortByDescending { it.datetime.toParsedDate() }
 | 
			
		||||
            fetchedItems =
 | 
			
		||||
                StatusAndData.succes(
 | 
			
		||||
                    itemsList,
 | 
			
		||||
@@ -422,7 +422,7 @@ class Repository(
 | 
			
		||||
                val response = api.login()
 | 
			
		||||
                result = response.isSuccess == true
 | 
			
		||||
            } catch (cause: Throwable) {
 | 
			
		||||
                Napier.e(cause.stackTraceToString(), tag = "RepositoryImpl.login")
 | 
			
		||||
                Napier.e("login failed", cause, tag = "RepositoryImpl.login")
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return result
 | 
			
		||||
@@ -436,7 +436,7 @@ class Repository(
 | 
			
		||||
                // a random rss feed, that would throw a NoTransformationFoundException
 | 
			
		||||
                fetchFailed = !api.getItemsWithoutCatch().success
 | 
			
		||||
            } catch (e: Throwable) {
 | 
			
		||||
                Napier.e(e.stackTraceToString(), tag = "RepositoryImpl.shouldBeSelfossInstance")
 | 
			
		||||
                Napier.e("checkIfFetchFails failed", e, tag = "RepositoryImpl.shouldBeSelfossInstance")
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -451,7 +451,7 @@ class Repository(
 | 
			
		||||
                    Napier.e("Couldn't logout.", tag = "RepositoryImpl.logout")
 | 
			
		||||
                }
 | 
			
		||||
            } catch (cause: Throwable) {
 | 
			
		||||
                Napier.e(cause.stackTraceToString(), tag = "RepositoryImpl.logout")
 | 
			
		||||
                Napier.e("logout failed", cause, tag = "RepositoryImpl.logout")
 | 
			
		||||
            }
 | 
			
		||||
            appSettingsService.clearAll()
 | 
			
		||||
        } else {
 | 
			
		||||
 
 | 
			
		||||
@@ -6,9 +6,16 @@ import com.russhwolf.settings.Settings
 | 
			
		||||
// This is to fix ACRA not sending reports anymore.
 | 
			
		||||
// See https://www.acra.ch/docs/Troubleshooting-Guide#applicationoncreate
 | 
			
		||||
class ACRASettings : Settings {
 | 
			
		||||
    override val keys: Set<String> = emptySet()
 | 
			
		||||
    override val keys: MutableSet<String> = mutableSetOf()
 | 
			
		||||
    override val size: Int = 0
 | 
			
		||||
 | 
			
		||||
    val bools: MutableMap<String, Boolean> = mutableMapOf()
 | 
			
		||||
    val doubles: MutableMap<String, Double> = mutableMapOf()
 | 
			
		||||
    val floats: MutableMap<String, Float> = mutableMapOf()
 | 
			
		||||
    val ints: MutableMap<String, Int> = mutableMapOf()
 | 
			
		||||
    val longs: MutableMap<String, Long> = mutableMapOf()
 | 
			
		||||
    val strings: MutableMap<String, String> = mutableMapOf()
 | 
			
		||||
 | 
			
		||||
    override fun clear() {
 | 
			
		||||
        // Nothing
 | 
			
		||||
    }
 | 
			
		||||
@@ -16,90 +23,102 @@ class ACRASettings : Settings {
 | 
			
		||||
    override fun getBoolean(
 | 
			
		||||
        key: String,
 | 
			
		||||
        defaultValue: Boolean,
 | 
			
		||||
    ): Boolean = false
 | 
			
		||||
    ): Boolean = bools[key] ?: defaultValue
 | 
			
		||||
 | 
			
		||||
    override fun getBooleanOrNull(key: String): Boolean? = null
 | 
			
		||||
    override fun getBooleanOrNull(key: String): Boolean? = bools[key]
 | 
			
		||||
 | 
			
		||||
    override fun getDouble(
 | 
			
		||||
        key: String,
 | 
			
		||||
        defaultValue: Double,
 | 
			
		||||
    ): Double = 0.0
 | 
			
		||||
    ): Double = doubles[key] ?: defaultValue
 | 
			
		||||
 | 
			
		||||
    override fun getDoubleOrNull(key: String): Double? = null
 | 
			
		||||
    override fun getDoubleOrNull(key: String): Double? = doubles[key]
 | 
			
		||||
 | 
			
		||||
    override fun getFloat(
 | 
			
		||||
        key: String,
 | 
			
		||||
        defaultValue: Float,
 | 
			
		||||
    ): Float = 0.0F
 | 
			
		||||
    ): Float = floats[key] ?: defaultValue
 | 
			
		||||
 | 
			
		||||
    override fun getFloatOrNull(key: String): Float? = null
 | 
			
		||||
    override fun getFloatOrNull(key: String): Float? = floats[key]
 | 
			
		||||
 | 
			
		||||
    override fun getInt(
 | 
			
		||||
        key: String,
 | 
			
		||||
        defaultValue: Int,
 | 
			
		||||
    ): Int = 0
 | 
			
		||||
    ): Int = ints[key] ?: defaultValue
 | 
			
		||||
 | 
			
		||||
    override fun getIntOrNull(key: String): Int? = null
 | 
			
		||||
    override fun getIntOrNull(key: String): Int? = ints[key]
 | 
			
		||||
 | 
			
		||||
    override fun getLong(
 | 
			
		||||
        key: String,
 | 
			
		||||
        defaultValue: Long,
 | 
			
		||||
    ): Long = 0
 | 
			
		||||
    ): Long = longs[key] ?: defaultValue
 | 
			
		||||
 | 
			
		||||
    override fun getLongOrNull(key: String): Long? = null
 | 
			
		||||
    override fun getLongOrNull(key: String): Long? = longs[key]
 | 
			
		||||
 | 
			
		||||
    override fun getString(
 | 
			
		||||
        key: String,
 | 
			
		||||
        defaultValue: String,
 | 
			
		||||
    ): String = "0"
 | 
			
		||||
    ): String = strings[key] ?: defaultValue
 | 
			
		||||
 | 
			
		||||
    override fun getStringOrNull(key: String): String? = null
 | 
			
		||||
    override fun getStringOrNull(key: String): String? = strings[key]
 | 
			
		||||
 | 
			
		||||
    override fun hasKey(key: String): Boolean = false
 | 
			
		||||
    override fun hasKey(key: String): Boolean = keys.contains(key)
 | 
			
		||||
 | 
			
		||||
    override fun putBoolean(
 | 
			
		||||
        key: String,
 | 
			
		||||
        value: Boolean,
 | 
			
		||||
    ) {
 | 
			
		||||
        // Nothing
 | 
			
		||||
        keys.add(key)
 | 
			
		||||
        bools[key] = value
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun putDouble(
 | 
			
		||||
        key: String,
 | 
			
		||||
        value: Double,
 | 
			
		||||
    ) {
 | 
			
		||||
        // Nothing
 | 
			
		||||
        keys.add(key)
 | 
			
		||||
        doubles[key] = value
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun putFloat(
 | 
			
		||||
        key: String,
 | 
			
		||||
        value: Float,
 | 
			
		||||
    ) {
 | 
			
		||||
        // Nothing
 | 
			
		||||
        keys.add(key)
 | 
			
		||||
        floats[key] = value
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun putInt(
 | 
			
		||||
        key: String,
 | 
			
		||||
        value: Int,
 | 
			
		||||
    ) {
 | 
			
		||||
        // Nothing
 | 
			
		||||
        keys.add(key)
 | 
			
		||||
        ints[key] = value
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun putLong(
 | 
			
		||||
        key: String,
 | 
			
		||||
        value: Long,
 | 
			
		||||
    ) {
 | 
			
		||||
        // Nothing
 | 
			
		||||
        keys.add(key)
 | 
			
		||||
        longs[key] = value
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun putString(
 | 
			
		||||
        key: String,
 | 
			
		||||
        value: String,
 | 
			
		||||
    ) {
 | 
			
		||||
        // Nothing
 | 
			
		||||
        keys.add(key)
 | 
			
		||||
        strings[key] = value
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun remove(key: String) {
 | 
			
		||||
        // Nothing
 | 
			
		||||
        keys.remove(key)
 | 
			
		||||
        bools.remove(key)
 | 
			
		||||
        doubles.remove(key)
 | 
			
		||||
        floats.remove(key)
 | 
			
		||||
        ints.remove(key)
 | 
			
		||||
        longs.remove(key)
 | 
			
		||||
        strings.remove(key)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@@ -441,7 +441,7 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
 | 
			
		||||
        login: String,
 | 
			
		||||
        password: String,
 | 
			
		||||
    ) {
 | 
			
		||||
        val regex = """\/\/(\D+):(\D+)@""".toRegex()
 | 
			
		||||
        val regex = """\/\/(\S+):(\S+)@""".toRegex()
 | 
			
		||||
        val matchResult = regex.find(url)
 | 
			
		||||
        if (matchResult != null) {
 | 
			
		||||
            val (basicLogin, basicPassword) = matchResult.destructured
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,35 @@
 | 
			
		||||
package bou.amine.apps.readerforselfossv2.utils
 | 
			
		||||
 | 
			
		||||
import kotlinx.datetime.LocalDateTime
 | 
			
		||||
import kotlinx.datetime.TimeZone
 | 
			
		||||
import kotlinx.datetime.toInstant
 | 
			
		||||
 | 
			
		||||
fun String.toParsedDate(): Long {
 | 
			
		||||
    // Possible formats are
 | 
			
		||||
    // yyyy-mm-dd hh:mm:ss format
 | 
			
		||||
    val oldVersionFormat = "\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}(.()\\d*)?".toRegex()
 | 
			
		||||
 | 
			
		||||
    // yyyy-MM-dd'T'HH:mm:ss[.SSS]XXX (RFC3339)
 | 
			
		||||
    val newVersionFormat = "(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2})[+-](\\d{2}(:\\d{2})?)?".toRegex()
 | 
			
		||||
 | 
			
		||||
    val isoDateString: String =
 | 
			
		||||
        try {
 | 
			
		||||
            if (this.matches(oldVersionFormat)) {
 | 
			
		||||
                this.replace(" ", "T")
 | 
			
		||||
            } else if (this.matches(newVersionFormat)) {
 | 
			
		||||
                newVersionFormat.find(this)?.groups?.get(1)?.value ?: throw Exception("Couldn't parse $this")
 | 
			
		||||
            } else {
 | 
			
		||||
                throw Exception("Unrecognized format for $this")
 | 
			
		||||
            }
 | 
			
		||||
        } catch (e: Exception) {
 | 
			
		||||
            throw Exception("parseDate failed for $this", e)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    return LocalDateTime.parse(isoDateString).toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
expect class DateUtils() {
 | 
			
		||||
    companion object {
 | 
			
		||||
        fun parseDate(dateString: String): Long
 | 
			
		||||
 | 
			
		||||
        fun parseRelativeDate(dateString: String): String
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,21 +1,32 @@
 | 
			
		||||
package bou.amine.apps.readerforselfossv2.repository
 | 
			
		||||
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.utils.DateUtils
 | 
			
		||||
import junit.framework.TestCase.assertEquals
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.utils.toParsedDate
 | 
			
		||||
import kotlinx.datetime.LocalDateTime
 | 
			
		||||
import kotlinx.datetime.TimeZone
 | 
			
		||||
import kotlinx.datetime.toInstant
 | 
			
		||||
import org.junit.Test
 | 
			
		||||
import kotlin.test.Test
 | 
			
		||||
import kotlin.test.assertEquals
 | 
			
		||||
 | 
			
		||||
class DatesTest {
 | 
			
		||||
    private val newVersionDateVariant = "2022-12-24T17:00:08+00"
 | 
			
		||||
    private val newVersionDate = "2013-04-07T13:43:00+01:00"
 | 
			
		||||
    private val oldVersionDate = "2013-05-07 13:46:00"
 | 
			
		||||
    private val oldVersionDateVariant = "2021-03-21 10:32:00.000000"
 | 
			
		||||
    private val newVersionDateVariant =     "2022-12-24T17:00:08+00"
 | 
			
		||||
    private val newVersionDate =            "2013-04-07T13:43:00+01:00"
 | 
			
		||||
    private val newVersionDate2 =            "2013-04-07T13:43:00-01:00"
 | 
			
		||||
    private val oldVersionDate =            "2013-05-07 13:46:00"
 | 
			
		||||
    private val oldVersionDateVariant =     "2021-03-21 10:32:00.000000"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun new_version_date_should_be_parsed() {
 | 
			
		||||
        val date = DateUtils.parseDate(newVersionDate)
 | 
			
		||||
        val date = newVersionDate.toParsedDate()
 | 
			
		||||
        val expected =
 | 
			
		||||
            LocalDateTime(2013, 4, 7, 13, 43, 0, 0).toInstant(TimeZone.currentSystemDefault())
 | 
			
		||||
                .toEpochMilliseconds()
 | 
			
		||||
 | 
			
		||||
        assertEquals(expected, date)
 | 
			
		||||
    }
 | 
			
		||||
    @Test
 | 
			
		||||
    fun new_version_date2_should_be_parsed() {
 | 
			
		||||
        val date = newVersionDate2.toParsedDate()
 | 
			
		||||
        val expected =
 | 
			
		||||
            LocalDateTime(2013, 4, 7, 13, 43, 0, 0).toInstant(TimeZone.currentSystemDefault())
 | 
			
		||||
                .toEpochMilliseconds()
 | 
			
		||||
@@ -25,7 +36,7 @@ class DatesTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun old_version_date_should_be_parsed() {
 | 
			
		||||
        val date = DateUtils.parseDate(oldVersionDate)
 | 
			
		||||
        val date = oldVersionDate.toParsedDate()
 | 
			
		||||
        val expected =
 | 
			
		||||
            LocalDateTime(2013, 5, 7, 13, 46, 0, 0).toInstant(TimeZone.currentSystemDefault())
 | 
			
		||||
                .toEpochMilliseconds()
 | 
			
		||||
@@ -35,7 +46,7 @@ class DatesTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun old_version_variant_date_should_be_parsed() {
 | 
			
		||||
        val date = DateUtils.parseDate(oldVersionDateVariant)
 | 
			
		||||
        val date = oldVersionDateVariant.toParsedDate()
 | 
			
		||||
        val expected =
 | 
			
		||||
            LocalDateTime(2021, 3, 21, 10, 32, 0, 0).toInstant(TimeZone.currentSystemDefault())
 | 
			
		||||
                .toEpochMilliseconds()
 | 
			
		||||
@@ -45,7 +56,7 @@ class DatesTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun new_version_variant_date_should_be_parsed() {
 | 
			
		||||
        val date = DateUtils.parseDate(newVersionDateVariant)
 | 
			
		||||
        val date = newVersionDateVariant.toParsedDate()
 | 
			
		||||
        val expected =
 | 
			
		||||
            LocalDateTime(2022, 12, 24, 17, 0, 8, 0).toInstant(TimeZone.currentSystemDefault())
 | 
			
		||||
                .toEpochMilliseconds()
 | 
			
		||||
@@ -0,0 +1,9 @@
 | 
			
		||||
package bou.amine.apps.readerforselfossv2.utils
 | 
			
		||||
 | 
			
		||||
actual class DateUtils actual constructor() {
 | 
			
		||||
    actual companion object {
 | 
			
		||||
        actual fun parseRelativeDate(dateString: String): String {
 | 
			
		||||
            TODO("Not yet implemented")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,5 +0,0 @@
 | 
			
		||||
sonar.projectKey=RFS2
 | 
			
		||||
sonar.coverage.jacoco.xmlReportPaths=build/reports/kover/merged/xml/report.xml
 | 
			
		||||
sonar.sourceEncoding=UTF-8
 | 
			
		||||
sonar.sources=.
 | 
			
		||||
sonar.exclusions=shared/src/iosArm64Main/**, shared/src/iosX64Main/**, docs/** 
 | 
			
		||||
		Reference in New Issue
	
	Block a user