mirror of
				https://github.com/commons-app/apps-android-commons.git
				synced 2025-10-31 14:53:59 +01:00 
			
		
		
		
	Compare commits
	
		
			8 commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | e27f5d4458 | ||
|   | 50b3d2c5fb | ||
|   | 05895c5d9e | ||
|   | 9654a8ea34 | ||
|   | f4414b1c31 | ||
|   | 4edabede88 | ||
|   | 9880d7ea7e | ||
|   | 8b75c41143 | 
					 1671 changed files with 93494 additions and 96634 deletions
				
			
		
							
								
								
									
										4
									
								
								.github/ISSUE_TEMPLATE/bug-report.yml
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/ISSUE_TEMPLATE/bug-report.yml
									
										
									
									
										vendored
									
									
								
							|  | @ -1,7 +1,7 @@ | ||||||
| name: "\U0001F41E Bug report" | name: "\U0001F41E Bug report" | ||||||
| description: Create a report to help us improve. | description: Create a report to help us improve. | ||||||
| title: "[Bug]: " | title: "[Bug]: " | ||||||
| type: Bug  # Retained to categorize the issue as per organization-level type | labels: ["bug"] | ||||||
| body: | body: | ||||||
|   - type: markdown |   - type: markdown | ||||||
|     attributes: |     attributes: | ||||||
|  | @ -70,7 +70,7 @@ body: | ||||||
|       required: false |       required: false | ||||||
|   - type: textarea |   - type: textarea | ||||||
|     attributes: |     attributes: | ||||||
|       label: Screenshots |       label: Screen-shots | ||||||
|       description: Add screenshots related to the issue (if available). Can be created by pressing the Volume Down and Power Button at the same time on Android 4.0 and higher. |       description: Add screenshots related to the issue (if available). Can be created by pressing the Volume Down and Power Button at the same time on Android 4.0 and higher. | ||||||
|     validations: |     validations: | ||||||
|       required: false |       required: false | ||||||
|  |  | ||||||
							
								
								
									
										28
									
								
								.github/workflows/android.yml
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										28
									
								
								.github/workflows/android.yml
									
										
									
									
										vendored
									
									
								
							|  | @ -1,6 +1,6 @@ | ||||||
| name: Android CI | name: Android CI | ||||||
| 
 | 
 | ||||||
| on: [push, pull_request, workflow_dispatch] | on: [push, pull_request] | ||||||
| 
 | 
 | ||||||
| concurrency: | concurrency: | ||||||
|   group: build-${{ github.event.pull_request.number || github.ref }} |   group: build-${{ github.event.pull_request.number || github.ref }} | ||||||
|  | @ -12,17 +12,17 @@ jobs: | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - name: Checkout code |       - name: Checkout code | ||||||
|         uses: actions/checkout@v4 |         uses: actions/checkout@v2.4.0 | ||||||
| 
 | 
 | ||||||
|       - name: Set up JDK |       - name: Set up JDK | ||||||
|         uses: actions/setup-java@v4 |         uses: actions/setup-java@v2.5.0 | ||||||
|         with: |         with: | ||||||
|           distribution: 'temurin' |           distribution: "temurin" | ||||||
|           java-version: '17' |           java-version: 11 | ||||||
| 
 | 
 | ||||||
|       - name: Cache packages |       - name: Cache packages | ||||||
|         id: cache-packages |         id: cache-packages | ||||||
|         uses: actions/cache@v4 |         uses: actions/cache@v2.1.7 | ||||||
|         with: |         with: | ||||||
|           path: | |           path: | | ||||||
|             ~/.gradle/caches |             ~/.gradle/caches | ||||||
|  | @ -37,7 +37,7 @@ jobs: | ||||||
| 
 | 
 | ||||||
|       - name: AVD cache |       - name: AVD cache | ||||||
|         if: github.event_name != 'pull_request' |         if: github.event_name != 'pull_request' | ||||||
|         uses: actions/cache@v4 |         uses: actions/cache@v2 | ||||||
|         id: avd-cache |         id: avd-cache | ||||||
|         with: |         with: | ||||||
|           path: | |           path: | | ||||||
|  | @ -89,7 +89,7 @@ jobs: | ||||||
|         run: bash ./gradlew assembleBetaDebug --stacktrace |         run: bash ./gradlew assembleBetaDebug --stacktrace | ||||||
| 
 | 
 | ||||||
|       - name: Upload betaDebug APK |       - name: Upload betaDebug APK | ||||||
|         uses: actions/upload-artifact@v4 |         uses: actions/upload-artifact@v2.3.1 | ||||||
|         with: |         with: | ||||||
|           name: betaDebugAPK |           name: betaDebugAPK | ||||||
|           path: app/build/outputs/apk/beta/debug/app-*.apk |           path: app/build/outputs/apk/beta/debug/app-*.apk | ||||||
|  | @ -98,17 +98,7 @@ jobs: | ||||||
|         run: bash ./gradlew assembleProdDebug --stacktrace |         run: bash ./gradlew assembleProdDebug --stacktrace | ||||||
| 
 | 
 | ||||||
|       - name: Upload prodDebug APK |       - name: Upload prodDebug APK | ||||||
|         uses: actions/upload-artifact@v4 |         uses: actions/upload-artifact@v2.3.1 | ||||||
|         with: |         with: | ||||||
|           name: prodDebugAPK |           name: prodDebugAPK | ||||||
|           path: app/build/outputs/apk/prod/debug/app-*.apk |           path: app/build/outputs/apk/prod/debug/app-*.apk | ||||||
| 
 |  | ||||||
|       - name: Create and PR number artifact |  | ||||||
|         run: | |  | ||||||
|           echo "{\"pr_number\": ${{ github.event.pull_request.number || 'null' }}}" > pr_number.json |  | ||||||
|            |  | ||||||
|       - name: Upload PR number artifact |  | ||||||
|         uses: actions/upload-artifact@v4 |  | ||||||
|         with: |  | ||||||
|           name: pr_number |  | ||||||
|           path: ./pr_number.json |  | ||||||
|  |  | ||||||
							
								
								
									
										41
									
								
								.github/workflows/build-beta.yml
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										41
									
								
								.github/workflows/build-beta.yml
									
										
									
									
										vendored
									
									
								
							|  | @ -1,41 +0,0 @@ | ||||||
| name: Build beta only |  | ||||||
| 
 |  | ||||||
| on: [workflow_dispatch] |  | ||||||
| 
 |  | ||||||
| jobs: |  | ||||||
|   build: |  | ||||||
| 
 |  | ||||||
|     runs-on: ubuntu-latest |  | ||||||
| 
 |  | ||||||
|     steps: |  | ||||||
|     - uses: actions/checkout@v4 |  | ||||||
|     - name: set up JDK 17 |  | ||||||
|       uses: actions/setup-java@v4 |  | ||||||
|       with: |  | ||||||
|         java-version: '17' |  | ||||||
|         distribution: 'temurin' |  | ||||||
|         cache: gradle |  | ||||||
| 
 |  | ||||||
|     - name: Access test login credentials |  | ||||||
|       run: | |  | ||||||
|         echo "TEST_USER_NAME=${{ secrets.TEST_USER_NAME }}" >> local.properties |  | ||||||
|         echo "TEST_USER_PASSWORD=${{ secrets.TEST_USER_PASSWORD }}" >> local.properties |  | ||||||
| 
 |  | ||||||
|     - name: Grant execute permission for gradlew |  | ||||||
|       run: chmod +x gradlew |  | ||||||
| 
 |  | ||||||
|     - name: Set env |  | ||||||
|       run: echo "COMMIT_SHA=$(git log -n 1 --format='%h')" >> $GITHUB_ENV |  | ||||||
| 
 |  | ||||||
|     - name: Generate betaDebug APK |  | ||||||
|       run: ./gradlew assembleBetaDebug --stacktrace |  | ||||||
| 
 |  | ||||||
|     - name: Rename betaDebug APK |  | ||||||
|       run: mv app/build/outputs/apk/beta/debug/app-*.apk app/build/outputs/apk/beta/debug/apps-android-commons-betaDebug-$COMMIT_SHA.apk |  | ||||||
| 
 |  | ||||||
|     - name: Upload betaDebug APK |  | ||||||
|       uses: actions/upload-artifact@v4 |  | ||||||
|       with: |  | ||||||
|         name: apps-android-commons-betaDebugAPK-${{ env.COMMIT_SHA }} |  | ||||||
|         path: app/build/outputs/apk/beta/debug/*.apk |  | ||||||
|         retention-days: 30 |  | ||||||
							
								
								
									
										96
									
								
								.github/workflows/comment_artifacts_on_PR.yml
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										96
									
								
								.github/workflows/comment_artifacts_on_PR.yml
									
										
									
									
										vendored
									
									
								
							|  | @ -1,96 +0,0 @@ | ||||||
| name: Comment Artifacts on PR |  | ||||||
| 
 |  | ||||||
| on: |  | ||||||
|   workflow_run: |  | ||||||
|     workflows: [ "Android CI" ] |  | ||||||
|     types: [ completed ] |  | ||||||
| 
 |  | ||||||
| permissions: |  | ||||||
|   pull-requests: write |  | ||||||
|   contents: read |  | ||||||
| 
 |  | ||||||
| concurrency: |  | ||||||
|   group: comment-${{ github.event.workflow_run.id }} |  | ||||||
|   cancel-in-progress: true |  | ||||||
| 
 |  | ||||||
| jobs: |  | ||||||
|   comment: |  | ||||||
|     runs-on: ubuntu-latest |  | ||||||
|     if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request' }} |  | ||||||
|     steps: |  | ||||||
|       - name: Download and process artifacts |  | ||||||
|         uses: actions/github-script@v7 |  | ||||||
|         with: |  | ||||||
|           github-token: ${{ secrets.GITHUB_TOKEN }} |  | ||||||
|           script: | |  | ||||||
|             const fs = require('fs'); |  | ||||||
|             const runId = context.payload.workflow_run.id; |  | ||||||
| 
 |  | ||||||
|             const allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ |  | ||||||
|               owner: context.repo.owner, |  | ||||||
|               repo: context.repo.repo, |  | ||||||
|               run_id: runId, |  | ||||||
|             }); |  | ||||||
| 
 |  | ||||||
|             const prNumberArtifact = allArtifacts.data.artifacts.find(artifact => artifact.name === "pr_number"); |  | ||||||
|             if (!prNumberArtifact) { |  | ||||||
|               console.log("pr_number artifact not found."); |  | ||||||
|               return; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             const download = await github.rest.actions.downloadArtifact({ |  | ||||||
|               owner: context.repo.owner, |  | ||||||
|               repo: context.repo.repo, |  | ||||||
|               artifact_id: prNumberArtifact.id, |  | ||||||
|               archive_format: 'zip', |  | ||||||
|             }); |  | ||||||
| 
 |  | ||||||
|             fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/pr_number.zip`, Buffer.from(download.data)); |  | ||||||
|             const { execSync } = require('child_process'); |  | ||||||
|             execSync('unzip -q pr_number.zip -d ./pr_number/'); |  | ||||||
|             fs.unlinkSync('pr_number.zip'); |  | ||||||
| 
 |  | ||||||
|             const prData = JSON.parse(fs.readFileSync('./pr_number/pr_number.json', 'utf8')); |  | ||||||
|             const prNumber = prData.pr_number; |  | ||||||
| 
 |  | ||||||
|             if (!prNumber || prNumber === 'null') { |  | ||||||
|               console.log("No valid PR number found in pr_number.json. Skipping."); |  | ||||||
|               return; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             const artifactsToLink = allArtifacts.data.artifacts.filter(artifact => artifact.name !== "pr_number"); |  | ||||||
|             if (artifactsToLink.length === 0) { |  | ||||||
|               console.log("No artifacts to link found."); |  | ||||||
|               return; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             const comments = await github.rest.issues.listComments({ |  | ||||||
|               owner: context.repo.owner, |  | ||||||
|               repo: context.repo.repo, |  | ||||||
|               issue_number: Number(prNumber), |  | ||||||
|             }); |  | ||||||
|              |  | ||||||
|             const oldComments = comments.data.filter(comment =>  |  | ||||||
|               comment.body.startsWith("✅ Generated APK variants!") |  | ||||||
|             ); |  | ||||||
|             for (const comment of oldComments) { |  | ||||||
|               await github.rest.issues.deleteComment({ |  | ||||||
|                 owner: context.repo.owner, |  | ||||||
|                 repo: context.repo.repo, |  | ||||||
|                 comment_id: comment.id, |  | ||||||
|               }); |  | ||||||
|               console.log(`Deleted old comment ID: ${comment.id}`); |  | ||||||
|             }; |  | ||||||
|              |  | ||||||
|             const commentBody = `✅ Generated APK variants!\n` +  |  | ||||||
|             artifactsToLink.map(artifact => { |  | ||||||
|                 const artifactUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}/artifacts/${artifact.id}`; |  | ||||||
|                 return `- 🤖 [Download ${artifact.name}](${artifactUrl})`; |  | ||||||
|             }).join('\n'); |  | ||||||
| 
 |  | ||||||
|             await github.rest.issues.createComment({ |  | ||||||
|               owner: context.repo.owner, |  | ||||||
|               repo: context.repo.repo, |  | ||||||
|               issue_number: Number(prNumber), |  | ||||||
|               body: commentBody |  | ||||||
|             }); |  | ||||||
							
								
								
									
										5
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							|  | @ -43,8 +43,3 @@ app/src/main/jniLibs | ||||||
| #https://docs.opencv.org/3.3.0/ | #https://docs.opencv.org/3.3.0/ | ||||||
| /libraries/opencv/javadoc/ | /libraries/opencv/javadoc/ | ||||||
| captures/* | captures/* | ||||||
| 
 |  | ||||||
| # Test and other output |  | ||||||
| app/jacoco.exec |  | ||||||
| app/CommonsContributions |  | ||||||
| app/.* |  | ||||||
|  |  | ||||||
							
								
								
									
										18
									
								
								.idea/codeStyles/Project.xml
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										18
									
								
								.idea/codeStyles/Project.xml
									
										
									
										generated
									
									
									
								
							|  | @ -16,7 +16,6 @@ | ||||||
|       <option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="999" /> |       <option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="999" /> | ||||||
|       <option name="IMPORT_LAYOUT_TABLE"> |       <option name="IMPORT_LAYOUT_TABLE"> | ||||||
|         <value> |         <value> | ||||||
|           <package name="" withSubpackages="true" static="false" module="true" /> |  | ||||||
|           <package name="" withSubpackages="true" static="true" /> |           <package name="" withSubpackages="true" static="true" /> | ||||||
|           <emptyLine /> |           <emptyLine /> | ||||||
|           <package name="" withSubpackages="true" static="false" /> |           <package name="" withSubpackages="true" static="false" /> | ||||||
|  | @ -40,18 +39,21 @@ | ||||||
|       <option name="ALIGN_INIT_LIST_IN_COLUMNS" value="false" /> |       <option name="ALIGN_INIT_LIST_IN_COLUMNS" value="false" /> | ||||||
|       <option name="SPACE_BEFORE_SUPERCLASS_COLON" value="false" /> |       <option name="SPACE_BEFORE_SUPERCLASS_COLON" value="false" /> | ||||||
|     </Objective-C> |     </Objective-C> | ||||||
|  |     <Objective-C-extensions> | ||||||
|  |       <extensions> | ||||||
|  |         <pair source="cc" header="h" fileNamingConvention="NONE" /> | ||||||
|  |         <pair source="c" header="h" fileNamingConvention="NONE" /> | ||||||
|  |       </extensions> | ||||||
|  |     </Objective-C-extensions> | ||||||
|     <Python> |     <Python> | ||||||
|       <option name="USE_CONTINUATION_INDENT_FOR_ARGUMENTS" value="true" /> |       <option name="USE_CONTINUATION_INDENT_FOR_ARGUMENTS" value="true" /> | ||||||
|     </Python> |     </Python> | ||||||
|     <TypeScriptCodeStyleSettings> |     <TypeScriptCodeStyleSettings> | ||||||
|       <option name="INDENT_CHAINED_CALLS" value="false" /> |       <option name="INDENT_CHAINED_CALLS" value="false" /> | ||||||
|     </TypeScriptCodeStyleSettings> |     </TypeScriptCodeStyleSettings> | ||||||
|     <files> |     <XML> | ||||||
|       <extensions> |       <option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" /> | ||||||
|         <pair source="cc" header="h" fileNamingConvention="NONE" /> |     </XML> | ||||||
|         <pair source="c" header="h" fileNamingConvention="NONE" /> |  | ||||||
|       </extensions> |  | ||||||
|     </files> |  | ||||||
|     <codeStyleSettings language="CSS"> |     <codeStyleSettings language="CSS"> | ||||||
|       <indentOptions> |       <indentOptions> | ||||||
|         <option name="INDENT_SIZE" value="2" /> |         <option name="INDENT_SIZE" value="2" /> | ||||||
|  | @ -316,7 +318,9 @@ | ||||||
|     <codeStyleSettings language="protobuf"> |     <codeStyleSettings language="protobuf"> | ||||||
|       <option name="RIGHT_MARGIN" value="80" /> |       <option name="RIGHT_MARGIN" value="80" /> | ||||||
|       <indentOptions> |       <indentOptions> | ||||||
|  |         <option name="INDENT_SIZE" value="2" /> | ||||||
|         <option name="CONTINUATION_INDENT_SIZE" value="2" /> |         <option name="CONTINUATION_INDENT_SIZE" value="2" /> | ||||||
|  |         <option name="TAB_SIZE" value="2" /> | ||||||
|       </indentOptions> |       </indentOptions> | ||||||
|     </codeStyleSettings> |     </codeStyleSettings> | ||||||
|   </code_scheme> |   </code_scheme> | ||||||
|  |  | ||||||
							
								
								
									
										52
									
								
								.idea/inspectionProfiles/Project_Default.xml
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										52
									
								
								.idea/inspectionProfiles/Project_Default.xml
									
										
									
										generated
									
									
									
								
							|  | @ -1,36 +1,16 @@ | ||||||
| <component name="InspectionProjectProfileManager"> | <component name="InspectionProjectProfileManager"> | ||||||
|   <profile version="1.0"> |   <profile version="1.0"> | ||||||
|     <option name="myName" value="Project Default" /> |     <option name="myName" value="Project Default" /> | ||||||
|  |     <inspection_tool class="AndroidLintNewerVersionAvailable" enabled="true" level="WARNING" enabled_by_default="true" /> | ||||||
|  |     <inspection_tool class="AutoCloseableResource" enabled="true" level="WARNING" enabled_by_default="true" /> | ||||||
|     <inspection_tool class="ClassWithOnlyPrivateConstructors" enabled="true" level="WARNING" enabled_by_default="true" /> |     <inspection_tool class="ClassWithOnlyPrivateConstructors" enabled="true" level="WARNING" enabled_by_default="true" /> | ||||||
|     <inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true"> |  | ||||||
|       <option name="composableFile" value="true" /> |  | ||||||
|     </inspection_tool> |  | ||||||
|     <inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true"> |  | ||||||
|       <option name="composableFile" value="true" /> |  | ||||||
|     </inspection_tool> |  | ||||||
|     <inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true"> |  | ||||||
|       <option name="composableFile" value="true" /> |  | ||||||
|     </inspection_tool> |  | ||||||
|     <inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true"> |  | ||||||
|       <option name="composableFile" value="true" /> |  | ||||||
|     </inspection_tool> |  | ||||||
|     <inspection_tool class="ConfusingElse" enabled="true" level="WARNING" enabled_by_default="true"> |     <inspection_tool class="ConfusingElse" enabled="true" level="WARNING" enabled_by_default="true"> | ||||||
|       <option name="reportWhenNoStatementFollow" value="true" /> |       <option name="reportWhenNoStatementFollow" value="true" /> | ||||||
|     </inspection_tool> |     </inspection_tool> | ||||||
|     <inspection_tool class="ControlFlowStatementWithoutBraces" enabled="true" level="ERROR" enabled_by_default="true" /> |     <inspection_tool class="ControlFlowStatementWithoutBraces" enabled="true" level="ERROR" enabled_by_default="true" /> | ||||||
|  |     <inspection_tool class="DefaultNotLastCaseInSwitch" enabled="true" level="WARNING" enabled_by_default="true" /> | ||||||
|     <inspection_tool class="ExplicitThis" enabled="true" level="WEAK WARNING" enabled_by_default="true" /> |     <inspection_tool class="ExplicitThis" enabled="true" level="WEAK WARNING" enabled_by_default="true" /> | ||||||
|     <inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true"> |     <inspection_tool class="FieldMayBeFinal" enabled="true" level="WARNING" enabled_by_default="true" /> | ||||||
|       <option name="composableFile" value="true" /> |  | ||||||
|     </inspection_tool> |  | ||||||
|     <inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true"> |  | ||||||
|       <option name="composableFile" value="true" /> |  | ||||||
|     </inspection_tool> |  | ||||||
|     <inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true"> |  | ||||||
|       <option name="composableFile" value="true" /> |  | ||||||
|     </inspection_tool> |  | ||||||
|     <inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true"> |  | ||||||
|       <option name="composableFile" value="true" /> |  | ||||||
|     </inspection_tool> |  | ||||||
|     <inspection_tool class="LocalCanBeFinal" enabled="true" level="WARNING" enabled_by_default="true"> |     <inspection_tool class="LocalCanBeFinal" enabled="true" level="WARNING" enabled_by_default="true"> | ||||||
|       <option name="REPORT_VARIABLES" value="true" /> |       <option name="REPORT_VARIABLES" value="true" /> | ||||||
|       <option name="REPORT_PARAMETERS" value="true" /> |       <option name="REPORT_PARAMETERS" value="true" /> | ||||||
|  | @ -44,33 +24,14 @@ | ||||||
|     <inspection_tool class="OverlyStrongTypeCast" enabled="true" level="WARNING" enabled_by_default="true"> |     <inspection_tool class="OverlyStrongTypeCast" enabled="true" level="WARNING" enabled_by_default="true"> | ||||||
|       <option name="ignoreInMatchingInstanceof" value="false" /> |       <option name="ignoreInMatchingInstanceof" value="false" /> | ||||||
|     </inspection_tool> |     </inspection_tool> | ||||||
|     <inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true"> |  | ||||||
|       <option name="composableFile" value="true" /> |  | ||||||
|     </inspection_tool> |  | ||||||
|     <inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true"> |  | ||||||
|       <option name="composableFile" value="true" /> |  | ||||||
|     </inspection_tool> |  | ||||||
|     <inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true"> |  | ||||||
|       <option name="composableFile" value="true" /> |  | ||||||
|     </inspection_tool> |  | ||||||
|     <inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true"> |  | ||||||
|       <option name="composableFile" value="true" /> |  | ||||||
|     </inspection_tool> |  | ||||||
|     <inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true"> |  | ||||||
|       <option name="composableFile" value="true" /> |  | ||||||
|     </inspection_tool> |  | ||||||
|     <inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true"> |  | ||||||
|       <option name="composableFile" value="true" /> |  | ||||||
|     </inspection_tool> |  | ||||||
|     <inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true"> |  | ||||||
|       <option name="composableFile" value="true" /> |  | ||||||
|     </inspection_tool> |  | ||||||
|     <inspection_tool class="ProblematicWhitespace" enabled="true" level="WARNING" enabled_by_default="true" /> |     <inspection_tool class="ProblematicWhitespace" enabled="true" level="WARNING" enabled_by_default="true" /> | ||||||
|  |     <inspection_tool class="ProtectedMemberInFinalClass" enabled="true" level="WARNING" enabled_by_default="true" /> | ||||||
|     <inspection_tool class="RedundantFieldInitialization" enabled="true" level="WARNING" enabled_by_default="true" /> |     <inspection_tool class="RedundantFieldInitialization" enabled="true" level="WARNING" enabled_by_default="true" /> | ||||||
|     <inspection_tool class="RedundantImplements" enabled="true" level="WARNING" enabled_by_default="true"> |     <inspection_tool class="RedundantImplements" enabled="true" level="WARNING" enabled_by_default="true"> | ||||||
|       <option name="ignoreSerializable" value="false" /> |       <option name="ignoreSerializable" value="false" /> | ||||||
|       <option name="ignoreCloneable" value="false" /> |       <option name="ignoreCloneable" value="false" /> | ||||||
|     </inspection_tool> |     </inspection_tool> | ||||||
|  |     <inspection_tool class="RedundantMethodOverride" enabled="true" level="WARNING" enabled_by_default="true" /> | ||||||
|     <inspection_tool class="SimplifiableEqualsExpression" enabled="true" level="WARNING" enabled_by_default="true" /> |     <inspection_tool class="SimplifiableEqualsExpression" enabled="true" level="WARNING" enabled_by_default="true" /> | ||||||
|     <inspection_tool class="TypeParameterExtendsFinalClass" enabled="true" level="WARNING" enabled_by_default="true" /> |     <inspection_tool class="TypeParameterExtendsFinalClass" enabled="true" level="WARNING" enabled_by_default="true" /> | ||||||
|     <inspection_tool class="UnnecessarilyQualifiedStaticUsage" enabled="true" level="WARNING" enabled_by_default="true"> |     <inspection_tool class="UnnecessarilyQualifiedStaticUsage" enabled="true" level="WARNING" enabled_by_default="true"> | ||||||
|  | @ -86,5 +47,6 @@ | ||||||
|     <inspection_tool class="UnnecessaryQualifierForThis" enabled="true" level="WARNING" enabled_by_default="true" /> |     <inspection_tool class="UnnecessaryQualifierForThis" enabled="true" level="WARNING" enabled_by_default="true" /> | ||||||
|     <inspection_tool class="UnnecessarySuperConstructor" enabled="true" level="WARNING" enabled_by_default="true" /> |     <inspection_tool class="UnnecessarySuperConstructor" enabled="true" level="WARNING" enabled_by_default="true" /> | ||||||
|     <inspection_tool class="UnnecessaryThis" enabled="true" level="WARNING" enabled_by_default="true" /> |     <inspection_tool class="UnnecessaryThis" enabled="true" level="WARNING" enabled_by_default="true" /> | ||||||
|  |     <inspection_tool class="UnnecessaryToStringCall" enabled="true" level="WARNING" enabled_by_default="true" /> | ||||||
|   </profile> |   </profile> | ||||||
| </component> | </component> | ||||||
							
								
								
									
										5
									
								
								.mailmap
									
										
									
									
									
								
							
							
						
						
									
										5
									
								
								.mailmap
									
										
									
									
									
								
							|  | @ -1,5 +0,0 @@ | ||||||
| # See: https://git-scm.com/docs/git-shortlog#_mapping_authors |  | ||||||
| # |  | ||||||
| Brooke Vibber <bvibber@wikimedia.org> |  | ||||||
| Brooke Vibber <bvibber@wikimedia.org> <brion@wikimedia.org> |  | ||||||
| Brooke Vibber <bvibber@wikimedia.org> <brion@pobox.com> |  | ||||||
							
								
								
									
										329
									
								
								CHANGELOG.md
									
										
									
									
									
								
							
							
						
						
									
										329
									
								
								CHANGELOG.md
									
										
									
									
									
								
							|  | @ -1,334 +1,5 @@ | ||||||
| # Wikimedia Commons for Android | # Wikimedia Commons for Android | ||||||
| 
 | 
 | ||||||
| ## v6.0.2 |  | ||||||
| 
 |  | ||||||
| ### What's changed |  | ||||||
| * Addressed a bug that prevented the keyboard from appearing in various text fields, such as on the upload wizard |  | ||||||
| * Links in the "File usages" list are now clickable and will take you to the correct page. |  | ||||||
| * Titles for file usages are now clearer and easier to understand |  | ||||||
| * Bug fixes and stability improvements |  | ||||||
| 
 |  | ||||||
| ## v6.0.1 |  | ||||||
| 
 |  | ||||||
| ### What's changed |  | ||||||
| * The app now supports Android 15 with an improved user interface |  | ||||||
| * Enhanced Nearby with robust and more reliable labels |  | ||||||
| * Bug fixes and stability improvements |  | ||||||
| 
 |  | ||||||
| ## v5.6.1 |  | ||||||
| 
 |  | ||||||
| ### What's changed |  | ||||||
| * The app no longer uploads images to Wikidata if one exists already for a given item |  | ||||||
| * File usage displays correctly now |  | ||||||
| * No more infinite circular progress bar on nominating an image for deletion |  | ||||||
| * Enhanced location updates while using GPS |  | ||||||
| * Author/uploader names are now available in Media Details for Commons licensing compliance |  | ||||||
| * Improved usage of popups in Nearby |  | ||||||
| * Bug fixes and stability improvements  |  | ||||||
| 
 |  | ||||||
| ## v5.5.0 |  | ||||||
| 
 |  | ||||||
| ### What's changed |  | ||||||
| * Explore images will now be shown based on the map location and not at your current location |  | ||||||
| * Enhanced Wikidata feedback message |  | ||||||
| * Green labels in Explore map will no longer be hidden by other pins thumbnails |  | ||||||
| * Upload wizard's language drop-down now reflects the language used in the pin label |  | ||||||
| * Users can now pick only one image at a time while using the custom selector |  | ||||||
| * Bug fixes and stability improvements  |  | ||||||
| 
 |  | ||||||
| ## v5.4.1 |  | ||||||
| 
 |  | ||||||
| ### What's changed |  | ||||||
| * Custom picker now detects images that are already available on Commons |  | ||||||
| * Improve credit line in image list |  | ||||||
| * Show place cards with loaded names only in the Nearby list |  | ||||||
| * Fix the error that occurs while loading images in Explore |  | ||||||
| 
 |  | ||||||
| ## v5.3.0 |  | ||||||
| 
 |  | ||||||
| ### What's changed |  | ||||||
| * Enable EmailAuth support |  | ||||||
| * Explore map images no longer show "Unknown" |  | ||||||
| * Fix crash when removing last two images of multiupload |  | ||||||
| * Mark ❌ for closed locations (P3999) in Nearby |  | ||||||
| * Fix two pin labels staying visible at the same time in Explore map |  | ||||||
| * Refactoring and minor UI improvements |  | ||||||
| 
 |  | ||||||
| ## v5.2.0 |  | ||||||
| 
 |  | ||||||
| v5.2.0 boasts several new functionalities like: |  | ||||||
| 
 |  | ||||||
| * A new refresh button lets you quickly reload the Nearby map |  | ||||||
| * Bookmarks now support categories |  | ||||||
| * Improved feedback and consistency in the user interface |  | ||||||
| * Bug fixes and performance improvements |  | ||||||
| 
 |  | ||||||
| ### What's changed |  | ||||||
| * Implement "Refresh" button to clear the cache and reload the Nearby map. |  | ||||||
| * `CommonsApplication` migrate to kotlin & some lint fixes. |  | ||||||
| * Revert back to MainScope for database and UI updates and make database operations thread safe. |  | ||||||
| * Hide edit options for logged-out users in Explore screen. |  | ||||||
| * Introduced a button to delete the current folder in custom selector. |  | ||||||
| * Improve Unique File Name Search. |  | ||||||
| * Migration of several modules from Java to Kotlin. |  | ||||||
| * Fix modification on bottom sheet's data when coming from Nearby Banner and clicked on other pins. |  | ||||||
| * Bug fixes and enhancement of Achievements screen. |  | ||||||
| * Show where file is being used on Commons and other wikis. |  | ||||||
| * Migrate android.media.ExifInterface to androidx.exifinterface.media.ExifInterface as android.media.ExifInterface had security flaws on older devices. |  | ||||||
| * Make dialogs modal and always show the upload icon. |  | ||||||
| * Fix unintentional deletion of subfolders and non-images by custom selector. |  | ||||||
| * Bookmark categories. |  | ||||||
| * Add pull down to refresh in the Contributions screen. |  | ||||||
| * Fix race condition and lag when loading pin details, faster overlay management. |  | ||||||
| * Show cached pins in Nearby even when internet is unavailable |  | ||||||
| 
 |  | ||||||
|  Full changelog with the list of contributors: [`v5.1.2...v5.2.0`](https://github.com/commons-app/apps-android-commons/compare/v5.1.2...v5.2.0). |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| ## v5.1.2 |  | ||||||
| 
 |  | ||||||
| ### What's changed |  | ||||||
| 
 |  | ||||||
| * Fix the broken category search in the explore screen |  | ||||||
| 
 |  | ||||||
| ## v5.1.1 |  | ||||||
| 
 |  | ||||||
| ### What's changed |  | ||||||
| 
 |  | ||||||
| * Use Android's new EXIF interface to mitigate security issues in old |  | ||||||
|   EXIF interface. |  | ||||||
| * Make the icon that helps view the upload queue always visible as it ensures |  | ||||||
|   that the queue accessible at all times. |  | ||||||
| 
 |  | ||||||
| ## v5.1.0 |  | ||||||
| 
 |  | ||||||
| ### What's Changed |  | ||||||
| 
 |  | ||||||
| * Enhanced **upload queue management** in the Commons app for smoother, sequential |  | ||||||
|   processing, clearer progress tracking, prevention of stuck or duplicate |  | ||||||
|   uploads. As part of this improvement, the "Limited Connection mode" has been |  | ||||||
|   removed. |  | ||||||
| * Added an option in "Nearby" feature enabling users to **provide feedback on |  | ||||||
|   Wikidata items**. Users can report if an item doesn’t exist, is at a different |  | ||||||
|   location, or has other issues, with submissions tagged for easy tracking and |  | ||||||
|   updates. |  | ||||||
| * Improved the "Nearby" feature by splitting the query into two parts for faster |  | ||||||
|   loading and **better performance, especially in areas with dense amount of |  | ||||||
|   places**. This update also resolves issues with pins overlapping place names. |  | ||||||
| * Upgraded AGP and **target/compile SDK to 34** and make necessary adjustments to |  | ||||||
|   the app such as adding **"Partial Access" support**. Also includes some minor |  | ||||||
|   refactoring, and replacement of deprecated circular progress bars. |  | ||||||
| * Fixed an **UI issue where the 'Subcategories' and 'Parent Categories' tabs |  | ||||||
|   appeared blank** in the Category Details screen. Resolved by optimizing view |  | ||||||
|   binding handling in the parent fragments. |  | ||||||
| * Fixed an issue where editing depictions removed all other structured data from |  | ||||||
|   images. Now, **only depictions are updated, preserving other associated data**. |  | ||||||
| * Fixed **map centering** in the image upload flow to **use GPS EXIF tag location** |  | ||||||
|   from pictures and ensured "Show in map app" accurately reflects this location. |  | ||||||
| * Fixed navigation **after uploading via Nearby by directing users to the Uploads |  | ||||||
|   activity** instead of returning to Nearby, preventing confusion about needing to |  | ||||||
|   upload again. |  | ||||||
| 
 |  | ||||||
| ### Bug fixes and various changes |  | ||||||
| 
 |  | ||||||
| * Improved the "Nearby" feature to fetch labels based on the user's preferred |  | ||||||
|   language instead of defaulting to English. |  | ||||||
| * Added a legend to the "Nearby" feature indicating pin statuses: red for items |  | ||||||
|   without pictures, green for those with pictures, and grey for items being |  | ||||||
|   checked. A floating action button now allows users to toggle the legend's |  | ||||||
|   visibility. |  | ||||||
| * Fixed an issue where the "Nominate for deletion" option is shown to logged out |  | ||||||
|   users, preventing app errors and crashes. |  | ||||||
| * Updated the regex pattern that filters categories with an year in it to also |  | ||||||
|   filter the 2020s. |  | ||||||
| * Fix an issue where past depictions were not shown as suggestions, despite |  | ||||||
|   being saved correctly. |  | ||||||
| * Fixed an issue in custom image picker where exiting the media preview showed |  | ||||||
|   only the first image and cleared selections. Now, previously selected images |  | ||||||
|   are restored correctly after exiting the preview. This was contributed. |  | ||||||
| * Fixed an issue in custom image picker where scrolling behavior did not |  | ||||||
|   maintain position after exiting fullscreen preview, ensuring users remain at |  | ||||||
|   the same point in their image roll unless actioned images are filtered. This |  | ||||||
|   was contributed. |  | ||||||
| * Fixed Nearby map not showing new pins on map move by removing the 2000m scroll |  | ||||||
|   threshold and adding an 800ms debounce for smoother pin updates when the map |  | ||||||
|   is moved. Queued searches are now canceled on fragment destruction. |  | ||||||
| * Revised author information retrieval to emphasize the custom author name from |  | ||||||
|   the metadata instead of the default registered username. |  | ||||||
| * Enhanced notification classification to properly identify "email" type |  | ||||||
|   notifications and prompting users to check their e-mail inbox when such |  | ||||||
|   notifications are clicked. |  | ||||||
| * Resolved a bug in the language chooser that incorrectly greyed-out previously |  | ||||||
|   selected languages, ensuring only the current language is non-selectable during |  | ||||||
|   image upload. |  | ||||||
| * Resolved pin color update issue in "Nearby" feature where the pin colour |  | ||||||
|   failed to be updated after a successful image upload. |  | ||||||
| 
 |  | ||||||
| What's listed here is only a subset of all the changes. Check the full-list of |  | ||||||
| the changes in [this link](https://github.com/commons-app/apps-android-commons/compare/v5.0.2...v5.1.0). |  | ||||||
| Alternatively, checkout [this release on GitHub releases page](https://github.com/commons-app/apps-android-commons/releases/tag/v5.1.0) |  | ||||||
| for an exhaustive list of changes and the various contributors who contributed the same. |  | ||||||
| 
 |  | ||||||
| ## v5.0.2 |  | ||||||
| 
 |  | ||||||
| - Enhanced multi-upload functionality with user prompts to clarify that all images would share the |  | ||||||
|   same category and depictions. |  | ||||||
| - Show Wikidata description on currently active Nearby pin to provide more useful information. |  | ||||||
| - Improve the visibility of map markers by dynamically adjusting their colors based on the app's |  | ||||||
|   theme. The map markers will now appear lighter when the app is in dark mode and darker when the |  | ||||||
|   app is in light mode. This change aims to enhance marker visibility and improve the overall user |  | ||||||
|   experience. |  | ||||||
| - Added information on where user feedback is posted, helping users track existing feedback and |  | ||||||
|   monitor their own submissions. |  | ||||||
| - Enhanced the edit location screen of the upload screen by centering the map on the picture's |  | ||||||
|   location from metadata when editing, or on the device's GPS location if metadata is unavailable, |  | ||||||
|   improving accuracy and user experience. |  | ||||||
| - Ensured the 'Add Location' button is renamed to 'Edit Location' when copying the location of a |  | ||||||
|   recently uploaded image, enhancing clarity and user experience. |  | ||||||
| - Added a ProgressBar to the media detail screen to indicate image loading status, enhancing user |  | ||||||
|   experience by showing loading progress until the image is fully loaded. |  | ||||||
| - Fixed an issue where caption and description fields would intermittently disappear when using |  | ||||||
|   voice input, ensuring text remains visible and stable across all entries. |  | ||||||
| - Fixed a crash that occurred when attempting to remove multiple instances of caption/description |  | ||||||
|   fields after initially adding them. |  | ||||||
| - Improve the text in the prompt shown when skipping login to sound more natural. |  | ||||||
| - Modified feedback addition logic to append new sections at the bottom of the page, ensuring |  | ||||||
|   auto-archiving of sections functions correctly on the feedback page. |  | ||||||
| - Resolved issue where the app failed to clear cookies upon logout. |  | ||||||
| 
 |  | ||||||
| ## v5.0.1 |  | ||||||
| 
 |  | ||||||
| Same as v5.0.0 except this fixes some R8 rules to ensure that the release |  | ||||||
| variants of the app work as intended. |  | ||||||
| 
 |  | ||||||
| ## v5.0.0 |  | ||||||
| 
 |  | ||||||
| ### What's Changed |  | ||||||
| 
 |  | ||||||
| - Redesigned the map feature to **replace Mapbox with the osmdroid library**. |  | ||||||
|   Key elements like pin visualization and user-centered display are still |  | ||||||
|   included in this redesign. This is done to guard against possible misuse of |  | ||||||
|   the Mapbox token and, more crucially, to keep the app from becoming dependent |  | ||||||
|   on a service that charges for usage but offers a free tier. |  | ||||||
| 
 |  | ||||||
|   With this change, the app retrieves the map tiles from [Wikimedia maps](https://maps.wikimedia.org). |  | ||||||
| - Add the ability to **export locations of nearby missing pictures in GPX and |  | ||||||
|   KML formats**. This allows users to browse the locations with desired radius |  | ||||||
|   for offline use in their favourite map apps like OsmAnd or Maps.me, enhancing |  | ||||||
|   accessibility  and offline functionality. |  | ||||||
| - **Limited the uploads via the custom image picker** to a maximum of 20. |  | ||||||
| - Added two menu choices for **transparent image backgrounds**, giving users the |  | ||||||
|   option of either a black or white background, increasing adaptability to |  | ||||||
|   various theme settings. |  | ||||||
| 
 |  | ||||||
|   User customization option has been provided with the |  | ||||||
|   ability to save background color selections permanently on a per image basis. |  | ||||||
| - Implemented functionality to **automatically resume uploads** that become |  | ||||||
|   stuck due to app termination or device reboot. |  | ||||||
| - Added a **compass arrow in the Nearby banner** shown in the "Contributions" |  | ||||||
|   screen to guide users towards the nearest item, thus providing the missing |  | ||||||
|   directional cues. The arrow dynamically adjusts based on device rotation, |  | ||||||
|   aligning with the calculated bearing towards the  target location. Further, |  | ||||||
|   the distance and direction are updated as the user moves. |  | ||||||
| - Implemented **voice input feature** for caption and description fields, |  | ||||||
|   enabling users to dictate text directly into these fields. |  | ||||||
| - Improved various flows in the app to **redirect users to the login page** and |  | ||||||
|   display a  persistent message **if their session becomes invalid** due to a |  | ||||||
|   password  change, enhancing user guidance and security measures. |  | ||||||
| 
 |  | ||||||
| ### Revamps and refactorings |  | ||||||
| 
 |  | ||||||
| - **Revamped initial upload screen layout and the description edit screen layout** |  | ||||||
|   for enhanced user experience and ensuring better symmetry in the design. |  | ||||||
| - **Replaced Butterknife with ViewBinding** in various places of the app. |  | ||||||
| - Transferred essential code from **the redundant data-client module** to the |  | ||||||
|   main Commons app code, enabling its integration and facilitating the removal |  | ||||||
|   of the redundant module. Further, convert various parts of the code to Kotlin. |  | ||||||
| - **Revamped the various location permission flows** to ensure consistency for |  | ||||||
|   the sake of a better user experience. |  | ||||||
| 
 |  | ||||||
| ### Bug fixes and various changes |  | ||||||
| 
 |  | ||||||
| - Resolved an issue where paused uploads that were subsequently cancelled were |  | ||||||
|   still being uploaded. |  | ||||||
| - Fixed an issue where some user information such as upload count were not |  | ||||||
|   displayed in the "Contributions" and "Profile" screens. |  | ||||||
| - Fixed the long-standing broken *"Picture of the Day" widget* to restore its |  | ||||||
|   usability. |  | ||||||
| - Resolved an issue where some categories were hidden at the top of Upload |  | ||||||
|   Wizard suggestions. |  | ||||||
| - Resolved an issue where there was a grey empty screen at Upload wizard when |  | ||||||
|   the app was denied the files permission. |  | ||||||
| - Implemented logic to bypass media in Peer Review if the current reviewer is |  | ||||||
|   also the user who uploaded the media. |  | ||||||
| - Corrected arrow image behaviour in the first upload screen: now displays down |  | ||||||
|   arrow when details card is fully visible, aligning with expected user |  | ||||||
|   interaction. |  | ||||||
| - Updated app icon to improve visibility and recognition on F-Droid. |  | ||||||
| - Fixed issue causing all pictures to disappear and activity to reload fully in |  | ||||||
|   the custom image selector after marking a picture as 'not for  upload', now |  | ||||||
|   ensuring only the selected picture is removed as expected. |  | ||||||
| 
 |  | ||||||
| What's listed here is only a subset of all the changes. Check the full-list of |  | ||||||
| the changes in [this link](https://github.com/commons-app/apps-android-commons/compare/v4.2.1...v5.0.0). |  | ||||||
| Alternatively, checkout [this release on GitHub releases page](https://github.com/commons-app/apps-android-commons/releases/tag/v5.0.0) |  | ||||||
| for an exhaustive list of changes and the various contributors who contributed the same. |  | ||||||
| 
 |  | ||||||
| ## v4.2.1 |  | ||||||
| 
 |  | ||||||
| - Provide the ability to edit an image to losslessly rotate it while uploading |  | ||||||
| - Fix a bug in v4.2.0 where the nearby places were not loading |  | ||||||
| - Fix a bug where editing depictions was showing a progress bar indefinitely |  | ||||||
| - In the upload screen, use different map icons to indicate if image is being uploaded with location |  | ||||||
|   metadata |  | ||||||
| - For nearby uploads, it is no longer possible to deselect the item's category and depiction |  | ||||||
| - The Mapbox account key used by the app has been changed |  | ||||||
| - Category search now shows exact matches without any discrepancies |  | ||||||
| - Various bug and crash fixes |  | ||||||
| 
 |  | ||||||
| ## v4.2.0 |  | ||||||
| - Dark mode colour improvements |  | ||||||
| - Enhancements done to address location metadata loss including the metadata loss that occurs in |  | ||||||
|   latest Android versions |  | ||||||
| - Enhancements done to address the issue where uploads get stuck in queued state |  | ||||||
| - Fix the inability to upload via the in-app camera option |  | ||||||
| - Provide the ability to optionally include location metadata for in-app camera uploads in case the |  | ||||||
|   device camera app does not provide location metadata |  | ||||||
| - Use geo location URL that works consistently across all map applications |  | ||||||
| - Fix crash when clicking on location target icon while trying to edit the location of an upload |  | ||||||
| - Fix crash that occurs randomly while returning to the app after leaving it in the background |  | ||||||
| - Fix crash in Sign up activity on Android version 5.0 and 5.1 |  | ||||||
| - Android 13 compatibility changes |  | ||||||
| 
 |  | ||||||
| ## v4.1.0 |  | ||||||
| - Location of pictures uploaded via custom picture selector are now recognized |  | ||||||
| - Improvements to the custom picture selector |  | ||||||
| - Ensure the WLM pictures are associated with the correct templates for each year |  | ||||||
| - Only show pictures uploaded via app in peer review |  | ||||||
| - Improve the variety of images show in peer review |  | ||||||
| - Allow going to current location in location edit dialog while uploading a picture |  | ||||||
| - Switch to using MapLibre instead of Mapbox and thereby disable telemetry sent to Mapbox |  | ||||||
| - Fixed various bugs |  | ||||||
| 
 |  | ||||||
| ## v4.0.5 |  | ||||||
| - Bumped min SDK to 29 to try and solve Google policy issue |  | ||||||
| - Reverted dialog |  | ||||||
| - Note: This encompasses versions 1031, 1032, and 1033, due to the Play Store's requirements to overwrite all the tracks with a post-fix version (otherwise no single track can be published) |  | ||||||
| 
 |  | ||||||
| ## v4.0.4 |  | ||||||
| - Added dialog for Google's location policy |  | ||||||
| 
 |  | ||||||
| ## v4.0.3 |  | ||||||
| - Added "Report" button for Google UGC policy |  | ||||||
| 
 |  | ||||||
| ## v4.0.2 |  | ||||||
| - Fixed bug with wrong dates taken from EXIF |  | ||||||
| - Fixed various crashes |  | ||||||
| 
 |  | ||||||
| ## v4.0.1 |  | ||||||
| - Fixed bug with no browser found |  | ||||||
| - Updated Mapbox SDK to fix hamburger crash |  | ||||||
| 
 |  | ||||||
| ## v4.0.0 | ## v4.0.0 | ||||||
| - Added map showing nearby Commons pictures | - Added map showing nearby Commons pictures | ||||||
| - Added custom SPARQL queries | - Added custom SPARQL queries | ||||||
|  |  | ||||||
							
								
								
									
										1
									
								
								CREDITS
									
										
									
									
									
								
							
							
						
						
									
										1
									
								
								CREDITS
									
										
									
									
									
								
							|  | @ -53,6 +53,7 @@ their contribution to the product. | ||||||
| * Butterknife | * Butterknife | ||||||
| * GSON | * GSON | ||||||
| * Timber | * Timber | ||||||
|  | * MapBox | ||||||
| 
 | 
 | ||||||
| 3rd party open source apps from which significant code has been reused: | 3rd party open source apps from which significant code has been reused: | ||||||
| * Android Wikipedia app https://github.com/wikimedia/apps-android-wikipedia | * Android Wikipedia app https://github.com/wikimedia/apps-android-wikipedia | ||||||
|  |  | ||||||
							
								
								
									
										19
									
								
								README.md
									
										
									
									
									
								
							
							
						
						
									
										19
									
								
								README.md
									
										
									
									
									
								
							|  | @ -1,12 +1,12 @@ | ||||||
| # Wikimedia Commons Android app | # Wikimedia Commons Android app | ||||||
|  |  | ||||||
| [](https://github.com/commons-app/apps-android-commons/actions?query=branch%3Amain) | [](https://github.com/commons-app/apps-android-commons/actions?query=branch%3Amaster) | ||||||
| [](https://appetize.io/app/8ywtpe9f8tb8h6bey11c92vkcw) | [](https://appetize.io/app/8ywtpe9f8tb8h6bey11c92vkcw) | ||||||
| [](https://codecov.io/gh/commons-app/apps-android-commons) | [](https://codecov.io/gh/commons-app/apps-android-commons) | ||||||
| 
 | 
 | ||||||
| The Wikimedia Commons Android app allows users to upload pictures from their Android phone/tablet to Wikimedia Commons. Download the app [here][1], or view our [website][2]. | The Wikimedia Commons Android app allows users to upload pictures from their Android phone/tablet to Wikimedia Commons. Download the app [here][1], or view our [website][2]. | ||||||
| 
 | 
 | ||||||
| Initially started by the Wikimedia Foundation, this app is now maintained by grantees and volunteers of the Wikimedia community. Anyone is welcome to improve it, just choose among the [open issues][3] and send us a pull request! :-)  | Initially started by the Wikimedia Foundation, this app is now maintained by grantees and volunteers of the Wikimedia community. Anyone is welcome to improve it, just choose among the [open issues][3] and send us a pull request :-)  | ||||||
| 
 | 
 | ||||||
| <a href="https://f-droid.org/repository/browse/?fdid=fr.free.nrw.commons" target="_blank"> | <a href="https://f-droid.org/repository/browse/?fdid=fr.free.nrw.commons" target="_blank"> | ||||||
| <img src="https://upload.wikimedia.org/wikipedia/commons/archive/9/96/20200131184248%21%22Get_it_on_F-droid%22_Badge.png" alt="Get it on F-Droid" height="90"/></a> | <img src="https://upload.wikimedia.org/wikipedia/commons/archive/9/96/20200131184248%21%22Get_it_on_F-droid%22_Badge.png" alt="Get it on F-Droid" height="90"/></a> | ||||||
|  | @ -15,7 +15,7 @@ Initially started by the Wikimedia Foundation, this app is now maintained by gra | ||||||
| 
 | 
 | ||||||
| ## Documentation | ## Documentation | ||||||
| 
 | 
 | ||||||
| Our [documentation repository][4] contains extensive documentation for users, contributors, and developers alike: | We try to have an extensive documentation at our [documentation repository][4]: | ||||||
| 
 | 
 | ||||||
| * [User Documentation][5] | * [User Documentation][5] | ||||||
| * [Contributor Documentation][6] | * [Contributor Documentation][6] | ||||||
|  | @ -29,12 +29,11 @@ Thank you all for your work! | ||||||
| 
 | 
 | ||||||
| | [<img src="https://avatars.githubusercontent.com/u/3611199?v=4" width="100px;"/><br /><sub><b>misaochan</b></sub>](https://github.com/misaochan) | [<img src="https://avatars.githubusercontent.com/u/24829418?v=4" width="100px;"/><br /><sub><b>translatewiki</b></sub>](https://github.com/translatewiki) | [<img src="https://avatars.githubusercontent.com/u/3127881?v=4" width="100px;"/><br /><sub><b>neslihanturan</b></sub>](https://github.com/neslihanturan) | [<img src="https://avatars.githubusercontent.com/u/30430?v=4" width="100px;"/><br /><sub><b>yuvipanda</b></sub>](https://github.com/yuvipanda) | [<img src="https://avatars.githubusercontent.com/u/99590?v=4" width="100px;"/><br /><sub><b>nicolas-raoul</b></sub>](https://github.com/nicolas-raoul) | | | [<img src="https://avatars.githubusercontent.com/u/3611199?v=4" width="100px;"/><br /><sub><b>misaochan</b></sub>](https://github.com/misaochan) | [<img src="https://avatars.githubusercontent.com/u/24829418?v=4" width="100px;"/><br /><sub><b>translatewiki</b></sub>](https://github.com/translatewiki) | [<img src="https://avatars.githubusercontent.com/u/3127881?v=4" width="100px;"/><br /><sub><b>neslihanturan</b></sub>](https://github.com/neslihanturan) | [<img src="https://avatars.githubusercontent.com/u/30430?v=4" width="100px;"/><br /><sub><b>yuvipanda</b></sub>](https://github.com/yuvipanda) | [<img src="https://avatars.githubusercontent.com/u/99590?v=4" width="100px;"/><br /><sub><b>nicolas-raoul</b></sub>](https://github.com/nicolas-raoul) | | ||||||
| | :---: | :---: | :---: | :---: | :---: | | | :---: | :---: | :---: | :---: | :---: | | ||||||
| | [<img src="https://avatars.githubusercontent.com/u/407647?v=4" width="100px;"/><br /><sub><b>psh</b></sub>](https://github.com/psh) | [<img src="https://avatars.githubusercontent.com/u/4953590?v=4" width="100px;"/><br /><sub><b>domdomegg</b></sub>](https://github.com/domdomegg) | [<img src="https://avatars.githubusercontent.com/u/3069373?v=4" width="100px;"/><br /><sub><b>maskaravivek</b></sub>](https://github.com/maskaravivek) | [<img src="https://avatars.githubusercontent.com/u/30932899?v=4" width="100px;"/><br /><sub><b>madhurgupta10</b></sub>](https://github.com/madhurgupta10) | [<img src="https://avatars.githubusercontent.com/u/17375274?v=4" width="100px;"/><br /><sub><b>ashishkumar468</b></sub>](https://github.com/ashishkumar468) | | | [<img src="https://avatars.githubusercontent.com/u/4953590?v=4" width="100px;"/><br /><sub><b>domdomegg</b></sub>](https://github.com/domdomegg) | [<img src="https://avatars.githubusercontent.com/u/3069373?v=4" width="100px;"/><br /><sub><b>maskaravivek</b></sub>](https://github.com/maskaravivek) | [<img src="https://avatars.githubusercontent.com/u/407647?v=4" width="100px;"/><br /><sub><b>psh</b></sub>](https://github.com/psh) | [<img src="https://avatars.githubusercontent.com/u/103075?v=4" width="100px;"/><br /><sub><b>brion</b></sub>](https://github.com/brion) | [<img src="https://avatars.githubusercontent.com/u/17375274?v=4" width="100px;"/><br /><sub><b>ashishkumar468</b></sub>](https://github.com/ashishkumar468) | | ||||||
| | [<img src="https://avatars.githubusercontent.com/u/103075?v=4" width="100px;"/><br /><sub><b>bvibber</b></sub>](https://github.com/bvibber) | [<img src="https://avatars.githubusercontent.com/u/10674?v=4" width="100px;"/><br /><sub><b>whym</b></sub>](https://github.com/whym) | [<img src="https://avatars.githubusercontent.com/u/10153800?v=4" width="100px;"/><br /><sub><b>akaita</b></sub>](https://github.com/akaita) | [<img src="https://avatars.githubusercontent.com/u/12448084?v=4" width="100px;"/><br /><sub><b>sivaraam</b></sub>](https://github.com/sivaraam) | [<img src="https://avatars.githubusercontent.com/u/6900601?v=4" width="100px;"/><br /><sub><b>veyndan</b></sub>](https://github.com/veyndan) | | | [<img src="https://avatars.githubusercontent.com/u/10674?v=4" width="100px;"/><br /><sub><b>whym</b></sub>](https://github.com/whym) | [<img src="https://avatars.githubusercontent.com/u/10153800?v=4" width="100px;"/><br /><sub><b>akaita</b></sub>](https://github.com/akaita) | [<img src="https://avatars.githubusercontent.com/u/30932899?v=4" width="100px;"/><br /><sub><b>madhurgupta10</b></sub>](https://github.com/madhurgupta10) | [<img src="https://avatars.githubusercontent.com/u/6900601?v=4" width="100px;"/><br /><sub><b>veyndan</b></sub>](https://github.com/veyndan) | [<img src="https://avatars.githubusercontent.com/u/19607555?v=4" width="100px;"/><br /><sub><b>ujjwalagrawal17</b></sub>](https://github.com/ujjwalagrawal17) | | ||||||
| | [<img src="https://avatars.githubusercontent.com/u/19607555?v=4" width="100px;"/><br /><sub><b>ujjwalagrawal17</b></sub>](https://github.com/ujjwalagrawal17) | [<img src="https://avatars.githubusercontent.com/u/3358282?v=4" width="100px;"/><br /><sub><b>macgills</b></sub>](https://github.com/macgills) | [<img src="https://avatars.githubusercontent.com/u/346271?v=4" width="100px;"/><br /><sub><b>amire80</b></sub>](https://github.com/amire80) | [<img src="https://avatars.githubusercontent.com/u/1682214?v=4" width="100px;"/><br /><sub><b>dbrant</b></sub>](https://github.com/dbrant) | [<img src="https://avatars.githubusercontent.com/u/34261945?v=4" width="100px;"/><br /><sub><b>vanshikaarora</b></sub>](https://github.com/vanshikaarora) | | | [<img src="https://avatars.githubusercontent.com/u/3358282?v=4" width="100px;"/><br /><sub><b>macgills</b></sub>](https://github.com/macgills) | [<img src="https://avatars.githubusercontent.com/u/1682214?v=4" width="100px;"/><br /><sub><b>dbrant</b></sub>](https://github.com/dbrant) | [<img src="https://avatars.githubusercontent.com/u/34261945?v=4" width="100px;"/><br /><sub><b>vanshikaarora</b></sub>](https://github.com/vanshikaarora) | [<img src="https://avatars.githubusercontent.com/u/1345681?v=4" width="100px;"/><br /><sub><b>sandarumk</b></sub>](https://github.com/sandarumk) | [<img src="https://avatars.githubusercontent.com/u/29161745?v=4" width="100px;"/><br /><sub><b>tanvidadu</b></sub>](https://github.com/tanvidadu) | | ||||||
| | [<img src="https://avatars.githubusercontent.com/u/83745993?v=4" width="100px;"/><br /><sub><b>RitikaPahwa4444</b></sub>](https://github.com/RitikaPahwa4444) | [<img src="https://avatars.githubusercontent.com/u/71203077?v=4" width="100px;"/><br /><sub><b>Ayan-10</b></sub>](https://github.com/Ayan-10) | [<img src="https://avatars.githubusercontent.com/u/101377978?v=4" width="100px;"/><br /><sub><b>rohit9625</b></sub>](https://github.com/rohit9625) | [<img src="https://avatars.githubusercontent.com/u/126143257?v=4" width="100px;"/><br /><sub><b>shashankiitbhu</b></sub>](https://github.com/shashankiitbhu) | [<img src="https://avatars.githubusercontent.com/u/54663429?v=4" width="100px;"/><br /><sub><b>Pratham2305</b></sub>](https://github.com/Pratham2305) | | | [<img src="https://avatars.githubusercontent.com/u/39745544?v=4" width="100px;"/><br /><sub><b>cypherop</b></sub>](https://github.com/cypherop) | [<img src="https://avatars.githubusercontent.com/u/6953323?v=4" width="100px;"/><br /><sub><b>tobias47n9e</b></sub>](https://github.com/tobias47n9e) | [<img src="https://avatars.githubusercontent.com/u/25305892?v=4" width="100px;"/><br /><sub><b>hismaeel</b></sub>](https://github.com/hismaeel) | [<img src="https://avatars.githubusercontent.com/u/12574756?v=4" width="100px;"/><br /><sub><b>tshradheya</b></sub>](https://github.com/tshradheya) | [<img src="https://avatars.githubusercontent.com/u/3308769?v=4" width="100px;"/><br /><sub><b>addshore</b></sub>](https://github.com/addshore) | | ||||||
| | [<img src="https://avatars.githubusercontent.com/u/111801812?v=4" width="100px;"/><br /><sub><b>parneet-guraya</b></sub>](https://github.com/parneet-guraya) | [<img src="https://avatars.githubusercontent.com/u/1345681?v=4" width="100px;"/><br /><sub><b>sandarumk</b></sub>](https://github.com/sandarumk) | [<img src="https://avatars.githubusercontent.com/u/29161745?v=4" width="100px;"/><br /><sub><b>tanvidadu</b></sub>](https://github.com/tanvidadu) | [<img src="https://avatars.githubusercontent.com/u/39745544?v=4" width="100px;"/><br /><sub><b>cypherop</b></sub>](https://github.com/cypherop) | [<img src="https://avatars.githubusercontent.com/u/65972015?v=4" width="100px;"/><br /><sub><b>Prince-kushwaha</b></sub>](https://github.com/Prince-kushwaha) | | | [<img src="https://avatars.githubusercontent.com/u/20313518?v=4" width="100px;"/><br /><sub><b>knight-shade</b></sub>](https://github.com/knight-shade) | [<img src="https://avatars.githubusercontent.com/u/210297?v=4" width="100px;"/><br /><sub><b>siebrand</b></sub>](https://github.com/siebrand) | [<img src="https://avatars.githubusercontent.com/u/12448084?v=4" width="100px;"/><br /><sub><b>sivaraam</b></sub>](https://github.com/sivaraam) | [<img src="https://avatars.githubusercontent.com/u/5329780?v=4" width="100px;"/><br /><sub><b>Bluesir9</b></sub>](https://github.com/Bluesir9) | [<img src="https://avatars.githubusercontent.com/u/44129798?v=4" width="100px;"/><br /><sub><b>kbhardwaj123</b></sub>](https://github.com/kbhardwaj123) | | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| .. and [many more](https://github.com/commons-app/apps-android-commons/graphs/contributors). | .. and [many more](https://github.com/commons-app/apps-android-commons/graphs/contributors). | ||||||
|  | @ -46,7 +45,7 @@ This software is open source, licensed under the [Apache License 2.0][10]. | ||||||
| 
 | 
 | ||||||
| [1]: https://play.google.com/store/apps/details?id=fr.free.nrw.commons | [1]: https://play.google.com/store/apps/details?id=fr.free.nrw.commons | ||||||
| [2]: https://commons-app.github.io/ | [2]: https://commons-app.github.io/ | ||||||
| [3]: https://github.com/commons-app/apps-android-commons/issues?q=is%3Aopen+is%3Aissue+no%3Aassignee+-label%3Adebated+label%3Abug+-label%3A%22low+priority%22+-label%3Aupstream | [3]: https://github.com/commons-app/apps-android-commons/issues | ||||||
| 
 | 
 | ||||||
| [4]: https://github.com/commons-app/commons-app-documentation/blob/master/android/README.md#-android-documentation | [4]: https://github.com/commons-app/commons-app-documentation/blob/master/android/README.md#-android-documentation | ||||||
| [5]: https://github.com/commons-app/commons-app-documentation/blob/master/android/README.md#-user-documentation | [5]: https://github.com/commons-app/commons-app-documentation/blob/master/android/README.md#-user-documentation | ||||||
|  |  | ||||||
							
								
								
									
										375
									
								
								app/build.gradle
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										375
									
								
								app/build.gradle
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,375 @@ | ||||||
|  | plugins { | ||||||
|  |     id 'com.github.triplet.play' version '2.7.2' apply false | ||||||
|  | } | ||||||
|  | apply from: '../gitutils.gradle' | ||||||
|  | apply plugin: 'com.android.application' | ||||||
|  | apply plugin: 'kotlin-android' | ||||||
|  | apply plugin: 'kotlin-kapt' | ||||||
|  | apply plugin: 'kotlin-android-extensions' | ||||||
|  | apply from: "$rootDir/jacoco.gradle" | ||||||
|  | 
 | ||||||
|  | def isRunningOnTravisAndIsNotPRBuild = System.getenv("CI") == "true" && file('../play.p12').exists() | ||||||
|  | 
 | ||||||
|  | if (isRunningOnTravisAndIsNotPRBuild) { | ||||||
|  |     apply plugin: 'com.github.triplet.play' | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | dependencies { | ||||||
|  | 
 | ||||||
|  |     implementation project(':wikimedia-data-client') | ||||||
|  |     // Utils | ||||||
|  |     implementation 'in.yuvi:http.fluent:1.3' | ||||||
|  |     implementation 'com.google.code.gson:gson:2.8.5' | ||||||
|  |     implementation ("com.squareup.okhttp3:okhttp:$OKHTTP_VERSION"){ | ||||||
|  |         force = true //API 19 support | ||||||
|  |     } | ||||||
|  |     implementation 'com.squareup.okio:okio:2.2.2' | ||||||
|  |     implementation 'io.reactivex.rxjava2:rxandroid:2.1.0' | ||||||
|  |     implementation 'io.reactivex.rxjava2:rxjava:2.2.3' | ||||||
|  |     implementation 'com.jakewharton.rxbinding2:rxbinding:2.1.1' | ||||||
|  |     implementation 'com.jakewharton.rxbinding3:rxbinding-appcompat:3.0.0' | ||||||
|  |     implementation 'com.jakewharton.rxbinding2:rxbinding-support-v4:2.1.1' | ||||||
|  |     implementation 'com.jakewharton.rxbinding2:rxbinding-appcompat-v7:2.1.1' | ||||||
|  |     implementation 'com.jakewharton.rxbinding2:rxbinding-design:2.1.1' | ||||||
|  |     implementation 'com.facebook.fresco:fresco:1.13.0' | ||||||
|  |     implementation 'org.apache.commons:commons-lang3:3.8.1' | ||||||
|  | 
 | ||||||
|  |     // UI | ||||||
|  |     implementation 'fr.avianey.com.viewpagerindicator:library:2.4.1.1@aar' | ||||||
|  |     implementation 'com.github.chrisbanes:PhotoView:2.0.0' | ||||||
|  |     implementation 'com.github.pedrovgs:renderers:3.3.3' | ||||||
|  |     implementation 'com.mapbox.mapboxsdk:mapbox-android-sdk:9.1.0' | ||||||
|  |     implementation 'com.mapbox.mapboxsdk:mapbox-android-plugin-localization-v8:0.11.0' | ||||||
|  |     implementation 'com.mapbox.mapboxsdk:mapbox-android-plugin-scalebar-v9:0.4.0' | ||||||
|  |     implementation 'com.mapbox.mapboxsdk:mapbox-android-telemetry:6.1.0' | ||||||
|  |     implementation 'com.github.deano2390:MaterialShowcaseView:1.2.0' | ||||||
|  |     implementation 'com.dinuscxj:circleprogressbar:1.1.1' | ||||||
|  |     implementation 'com.karumi:dexter:5.0.0' | ||||||
|  |     implementation "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION" | ||||||
|  |     implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' | ||||||
|  | 
 | ||||||
|  |     kapt "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION" | ||||||
|  |     implementation "com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:$ADAPTER_DELEGATES_VERSION" | ||||||
|  |     implementation "com.hannesdorfmann:adapterdelegates4-pagination:$ADAPTER_DELEGATES_VERSION" | ||||||
|  |     implementation "androidx.paging:paging-runtime-ktx:$PAGING_VERSION" | ||||||
|  |     testImplementation "androidx.paging:paging-common-ktx:$PAGING_VERSION" | ||||||
|  |     implementation "androidx.paging:paging-rxjava2-ktx:$PAGING_VERSION" | ||||||
|  |     implementation "androidx.recyclerview:recyclerview:1.2.0-alpha02" | ||||||
|  |     implementation "com.squareup.okhttp3:okhttp-ws:$OKHTTP_VERSION" | ||||||
|  | 
 | ||||||
|  |     // Logging | ||||||
|  |     implementation 'ch.acra:acra-dialog:5.8.4' | ||||||
|  |     implementation 'ch.acra:acra-mail:5.8.4' | ||||||
|  |     implementation 'org.slf4j:slf4j-api:1.7.25' | ||||||
|  |     api('com.github.tony19:logback-android-classic:1.1.1-6') { | ||||||
|  |         exclude group: 'com.google.android', module: 'android' | ||||||
|  |     } | ||||||
|  |     implementation "com.squareup.okhttp3:logging-interceptor:$OKHTTP_VERSION" | ||||||
|  | 
 | ||||||
|  |     // Dependency injector | ||||||
|  |     implementation "com.google.dagger:dagger-android:$DAGGER_VERSION" | ||||||
|  |     implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION" | ||||||
|  |     kapt "com.google.dagger:dagger-android-processor:$DAGGER_VERSION" | ||||||
|  |     kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION" | ||||||
|  |     annotationProcessor "com.google.dagger:dagger-android-processor:$DAGGER_VERSION" | ||||||
|  | 
 | ||||||
|  |     implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$KOTLIN_VERSION" | ||||||
|  |     implementation "org.jetbrains.kotlin:kotlin-reflect:$KOTLIN_VERSION" | ||||||
|  | 
 | ||||||
|  |     //Mocking | ||||||
|  |     testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0' | ||||||
|  |     testImplementation 'org.mockito:mockito-inline:2.13.0' | ||||||
|  |     testImplementation 'org.mockito:mockito-core:2.25.1' | ||||||
|  |     testImplementation "org.powermock:powermock-module-junit4:2.0.2" | ||||||
|  |     testImplementation "org.powermock:powermock-api-mockito2:2.0.2" | ||||||
|  | 
 | ||||||
|  |     // Unit testing | ||||||
|  |     testImplementation 'junit:junit:4.13.2' | ||||||
|  |     testImplementation 'org.robolectric:robolectric:4.6-alpha-1' | ||||||
|  |     testImplementation 'androidx.test:core:1.4.0' | ||||||
|  |     testImplementation "com.squareup.okhttp3:mockwebserver:$OKHTTP_VERSION" | ||||||
|  |     testImplementation "com.jraska.livedata:testing-ktx:1.1.2" | ||||||
|  |     testImplementation "androidx.arch.core:core-testing:2.1.0" | ||||||
|  |     testImplementation "org.junit.jupiter:junit-jupiter-api:5.7.0" | ||||||
|  |     testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.7.0" | ||||||
|  |     testImplementation 'com.facebook.soloader:soloader:0.10.1' | ||||||
|  |     testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.0" | ||||||
|  | 
 | ||||||
|  |     // Android testing | ||||||
|  |     androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$KOTLIN_VERSION" | ||||||
|  |     androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0-alpha04' | ||||||
|  |     androidTestImplementation 'androidx.test.espresso:espresso-intents:3.4.0' | ||||||
|  |     androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.5.0-alpha04' | ||||||
|  |     androidTestImplementation 'androidx.test:runner:1.4.0' | ||||||
|  |     androidTestImplementation 'androidx.test:rules:1.4.1-alpha04' | ||||||
|  |     androidTestImplementation 'androidx.test:core:1.4.0' | ||||||
|  |     androidTestImplementation 'androidx.test.ext:junit:1.1.3' | ||||||
|  |     androidTestImplementation 'androidx.annotation:annotation:1.3.0' | ||||||
|  |     androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.8.0' | ||||||
|  |     androidTestImplementation "androidx.test.uiautomator:uiautomator:2.2.0" | ||||||
|  |     androidTestUtil 'androidx.test:orchestrator:1.4.1' | ||||||
|  | 
 | ||||||
|  |     // Debugging | ||||||
|  |     debugImplementation "com.squareup.leakcanary:leakcanary-android:$LEAK_CANARY_VERSION" | ||||||
|  |     releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY_VERSION" | ||||||
|  |     testImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY_VERSION" | ||||||
|  | 
 | ||||||
|  |     // Support libraries | ||||||
|  |     implementation "com.google.android.material:material:1.1.0-alpha04" | ||||||
|  |     implementation "androidx.browser:browser:1.3.0" | ||||||
|  |     implementation "androidx.cardview:cardview:1.0.0" | ||||||
|  |     implementation 'androidx.constraintlayout:constraintlayout:1.1.3' | ||||||
|  |     implementation "androidx.exifinterface:exifinterface:1.3.2" | ||||||
|  |     implementation "androidx.core:core-ktx:$CORE_KTX_VERSION" | ||||||
|  |     implementation "androidx.multidex:multidex:2.0.1" | ||||||
|  | 
 | ||||||
|  |     //swipe_layout | ||||||
|  |     implementation 'com.daimajia.swipelayout:library:1.2.0@aar' | ||||||
|  | 
 | ||||||
|  |     //Room | ||||||
|  |     implementation "androidx.room:room-runtime:$ROOM_VERSION" | ||||||
|  |     implementation "androidx.room:room-ktx:$ROOM_VERSION" | ||||||
|  |     implementation "androidx.room:room-rxjava2:$ROOM_VERSION" | ||||||
|  |     kapt "androidx.room:room-compiler:$ROOM_VERSION" | ||||||
|  |     // For Kotlin use kapt instead of annotationProcessor | ||||||
|  |     implementation 'com.squareup.retrofit2:retrofit:2.8.1' | ||||||
|  |     testImplementation "androidx.arch.core:core-testing:2.1.0" | ||||||
|  | 
 | ||||||
|  |     // Pref | ||||||
|  |     // Java language implementation | ||||||
|  |     implementation "androidx.preference:preference:$PREFERENCE_VERSION" | ||||||
|  |     // Kotlin | ||||||
|  |     implementation "androidx.preference:preference-ktx:$PREFERENCE_VERSION" | ||||||
|  | 
 | ||||||
|  |     implementation "androidx.multidex:multidex:$MULTIDEX_VERSION" | ||||||
|  | 
 | ||||||
|  |     def work_version = "2.6.0" | ||||||
|  |     // Kotlin + coroutines | ||||||
|  |     implementation "androidx.work:work-runtime-ktx:$work_version" | ||||||
|  |     testImplementation "androidx.work:work-testing:$work_version" | ||||||
|  | 
 | ||||||
|  |     //Glide | ||||||
|  |     implementation 'com.github.bumptech.glide:glide:4.12.0' | ||||||
|  |     annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0' | ||||||
|  | 
 | ||||||
|  |     implementation("io.github.coordinates2country:coordinates2country-android:1.3") {  exclude group: 'com.google.android', module: 'android' } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | task disableAnimations(type: Exec) { | ||||||
|  |     def adb = "$System.env.ANDROID_HOME/platform-tools/adb" | ||||||
|  |     commandLine "$adb", 'shell', 'settings', 'put', 'global', 'window_animation_scale', '0' | ||||||
|  |     commandLine "$adb", 'shell', 'settings', 'put', 'global', 'transition_animation_scale', '0' | ||||||
|  |     commandLine "$adb", 'shell', 'settings', 'put', 'global', 'animator_duration_scale', '0' | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | project.gradle.taskGraph.whenReady { | ||||||
|  |     connectedBetaDebugAndroidTest.dependsOn disableAnimations | ||||||
|  |     connectedProdDebugAndroidTest.dependsOn disableAnimations | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | android { | ||||||
|  |     compileSdkVersion 30 | ||||||
|  | 
 | ||||||
|  |     defaultConfig { | ||||||
|  |         //applicationId 'fr.free.nrw.commons' | ||||||
|  | 
 | ||||||
|  |         versionCode 1026 | ||||||
|  |         versionName '4.0.0' | ||||||
|  |         setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName()) | ||||||
|  | 
 | ||||||
|  |         minSdkVersion 19 | ||||||
|  |         targetSdkVersion 30 | ||||||
|  |         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" | ||||||
|  |         testInstrumentationRunnerArguments clearPackageData: 'true' | ||||||
|  | 
 | ||||||
|  |         multiDexEnabled true | ||||||
|  | 
 | ||||||
|  |         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" | ||||||
|  | 
 | ||||||
|  |         vectorDrawables.useSupportLibrary = true | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     packagingOptions { | ||||||
|  |         exclude 'META-INF/androidx.*' | ||||||
|  |         exclude 'META-INF/proguard/androidx-annotations.pro' | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     testOptions { | ||||||
|  |         animationsDisabled true | ||||||
|  | 
 | ||||||
|  |         unitTests.returnDefaultValues = true | ||||||
|  |         unitTests.includeAndroidResources = true | ||||||
|  | 
 | ||||||
|  |         unitTests.all { | ||||||
|  |             jvmArgs '-noverify' | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     sourceSets { | ||||||
|  |         // use kotlin only in tests (for now) | ||||||
|  |         test.java.srcDirs += 'src/test/kotlin' | ||||||
|  | 
 | ||||||
|  |         // use main assets and resources in test | ||||||
|  |         test.assets.srcDirs += 'src/main/assets' | ||||||
|  |         test.resources.srcDirs += 'src/main/resoures' | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     signingConfigs { | ||||||
|  |         release | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     buildTypes { | ||||||
|  |         release { | ||||||
|  |             minifyEnabled true | ||||||
|  |             proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' | ||||||
|  |             testProguardFile 'test-proguard-rules.txt' | ||||||
|  |             if (isRunningOnTravisAndIsNotPRBuild) { | ||||||
|  |                 signingConfig signingConfigs.release | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         debug { | ||||||
|  |             minifyEnabled false | ||||||
|  |             testCoverageEnabled true | ||||||
|  |             proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' | ||||||
|  |             testProguardFile 'test-proguard-rules.txt' | ||||||
|  |             versionNameSuffix "-debug-" + getBranchName() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (isRunningOnTravisAndIsNotPRBuild) { | ||||||
|  |         // configure keystore based on env vars in Travis for automated alpha builds | ||||||
|  |         signingConfigs.release.storeFile = file("../nr-commons.keystore") | ||||||
|  |         signingConfigs.release.storePassword = System.getenv("keystore_password") | ||||||
|  |         signingConfigs.release.keyAlias = System.getenv("key_alias") | ||||||
|  |         signingConfigs.release.keyPassword = System.getenv("key_password") | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     configurations.all { | ||||||
|  |         resolutionStrategy.force 'androidx.annotation:annotation:1.1.0' | ||||||
|  |         exclude module: 'okhttp-ws' | ||||||
|  |     } | ||||||
|  |     flavorDimensions 'tier' | ||||||
|  |     productFlavors { | ||||||
|  |         prod { | ||||||
|  | 
 | ||||||
|  |             applicationId 'fr.free.nrw.commons' | ||||||
|  | 
 | ||||||
|  |             buildConfigField "String", "WIKIMEDIA_API_POTD", "\"https://commons.wikimedia.org/w/api.php?action=featuredfeed&feed=potd&feedformat=rss&language=en\"" | ||||||
|  |             buildConfigField "String", "WIKIMEDIA_API_HOST", "\"https://commons.wikimedia.org/w/api.php\"" | ||||||
|  |             buildConfigField "String", "WIKIDATA_API_HOST", "\"https://www.wikidata.org/w/api.php\"" | ||||||
|  |             buildConfigField "String", "WIKIDATA_URL", "\"https://www.wikidata.org\"" | ||||||
|  |             buildConfigField "String", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\"" | ||||||
|  |             buildConfigField "String", "WIKIMEDIA_CAMPAIGNS_URL", "\"https://raw.githubusercontent.com/commons-app/campaigns/master/campaigns.json\"" | ||||||
|  |             buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.wikimedia.org/wikipedia/commons\"" | ||||||
|  |             buildConfigField "String", "HOME_URL", "\"https://commons.wikimedia.org/wiki/\"" | ||||||
|  |             buildConfigField "String", "COMMONS_URL", "\"https://commons.wikimedia.org\"" | ||||||
|  |             buildConfigField "String", "WIKIDATA_URL", "\"https://www.wikidata.org\"" | ||||||
|  |             buildConfigField "String", "MOBILE_HOME_URL", "\"https://commons.m.wikimedia.org/wiki/\"" | ||||||
|  |             buildConfigField "String", "SIGNUP_LANDING_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Special:CreateAccount&returnto=Main+Page&returntoquery=welcome%3Dyes\"" | ||||||
|  |             buildConfigField "String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Main_Page&welcome=yes\"" | ||||||
|  |             buildConfigField "String", "FORGOT_PASSWORD_URL", "\"https://commons.wikimedia.org/wiki/Special:PasswordReset\"" | ||||||
|  |             buildConfigField "String", "PRIVACY_POLICY_URL", "\"https://github.com/commons-app/commons-app-documentation/blob/master/android/Privacy-policy.md\"" | ||||||
|  |             buildConfigField "String", "ACCOUNT_TYPE", "\"fr.free.nrw.commons\"" | ||||||
|  |             buildConfigField "String", "CONTRIBUTION_AUTHORITY", "\"fr.free.nrw.commons.contributions.contentprovider\"" | ||||||
|  |             buildConfigField "String", "MODIFICATION_AUTHORITY", "\"fr.free.nrw.commons.modifications.contentprovider\"" | ||||||
|  |             buildConfigField "String", "CATEGORY_AUTHORITY", "\"fr.free.nrw.commons.categories.contentprovider\"" | ||||||
|  |             buildConfigField "String", "RECENT_SEARCH_AUTHORITY", "\"fr.free.nrw.commons.explore.recentsearches.contentprovider\"" | ||||||
|  |             buildConfigField "String", "RECENT_LANGUAGE_AUTHORITY", "\"fr.free.nrw.commons.recentlanguages.contentprovider\"" | ||||||
|  |             buildConfigField "String", "BOOKMARK_AUTHORITY", "\"fr.free.nrw.commons.bookmarks.contentprovider\"" | ||||||
|  |             buildConfigField "String", "BOOKMARK_LOCATIONS_AUTHORITY", "\"fr.free.nrw.commons.bookmarks.locations.contentprovider\"" | ||||||
|  |             buildConfigField "String", "BOOKMARK_ITEMS_AUTHORITY", "\"fr.free.nrw.commons.bookmarks.items.contentprovider\"" | ||||||
|  |             buildConfigField "String", "COMMIT_SHA", "\"" + getBuildVersion().toString() + "\"" | ||||||
|  |             buildConfigField "String", "TEST_USERNAME", "\"" + getTestUserName() + "\"" | ||||||
|  |             buildConfigField "String", "TEST_PASSWORD", "\"" + getTestPassword() + "\"" | ||||||
|  |             buildConfigField "String", "DEPICTS_PROPERTY", "\"P180\"" | ||||||
|  | 
 | ||||||
|  |             dimension 'tier' | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         beta { | ||||||
|  |             applicationId 'fr.free.nrw.commons.beta' | ||||||
|  | 
 | ||||||
|  |             // What values do we need to hit the BETA versions of the site / api ? | ||||||
|  |             buildConfigField "String", "WIKIMEDIA_API_POTD", "\"https://commons.wikimedia.org/w/api.php?action=featuredfeed&feed=potd&feedformat=rss&language=en\"" | ||||||
|  |             buildConfigField "String", "WIKIMEDIA_API_HOST", "\"https://commons.wikimedia.beta.wmflabs.org/w/api.php\"" | ||||||
|  |             buildConfigField "String", "WIKIDATA_API_HOST", "\"https://www.wikidata.org/w/api.php\"" | ||||||
|  |             buildConfigField "String", "WIKIDATA_URL", "\"https://www.wikidata.org\"" | ||||||
|  |             buildConfigField "String", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\"" | ||||||
|  |             buildConfigField "String", "WIKIMEDIA_CAMPAIGNS_URL", "\"https://raw.githubusercontent.com/commons-app/campaigns/master/campaigns_beta_active.json\"" | ||||||
|  |             buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.beta.wmflabs.org/wikipedia/commons\"" | ||||||
|  |             buildConfigField "String", "HOME_URL", "\"https://commons.wikimedia.beta.wmflabs.org/wiki/\"" | ||||||
|  |             buildConfigField "String", "COMMONS_URL", "\"https://commons.wikimedia.beta.wmflabs.org\"" | ||||||
|  |             buildConfigField "String", "WIKIDATA_URL", "\"https://www.wikidata.org\"" | ||||||
|  |             buildConfigField "String", "MOBILE_HOME_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/wiki/\"" | ||||||
|  |             buildConfigField "String", "SIGNUP_LANDING_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Special:CreateAccount&returnto=Main+Page&returntoquery=welcome%3Dyes\"" | ||||||
|  |             buildConfigField "String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Main_Page&welcome=yes\"" | ||||||
|  |             buildConfigField "String", "FORGOT_PASSWORD_URL", "\"https://commons.wikimedia.beta.wmflabs.org/wiki/Special:PasswordReset\"" | ||||||
|  |             buildConfigField "String", "PRIVACY_POLICY_URL", "\"https://github.com/commons-app/commons-app-documentation/blob/master/android/Privacy-policy.md\"" | ||||||
|  |             buildConfigField "String", "ACCOUNT_TYPE", "\"fr.free.nrw.commons.beta\"" | ||||||
|  |             buildConfigField "String", "CONTRIBUTION_AUTHORITY", "\"fr.free.nrw.commons.beta.contributions.contentprovider\"" | ||||||
|  |             buildConfigField "String", "MODIFICATION_AUTHORITY", "\"fr.free.nrw.commons.beta.modifications.contentprovider\"" | ||||||
|  |             buildConfigField "String", "CATEGORY_AUTHORITY", "\"fr.free.nrw.commons.beta.categories.contentprovider\"" | ||||||
|  |             buildConfigField "String", "RECENT_SEARCH_AUTHORITY", "\"fr.free.nrw.commons.beta.explore.recentsearches.contentprovider\"" | ||||||
|  |             buildConfigField "String", "RECENT_LANGUAGE_AUTHORITY", "\"fr.free.nrw.commons.beta.recentlanguages.contentprovider\"" | ||||||
|  |             buildConfigField "String", "BOOKMARK_AUTHORITY", "\"fr.free.nrw.commons.beta.bookmarks.contentprovider\"" | ||||||
|  |             buildConfigField "String", "BOOKMARK_LOCATIONS_AUTHORITY", "\"fr.free.nrw.commons.beta.bookmarks.locations.contentprovider\"" | ||||||
|  |             buildConfigField "String", "BOOKMARK_ITEMS_AUTHORITY", "\"fr.free.nrw.commons.beta.bookmarks.items.contentprovider\"" | ||||||
|  |             buildConfigField "String", "COMMIT_SHA", "\"" + getBuildVersion().toString() + "\"" | ||||||
|  |             buildConfigField "String", "TEST_USERNAME", "\"" + getTestUserName() + "\"" | ||||||
|  |             buildConfigField "String", "TEST_PASSWORD", "\"" + getTestPassword() + "\"" | ||||||
|  |             buildConfigField "String", "DEPICTS_PROPERTY", "\"P245962\"" | ||||||
|  | 
 | ||||||
|  |             dimension 'tier' | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     lintOptions { | ||||||
|  |         disable 'MissingTranslation' | ||||||
|  |         disable 'ExtraTranslation' | ||||||
|  |         abortOnError false | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     compileOptions { | ||||||
|  |         sourceCompatibility JavaVersion.VERSION_1_8 | ||||||
|  |         targetCompatibility JavaVersion.VERSION_1_8 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     buildToolsVersion buildToolsVersion | ||||||
|  | 
 | ||||||
|  |     buildFeatures { | ||||||
|  |         viewBinding true | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | String getTestUserName() { | ||||||
|  |     def propFile = rootProject.file("./local.properties") | ||||||
|  |     def properties = new Properties() | ||||||
|  |     properties.load(new FileInputStream(propFile)) | ||||||
|  |     return properties['TEST_USER_NAME'] | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | String getTestPassword() { | ||||||
|  |     def propFile = rootProject.file("./local.properties") | ||||||
|  |     def properties = new Properties() | ||||||
|  |     properties.load(new FileInputStream(propFile)) | ||||||
|  |     return properties['TEST_USER_PASSWORD'] | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | if (isRunningOnTravisAndIsNotPRBuild) { | ||||||
|  |     play { | ||||||
|  |         track = "alpha" | ||||||
|  |         userFraction = 1 | ||||||
|  |         serviceAccountEmail = System.getenv("SERVICE_ACCOUNT_NAME") | ||||||
|  |         serviceAccountCredentials = file("../play.p12") | ||||||
|  | 
 | ||||||
|  |         resolutionStrategy = "auto" | ||||||
|  |         outputProcessor { // this: ApkVariantOutput | ||||||
|  |             versionNameOverride = "$versionNameOverride.$versionCode" | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | androidExtensions { | ||||||
|  |     experimental = true | ||||||
|  | } | ||||||
|  | @ -1,447 +0,0 @@ | ||||||
| import java.util.Properties |  | ||||||
| import java.io.ByteArrayOutputStream |  | ||||||
| 
 |  | ||||||
| plugins { |  | ||||||
|     alias(libs.plugins.android.application) |  | ||||||
|     alias(libs.plugins.jetbrains.kotlin.android) |  | ||||||
|     alias(libs.plugins.kotlin.kapt) |  | ||||||
|     alias(libs.plugins.kotlin.parcelize) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| apply(from = "$rootDir/jacoco.gradle") |  | ||||||
| 
 |  | ||||||
| val isRunningOnTravisAndIsNotPRBuild = System.getenv("CI") == "true" && file("../play.p12").exists() |  | ||||||
| 
 |  | ||||||
| if (isRunningOnTravisAndIsNotPRBuild) { |  | ||||||
|     apply(plugin = "com.github.triplet.play") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| android { |  | ||||||
|     namespace = "fr.free.nrw.commons" |  | ||||||
|     compileSdk = 35 |  | ||||||
| 
 |  | ||||||
|     defaultConfig { |  | ||||||
|         applicationId = "fr.free.nrw.commons" |  | ||||||
|         minSdk = 21 |  | ||||||
|         targetSdk = 35 |  | ||||||
|         versionCode = 1059 |  | ||||||
|         versionName = "6.1.0" |  | ||||||
| 
 |  | ||||||
|         setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName()) |  | ||||||
|         testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" |  | ||||||
|         testInstrumentationRunnerArguments["clearPackageData"] = "true" |  | ||||||
| 
 |  | ||||||
|         multiDexEnabled = true |  | ||||||
| 
 |  | ||||||
|         vectorDrawables { |  | ||||||
|             useSupportLibrary = true |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     sourceSets { |  | ||||||
|         getByName("test") { |  | ||||||
|             // Use kotlin only in tests (for now) |  | ||||||
|             java.srcDirs("src/test/kotlin") |  | ||||||
| 
 |  | ||||||
|             // Use main assets and resources in test |  | ||||||
|             assets.srcDirs("src/main/assets") |  | ||||||
|             resources.srcDirs("src/main/resources") |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     signingConfigs { |  | ||||||
|         create("release") { |  | ||||||
|             // Configure keystore based on env vars in Travis for automated alpha builds |  | ||||||
|             if(isRunningOnTravisAndIsNotPRBuild) { |  | ||||||
|                 storeFile = file("../nr-commons.keystore") |  | ||||||
|                 storePassword = System.getenv("keystore_password") |  | ||||||
|                 keyAlias = System.getenv("key_alias") |  | ||||||
|                 keyPassword = System.getenv("key_password") |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     buildTypes { |  | ||||||
|         release { |  | ||||||
|             isMinifyEnabled = true |  | ||||||
|             proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.txt") |  | ||||||
|             testProguardFile("test-proguard-rules.txt") |  | ||||||
| 
 |  | ||||||
|             signingConfig = signingConfigs.getByName("debug") |  | ||||||
|             if (isRunningOnTravisAndIsNotPRBuild) { |  | ||||||
|                 signingConfig = signingConfigs.getByName("release") |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         debug { |  | ||||||
|             isMinifyEnabled = false |  | ||||||
|             proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.txt") |  | ||||||
|             testProguardFile("test-proguard-rules.txt") |  | ||||||
| 
 |  | ||||||
|             versionNameSuffix = "-debug-" + getBranchName() |  | ||||||
|             enableUnitTestCoverage = true |  | ||||||
|             enableAndroidTestCoverage = true |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     configurations.all { |  | ||||||
|         resolutionStrategy { |  | ||||||
|             force("androidx.annotation:annotation:1.1.0") |  | ||||||
|             force("com.jakewharton.timber:timber:4.7.1") |  | ||||||
|             force("androidx.fragment:fragment:1.3.6") |  | ||||||
|         } |  | ||||||
|         exclude(module = "okhttp-ws") |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     flavorDimensions += "tier" |  | ||||||
|     productFlavors { |  | ||||||
|         create("prod") { |  | ||||||
|             dimension = "tier" |  | ||||||
|             applicationId = "fr.free.nrw.commons" |  | ||||||
| 
 |  | ||||||
|             buildConfigField("String", "WIKIMEDIA_API_POTD", "\"https://commons.wikimedia.org/w/api.php?action=featuredfeed&feed=potd&feedformat=rss&language=en\"") |  | ||||||
|             buildConfigField("String", "WIKIMEDIA_API_HOST", "\"https://commons.wikimedia.org/w/api.php\"") |  | ||||||
|             buildConfigField("String", "WIKIDATA_API_HOST", "\"https://www.wikidata.org/w/api.php\"") |  | ||||||
|             buildConfigField("String", "WIKIDATA_URL", "\"https://www.wikidata.org\"") |  | ||||||
|             buildConfigField("String", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\"") |  | ||||||
|             buildConfigField("String", "WIKIMEDIA_CAMPAIGNS_URL", "\"https://raw.githubusercontent.com/commons-app/campaigns/master/campaigns.json\"") |  | ||||||
|             buildConfigField("String", "IMAGE_URL_BASE", "\"https://upload.wikimedia.org/wikipedia/commons\"") |  | ||||||
|             buildConfigField("String", "HOME_URL", "\"https://commons.wikimedia.org/wiki/\"") |  | ||||||
|             buildConfigField("String", "COMMONS_URL", "\"https://commons.wikimedia.org\"") |  | ||||||
|             buildConfigField("String", "WIKIDATA_URL", "\"https://www.wikidata.org\"") |  | ||||||
|             buildConfigField("String", "MOBILE_HOME_URL", "\"https://commons.m.wikimedia.org/wiki/\"") |  | ||||||
|             buildConfigField("String", "MOBILE_META_URL", "\"https://meta.m.wikimedia.org/wiki/\"") |  | ||||||
|             buildConfigField("String", "SIGNUP_LANDING_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Special:CreateAccount&returnto=Main+Page&returntoquery=welcome%3Dyes\"") |  | ||||||
|             buildConfigField("String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Main_Page&welcome=yes\"") |  | ||||||
|             buildConfigField("String", "FORGOT_PASSWORD_URL", "\"https://commons.wikimedia.org/wiki/Special:PasswordReset\"") |  | ||||||
|             buildConfigField("String", "PRIVACY_POLICY_URL", "\"https://github.com/commons-app/commons-app-documentation/blob/master/android/Privacy-policy.md\"") |  | ||||||
|             buildConfigField("String", "FILE_USAGES_BASE_URL", "\"https://commons.wikimedia.org/w/api.php?action=query&format=json&formatversion=2\"") |  | ||||||
|             buildConfigField("String", "ACCOUNT_TYPE", "\"fr.free.nrw.commons\"") |  | ||||||
|             buildConfigField("String", "CONTRIBUTION_AUTHORITY", "\"fr.free.nrw.commons.contributions.contentprovider\"") |  | ||||||
|             buildConfigField("String", "MODIFICATION_AUTHORITY", "\"fr.free.nrw.commons.modifications.contentprovider\"") |  | ||||||
|             buildConfigField("String", "CATEGORY_AUTHORITY", "\"fr.free.nrw.commons.categories.contentprovider\"") |  | ||||||
|             buildConfigField("String", "RECENT_SEARCH_AUTHORITY", "\"fr.free.nrw.commons.explore.recentsearches.contentprovider\"") |  | ||||||
|             buildConfigField("String", "RECENT_LANGUAGE_AUTHORITY", "\"fr.free.nrw.commons.recentlanguages.contentprovider\"") |  | ||||||
|             buildConfigField("String", "BOOKMARK_AUTHORITY", "\"fr.free.nrw.commons.bookmarks.contentprovider\"") |  | ||||||
|             buildConfigField("String", "BOOKMARK_LOCATIONS_AUTHORITY", "\"fr.free.nrw.commons.bookmarks.locations.contentprovider\"") |  | ||||||
|             buildConfigField("String", "BOOKMARK_ITEMS_AUTHORITY", "\"fr.free.nrw.commons.bookmarks.items.contentprovider\"") |  | ||||||
|             buildConfigField("String", "COMMIT_SHA", "\"" + getBuildVersion().toString() + "\"") |  | ||||||
|             buildConfigField("String", "TEST_USERNAME", "\"" + getTestUserName() + "\"") |  | ||||||
|             buildConfigField("String", "TEST_PASSWORD", "\"" + getTestPassword() + "\"") |  | ||||||
|             buildConfigField("String", "DEPICTS_PROPERTY", "\"P180\"") |  | ||||||
|             buildConfigField("String", "CREATOR_PROPERTY", "\"P170\"") |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         create("beta") { |  | ||||||
|             dimension = "tier" |  | ||||||
|             applicationId = "fr.free.nrw.commons.beta" |  | ||||||
| 
 |  | ||||||
|             // What values do we need to hit the BETA versions of the site / api ? |  | ||||||
|             buildConfigField("String", "WIKIMEDIA_API_POTD", "\"https://commons.wikimedia.org/w/api.php?action=featuredfeed&feed=potd&feedformat=rss&language=en\"") |  | ||||||
|             buildConfigField("String", "WIKIMEDIA_API_HOST", "\"https://commons.wikimedia.beta.wmflabs.org/w/api.php\"") |  | ||||||
|             buildConfigField("String", "WIKIDATA_API_HOST", "\"https://www.wikidata.org/w/api.php\"") |  | ||||||
|             buildConfigField("String", "WIKIDATA_URL", "\"https://www.wikidata.org\"") |  | ||||||
|             buildConfigField("String", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\"") |  | ||||||
|             buildConfigField("String", "WIKIMEDIA_CAMPAIGNS_URL", "\"https://raw.githubusercontent.com/commons-app/campaigns/master/campaigns_beta_active.json\"") |  | ||||||
|             buildConfigField("String", "IMAGE_URL_BASE", "\"https://upload.beta.wmflabs.org/wikipedia/commons\"") |  | ||||||
|             buildConfigField("String", "HOME_URL", "\"https://commons.wikimedia.beta.wmflabs.org/wiki/\"") |  | ||||||
|             buildConfigField("String", "COMMONS_URL", "\"https://commons.wikimedia.beta.wmflabs.org\"") |  | ||||||
|             buildConfigField("String", "WIKIDATA_URL", "\"https://www.wikidata.org\"") |  | ||||||
|             buildConfigField("String", "MOBILE_HOME_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/wiki/\"") |  | ||||||
|             buildConfigField("String", "MOBILE_META_URL", "\"https://meta.m.wikimedia.beta.wmflabs.org/wiki/\"") |  | ||||||
|             buildConfigField("String", "SIGNUP_LANDING_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Special:CreateAccount&returnto=Main+Page&returntoquery=welcome%3Dyes\"") |  | ||||||
|             buildConfigField("String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Main_Page&welcome=yes\"") |  | ||||||
|             buildConfigField("String", "FORGOT_PASSWORD_URL", "\"https://commons.wikimedia.beta.wmflabs.org/wiki/Special:PasswordReset\"") |  | ||||||
|             buildConfigField("String", "PRIVACY_POLICY_URL", "\"https://github.com/commons-app/commons-app-documentation/blob/master/android/Privacy-policy.md\"") |  | ||||||
|             buildConfigField("String", "FILE_USAGES_BASE_URL", "\"https://commons.wikimedia.org/w/api.php?action=query&format=json&formatversion=2\"") |  | ||||||
|             buildConfigField("String", "ACCOUNT_TYPE", "\"fr.free.nrw.commons.beta\"") |  | ||||||
|             buildConfigField("String", "CONTRIBUTION_AUTHORITY", "\"fr.free.nrw.commons.beta.contributions.contentprovider\"") |  | ||||||
|             buildConfigField("String", "MODIFICATION_AUTHORITY", "\"fr.free.nrw.commons.beta.modifications.contentprovider\"") |  | ||||||
|             buildConfigField("String", "CATEGORY_AUTHORITY", "\"fr.free.nrw.commons.beta.categories.contentprovider\"") |  | ||||||
|             buildConfigField("String", "RECENT_SEARCH_AUTHORITY", "\"fr.free.nrw.commons.beta.explore.recentsearches.contentprovider\"") |  | ||||||
|             buildConfigField("String", "RECENT_LANGUAGE_AUTHORITY", "\"fr.free.nrw.commons.beta.recentlanguages.contentprovider\"") |  | ||||||
|             buildConfigField("String", "BOOKMARK_AUTHORITY", "\"fr.free.nrw.commons.beta.bookmarks.contentprovider\"") |  | ||||||
|             buildConfigField("String", "BOOKMARK_LOCATIONS_AUTHORITY", "\"fr.free.nrw.commons.beta.bookmarks.locations.contentprovider\"") |  | ||||||
|             buildConfigField("String", "BOOKMARK_ITEMS_AUTHORITY", "\"fr.free.nrw.commons.beta.bookmarks.items.contentprovider\"") |  | ||||||
|             buildConfigField("String", "COMMIT_SHA", "\"" + getBuildVersion().toString() + "\"") |  | ||||||
|             buildConfigField("String", "TEST_USERNAME", "\"" + getTestUserName() + "\"") |  | ||||||
|             buildConfigField("String", "TEST_PASSWORD", "\"" + getTestPassword() + "\"") |  | ||||||
|             buildConfigField("String", "DEPICTS_PROPERTY", "\"P245962\"") |  | ||||||
|             buildConfigField("String", "CREATOR_PROPERTY", "\"P253075\"") |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|     compileOptions { |  | ||||||
|         sourceCompatibility = JavaVersion.VERSION_17 |  | ||||||
|         targetCompatibility = JavaVersion.VERSION_17 |  | ||||||
|     } |  | ||||||
|     kotlinOptions { |  | ||||||
|         jvmTarget = "17" |  | ||||||
|     } |  | ||||||
|     buildFeatures { |  | ||||||
|         buildConfig = true |  | ||||||
|         viewBinding = true |  | ||||||
|         compose = true |  | ||||||
|     } |  | ||||||
|     buildToolsVersion = buildToolsVersion |  | ||||||
|     composeOptions { |  | ||||||
|         kotlinCompilerExtensionVersion = "1.5.8" |  | ||||||
|     } |  | ||||||
|     packaging { |  | ||||||
|         jniLibs { |  | ||||||
|             excludes += listOf("META-INF/androidx.*") |  | ||||||
|         } |  | ||||||
|         resources { |  | ||||||
|             excludes += listOf( |  | ||||||
|                 "META-INF/androidx.*", |  | ||||||
|                 "META-INF/proguard/androidx-annotations.pro", |  | ||||||
|                 "/META-INF/LICENSE.md", |  | ||||||
|                 "/META-INF/LICENSE-notice.md" |  | ||||||
|             ) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|     testOptions { |  | ||||||
|         animationsDisabled = true |  | ||||||
|         unitTests { |  | ||||||
|             isReturnDefaultValues = true |  | ||||||
|             isIncludeAndroidResources = true |  | ||||||
|         } |  | ||||||
|         unitTests.all { |  | ||||||
|             it.jvmArgs("-noverify") |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|     lint { |  | ||||||
|         abortOnError = false |  | ||||||
|         disable += listOf("MissingTranslation", "ExtraTranslation") |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| dependencies { |  | ||||||
|     // Utils |  | ||||||
|     implementation(libs.gson) |  | ||||||
|     implementation(libs.okhttp) |  | ||||||
|     implementation(libs.retrofit) |  | ||||||
|     implementation(libs.retrofit.converter.gson) |  | ||||||
|     implementation(libs.retrofit.adapter.rxjava) |  | ||||||
|     implementation(libs.rxandroid) |  | ||||||
|     implementation(libs.rxjava) |  | ||||||
|     implementation(libs.rxbinding) |  | ||||||
|     implementation(libs.rxbinding.appcompat) |  | ||||||
|     implementation(libs.facebook.fresco) |  | ||||||
|     implementation(libs.facebook.fresco.middleware) |  | ||||||
|     implementation(libs.apache.commons.lang3) |  | ||||||
| 
 |  | ||||||
|     // UI |  | ||||||
|     implementation("${libs.viewpagerindicator.library.get()}@aar") |  | ||||||
|     implementation(libs.photoview) |  | ||||||
|     implementation(libs.android.sdk) |  | ||||||
|     implementation(libs.android.plugin.scalebar) |  | ||||||
| 
 |  | ||||||
|     implementation(libs.timber) |  | ||||||
|     implementation(libs.android.material) |  | ||||||
|     implementation(libs.dexter) |  | ||||||
| 
 |  | ||||||
|     // Jetpack Compose |  | ||||||
|     implementation(libs.androidx.core.ktx) |  | ||||||
|     implementation(libs.androidx.lifecycle.runtime.ktx) |  | ||||||
|     implementation(libs.androidx.activity.compose) |  | ||||||
|     implementation(platform(libs.androidx.compose.bom)) |  | ||||||
|     implementation(libs.androidx.compose.runtime) |  | ||||||
|     implementation(libs.androidx.ui) |  | ||||||
|     implementation(libs.androidx.ui.graphics) |  | ||||||
|     implementation(libs.androidx.ui.tooling.preview) |  | ||||||
|     implementation(libs.androidx.ui.viewbinding) |  | ||||||
|     implementation(libs.androidx.material3) |  | ||||||
|     implementation(libs.androidx.foundation) |  | ||||||
|     implementation(libs.androidx.foundation.layout) |  | ||||||
|     androidTestImplementation(platform(libs.androidx.compose.bom)) |  | ||||||
|     androidTestImplementation(libs.androidx.ui.test.junit4) |  | ||||||
|     debugImplementation(libs.androidx.ui.tooling) |  | ||||||
|     debugImplementation(libs.androidx.ui.test.manifest) |  | ||||||
| 
 |  | ||||||
|     implementation(libs.adapterdelegates4.kotlin.dsl.viewbinding) |  | ||||||
|     implementation(libs.adapterdelegates4.pagination) |  | ||||||
|     implementation(libs.androidx.paging.runtime.ktx) |  | ||||||
|     testImplementation(libs.androidx.paging.common.ktx) |  | ||||||
|     implementation(libs.androidx.paging.rxjava2.ktx) |  | ||||||
|     implementation(libs.androidx.recyclerview) |  | ||||||
| 
 |  | ||||||
|     // Logging |  | ||||||
|     implementation(libs.acra.dialog) |  | ||||||
|     implementation(libs.acra.mail) |  | ||||||
|     implementation(libs.slf4j.api) |  | ||||||
|     implementation(libs.logback.android.classic) { |  | ||||||
|         exclude(group = "com.google.android", module = "android") |  | ||||||
|     } |  | ||||||
|     implementation(libs.logging.interceptor) |  | ||||||
| 
 |  | ||||||
|     // Dependency injector |  | ||||||
|     implementation(libs.dagger.android) |  | ||||||
|     implementation(libs.dagger.android.support) |  | ||||||
|     kapt(libs.dagger.android.processor) |  | ||||||
|     kapt(libs.dagger.compiler) |  | ||||||
|     annotationProcessor(libs.dagger.android.processor) |  | ||||||
| 
 |  | ||||||
|     implementation(libs.kotlin.reflect) |  | ||||||
| 
 |  | ||||||
|     //Mocking |  | ||||||
|     testImplementation(libs.mockito.kotlin) |  | ||||||
|     testImplementation(libs.mockito.core) |  | ||||||
|     testImplementation(libs.powermock.module.junit) |  | ||||||
|     testImplementation(libs.powermock.api.mockito) |  | ||||||
|     testImplementation(libs.mockk) |  | ||||||
| 
 |  | ||||||
|     // Unit testing |  | ||||||
|     testImplementation(libs.junit) |  | ||||||
|     testImplementation(libs.robolectric) |  | ||||||
|     testImplementation(libs.androidx.test.core) |  | ||||||
|     testImplementation(libs.androidx.runner) |  | ||||||
|     testImplementation(libs.androidx.test.ext.junit) |  | ||||||
|     testImplementation(libs.androidx.test.rules) |  | ||||||
|     testImplementation(libs.mockwebserver) |  | ||||||
|     testImplementation(libs.livedata.testing.ktx) |  | ||||||
|     testImplementation(libs.androidx.core.testing) |  | ||||||
|     testImplementation(libs.junit.jupiter.api) |  | ||||||
|     testRuntimeOnly(libs.junit.jupiter.engine) |  | ||||||
|     testImplementation(libs.soloader) |  | ||||||
|     testImplementation(libs.kotlinx.coroutines.test) |  | ||||||
|     debugImplementation(libs.androidx.fragment.testing) |  | ||||||
|     testImplementation(libs.commons.io) |  | ||||||
| 
 |  | ||||||
|     // Android testing |  | ||||||
|     androidTestImplementation(libs.androidx.espresso.core) |  | ||||||
|     androidTestImplementation(libs.androidx.espresso.intents) |  | ||||||
|     androidTestImplementation(libs.androidx.espresso.contrib) |  | ||||||
|     androidTestImplementation(libs.androidx.runner) |  | ||||||
|     androidTestImplementation(libs.androidx.test.rules) |  | ||||||
|     androidTestImplementation(libs.androidx.test.core) |  | ||||||
|     androidTestImplementation(libs.androidx.test.ext.junit) |  | ||||||
|     androidTestImplementation(libs.androidx.annotation) |  | ||||||
|     androidTestImplementation(libs.mockwebserver) |  | ||||||
|     androidTestImplementation(libs.androidx.uiautomator) |  | ||||||
| 
 |  | ||||||
|     // Debugging |  | ||||||
|     debugImplementation(libs.leakcanary.android) |  | ||||||
| 
 |  | ||||||
|     // Support libraries |  | ||||||
|     implementation(libs.androidx.browser) |  | ||||||
|     implementation(libs.androidx.cardview) |  | ||||||
|     implementation(libs.androidx.constraintlayout) |  | ||||||
|     implementation(libs.androidx.exifinterface) |  | ||||||
|     implementation(libs.recyclerview.fastscroll) |  | ||||||
| 
 |  | ||||||
|     //swipe_layout |  | ||||||
|     implementation(libs.swipelayout.library) |  | ||||||
| 
 |  | ||||||
|     //Room |  | ||||||
|     implementation(libs.androidx.room.runtime) |  | ||||||
|     implementation(libs.androidx.room.ktx) |  | ||||||
|     implementation(libs.androidx.room.rxjava) |  | ||||||
|     kapt(libs.androidx.room.compiler) |  | ||||||
| 
 |  | ||||||
|     // Preferences |  | ||||||
|     implementation(libs.androidx.preference) |  | ||||||
|     implementation(libs.androidx.preference.ktx) |  | ||||||
| 
 |  | ||||||
|     //Android Media |  | ||||||
|     implementation(libs.juanitobananas.androidDmediaUtil) |  | ||||||
|     implementation(libs.androidx.multidex) |  | ||||||
| 
 |  | ||||||
|     // Kotlin + coroutines |  | ||||||
|     implementation(libs.androidx.work.runtime.ktx) |  | ||||||
|     implementation(libs.androidx.work.runtime) |  | ||||||
|     implementation(libs.kotlinx.coroutines.rx2) |  | ||||||
|     testImplementation(libs.androidx.work.testing) |  | ||||||
| 
 |  | ||||||
|     //Glide |  | ||||||
|     implementation(libs.glide) |  | ||||||
|     annotationProcessor(libs.glide.compiler) |  | ||||||
|     kaptTest(libs.androidx.databinding.compiler) |  | ||||||
|     kaptAndroidTest(libs.androidx.databinding.compiler) |  | ||||||
| 
 |  | ||||||
|     implementation(libs.coordinates2country.android) { |  | ||||||
|         exclude(group = "com.google.android", module = "android") |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     //OSMDroid |  | ||||||
|     implementation(libs.osmdroid.android) |  | ||||||
|     constraints { |  | ||||||
|         implementation(libs.kotlin.stdlib.jdk7) { |  | ||||||
|             because("kotlin-stdlib-jdk7 is now a part of kotlin-stdlib") |  | ||||||
|         } |  | ||||||
|         implementation(libs.kotlin.stdlib.jdk8) { |  | ||||||
|             because("kotlin-stdlib-jdk8 is now a part of kotlin-stdlib") |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| tasks.register<Exec>("disableAnimations") { |  | ||||||
|     val adb = "${System.getenv("ANDROID_HOME")}/platform-tools/adb" |  | ||||||
|     commandLine(adb, "shell", "settings", "put", "global", "window_animation_scale", "0") |  | ||||||
|     commandLine(adb, "shell", "settings", "put", "global", "transition_animation_scale", "0") |  | ||||||
|     commandLine(adb, "shell", "settings", "put", "global", "animator_duration_scale", "0") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| project.gradle.taskGraph.whenReady { |  | ||||||
|     val connectedBetaDebugAndroidTest = tasks.named("connectedBetaDebugAndroidTest") |  | ||||||
|     val connectedProdDebugAndroidTest = tasks.named("connectedProdDebugAndroidTest") |  | ||||||
| 
 |  | ||||||
|     connectedBetaDebugAndroidTest.configure { |  | ||||||
|         dependsOn("disableAnimations") |  | ||||||
|     } |  | ||||||
|     connectedProdDebugAndroidTest.configure { |  | ||||||
|         dependsOn("disableAnimations") |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| fun getTestUserName(): String? { |  | ||||||
|     val propFile = rootProject.file("./local.properties") |  | ||||||
|     val properties = Properties() |  | ||||||
|     propFile.inputStream().use { properties.load(it) } |  | ||||||
|     return properties.getProperty("TEST_USER_NAME") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| fun getTestPassword(): String? { |  | ||||||
|     val propFile = rootProject.file("./local.properties") |  | ||||||
|     val properties = Properties() |  | ||||||
|     propFile.inputStream().use { properties.load(it) } |  | ||||||
|     return properties.getProperty("TEST_USER_PASSWORD") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| if (isRunningOnTravisAndIsNotPRBuild) { |  | ||||||
|     configure<com.github.triplet.gradle.play.PlayPublisherExtension> { |  | ||||||
|         track = "alpha" |  | ||||||
|         userFraction = 1.0 |  | ||||||
|         serviceAccountEmail = System.getenv("SERVICE_ACCOUNT_NAME") |  | ||||||
|         serviceAccountCredentials = file("../play.p12") |  | ||||||
| 
 |  | ||||||
|         resolutionStrategy = "auto" |  | ||||||
|         outputProcessor { // this: ApkVariantOutput |  | ||||||
|             versionNameOverride = "$versionNameOverride.$versionCode" |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| fun getBuildVersion(): String? { |  | ||||||
|     return try { |  | ||||||
|         val stdout = ByteArrayOutputStream() |  | ||||||
|         exec { |  | ||||||
|             commandLine("git", "rev-parse", "--short", "HEAD") |  | ||||||
|             standardOutput = stdout |  | ||||||
|         } |  | ||||||
|         stdout.toString().trim() |  | ||||||
|     } catch (e: Exception) { |  | ||||||
|         null |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| fun getBranchName(): String? { |  | ||||||
|     return try { |  | ||||||
|         val stdout = ByteArrayOutputStream() |  | ||||||
|         exec { |  | ||||||
|             commandLine("git", "rev-parse", "--abbrev-ref", "HEAD") |  | ||||||
|             standardOutput = stdout |  | ||||||
|         } |  | ||||||
|         stdout.toString().trim() |  | ||||||
|     } catch (e: Exception) { |  | ||||||
|         null |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -31,17 +31,6 @@ | ||||||
| -keepattributes Signature | -keepattributes Signature | ||||||
| # Retain declared checked exceptions for use by a Proxy instance. | # Retain declared checked exceptions for use by a Proxy instance. | ||||||
| -keepattributes Exceptions | -keepattributes Exceptions | ||||||
| 
 |  | ||||||
| # Note: The model package right now seems to include some other classes that |  | ||||||
| # are not used for serialization / deserialization over Gson. Hopefully |  | ||||||
| # that's not a problem since it only prevents R8 from avoiding trimming |  | ||||||
| # of few more classes. |  | ||||||
| -keepclasseswithmembers class fr.free.nrw.commons.*.model.** { *; } |  | ||||||
| -keepclasseswithmembers class fr.free.nrw.commons.actions.** { *; } |  | ||||||
| -keepclasseswithmembers class fr.free.nrw.commons.auth.csrf.** { *; } |  | ||||||
| -keepclasseswithmembers class fr.free.nrw.commons.auth.login.** { *; } |  | ||||||
| -keepclasseswithmembers class fr.free.nrw.commons.wikidata.mwapi.** { *; } |  | ||||||
| 
 |  | ||||||
| # --- /Retrofit --- | # --- /Retrofit --- | ||||||
| 
 | 
 | ||||||
| # --- OkHttp + Okio --- | # --- OkHttp + Okio --- | ||||||
|  | @ -66,9 +55,6 @@ | ||||||
| # Application classes that will be serialized/deserialized over Gson | # Application classes that will be serialized/deserialized over Gson | ||||||
| -keep class com.google.gson.examples.android.model.** { *; } | -keep class com.google.gson.examples.android.model.** { *; } | ||||||
| 
 | 
 | ||||||
| # Prevent R8 from obfuscating project classes used by Gson for parsing |  | ||||||
| -keep class fr.free.nrw.commons.fileusages.** { *; } |  | ||||||
| 
 |  | ||||||
| # Prevent proguard from stripping interface information from TypeAdapterFactory, | # Prevent proguard from stripping interface information from TypeAdapterFactory, | ||||||
| # JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter) | # JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter) | ||||||
| -keep class * implements com.google.gson.TypeAdapterFactory | -keep class * implements com.google.gson.TypeAdapterFactory | ||||||
|  |  | ||||||
|  | @ -25,6 +25,7 @@ import org.junit.runner.RunWith | ||||||
| 
 | 
 | ||||||
| @RunWith(AndroidJUnit4::class) | @RunWith(AndroidJUnit4::class) | ||||||
| class AboutActivityTest { | class AboutActivityTest { | ||||||
|  | 
 | ||||||
|     @get:Rule |     @get:Rule | ||||||
|     var activityRule: ActivityTestRule<*> = ActivityTestRule(AboutActivity::class.java) |     var activityRule: ActivityTestRule<*> = ActivityTestRule(AboutActivity::class.java) | ||||||
| 
 | 
 | ||||||
|  | @ -35,8 +36,7 @@ class AboutActivityTest { | ||||||
|         device.setOrientationNatural() |         device.setOrientationNatural() | ||||||
|         device.freezeRotation() |         device.freezeRotation() | ||||||
|         Intents.init() |         Intents.init() | ||||||
|         Intents |         Intents.intending(CoreMatchers.not(IntentMatchers.isInternal())) | ||||||
|             .intending(CoreMatchers.not(IntentMatchers.isInternal())) |  | ||||||
|             .respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null)) |             .respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null)) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -47,12 +47,11 @@ class AboutActivityTest { | ||||||
| 
 | 
 | ||||||
|     @Test |     @Test | ||||||
|     fun testBuildNumber() { |     fun testBuildNumber() { | ||||||
|         Espresso |         Espresso.onView(ViewMatchers.withId(R.id.about_version)) | ||||||
|             .onView(ViewMatchers.withId(R.id.about_version)) |  | ||||||
|             .check( |             .check( | ||||||
|                 ViewAssertions.matches( |                 ViewAssertions.matches( | ||||||
|                     withText(getApplicationContext<CommonsApplication>().getVersionNameWithSha()), |                     withText(getApplicationContext<CommonsApplication>().getVersionNameWithSha()) | ||||||
|                 ), |                 ) | ||||||
|             ) |             ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -62,8 +61,8 @@ class AboutActivityTest { | ||||||
|         Intents.intended( |         Intents.intended( | ||||||
|             CoreMatchers.allOf( |             CoreMatchers.allOf( | ||||||
|                 IntentMatchers.hasAction(Intent.ACTION_VIEW), |                 IntentMatchers.hasAction(Intent.ACTION_VIEW), | ||||||
|                 IntentMatchers.hasData(Urls.WEBSITE_URL), |                 IntentMatchers.hasData(Urls.WEBSITE_URL) | ||||||
|             ), |             ) | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -74,8 +73,8 @@ class AboutActivityTest { | ||||||
|             CoreMatchers.anyOf( |             CoreMatchers.anyOf( | ||||||
|                 IntentMatchers.hasAction(Intent.ACTION_VIEW), |                 IntentMatchers.hasAction(Intent.ACTION_VIEW), | ||||||
|                 IntentMatchers.hasData(Urls.FACEBOOK_WEB_URL), |                 IntentMatchers.hasData(Urls.FACEBOOK_WEB_URL), | ||||||
|                 IntentMatchers.hasPackage(Urls.FACEBOOK_PACKAGE_NAME), |                 IntentMatchers.hasPackage(Urls.FACEBOOK_PACKAGE_NAME) | ||||||
|             ), |             ) | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -85,8 +84,8 @@ class AboutActivityTest { | ||||||
|         Intents.intended( |         Intents.intended( | ||||||
|             CoreMatchers.allOf( |             CoreMatchers.allOf( | ||||||
|                 IntentMatchers.hasAction(Intent.ACTION_VIEW), |                 IntentMatchers.hasAction(Intent.ACTION_VIEW), | ||||||
|                 IntentMatchers.hasData(Urls.GITHUB_REPO_URL), |                 IntentMatchers.hasData(Urls.GITHUB_REPO_URL) | ||||||
|             ), |             ) | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -96,8 +95,8 @@ class AboutActivityTest { | ||||||
|         Intents.intended( |         Intents.intended( | ||||||
|             CoreMatchers.allOf( |             CoreMatchers.allOf( | ||||||
|                 IntentMatchers.hasAction(Intent.ACTION_VIEW), |                 IntentMatchers.hasAction(Intent.ACTION_VIEW), | ||||||
|                 IntentMatchers.hasData(BuildConfig.PRIVACY_POLICY_URL), |                 IntentMatchers.hasData(BuildConfig.PRIVACY_POLICY_URL) | ||||||
|             ), |             ) | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -105,12 +104,12 @@ class AboutActivityTest { | ||||||
|     fun testLaunchTranslate() { |     fun testLaunchTranslate() { | ||||||
|         Espresso.onView(ViewMatchers.withId(R.id.about_translate)).perform(ViewActions.click()) |         Espresso.onView(ViewMatchers.withId(R.id.about_translate)).perform(ViewActions.click()) | ||||||
|         Espresso.onView(ViewMatchers.withId(android.R.id.button1)).perform(ViewActions.click()) |         Espresso.onView(ViewMatchers.withId(android.R.id.button1)).perform(ViewActions.click()) | ||||||
|         val langCode = CommonsApplication.instance.languageLookUpTable!!.getCodes()[0] |         val langCode = CommonsApplication.getInstance().languageLookUpTable.codes[0] | ||||||
|         Intents.intended( |         Intents.intended( | ||||||
|             CoreMatchers.allOf( |             CoreMatchers.allOf( | ||||||
|                 IntentMatchers.hasAction(Intent.ACTION_VIEW), |                 IntentMatchers.hasAction(Intent.ACTION_VIEW), | ||||||
|                 IntentMatchers.hasData("${Urls.TRANSLATE_WIKI_URL}$langCode"), |                 IntentMatchers.hasData("${Urls.TRANSLATE_WIKI_URL}$langCode") | ||||||
|             ), |             ) | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -120,30 +119,27 @@ class AboutActivityTest { | ||||||
|         Intents.intended( |         Intents.intended( | ||||||
|             CoreMatchers.allOf( |             CoreMatchers.allOf( | ||||||
|                 IntentMatchers.hasAction(Intent.ACTION_VIEW), |                 IntentMatchers.hasAction(Intent.ACTION_VIEW), | ||||||
|                 IntentMatchers.hasData(Urls.CREDITS_URL), |                 IntentMatchers.hasData(Urls.CREDITS_URL) | ||||||
|             ), |             ) | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Test |     @Test | ||||||
|     fun testLaunchUserGuide() { |     fun testLaunchUserGuide() { | ||||||
|         Espresso.onView(ViewMatchers.withId(R.id.about_user_guide)).perform(ViewActions.click()) |         Espresso.onView(ViewMatchers.withId(R.id.about_user_guide)).perform(ViewActions.click()) | ||||||
|         Intents.intended( |         Intents.intended(CoreMatchers.allOf(IntentMatchers.hasAction(Intent.ACTION_VIEW), | ||||||
|             CoreMatchers.allOf( |             IntentMatchers.hasData(Urls.USER_GUIDE_URL))) | ||||||
|                 IntentMatchers.hasAction(Intent.ACTION_VIEW), |  | ||||||
|                 IntentMatchers.hasData(Urls.USER_GUIDE_URL), |  | ||||||
|             ), |  | ||||||
|         ) |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|     @Test |     @Test | ||||||
|     fun testLaunchAboutFaq() { |     fun testLaunchAboutFaq() { | ||||||
|         Espresso.onView(ViewMatchers.withId(R.id.about_faq)).perform(ViewActions.click()) |         Espresso.onView(ViewMatchers.withId(R.id.about_faq)).perform(ViewActions.click()) | ||||||
|         Intents.intended( |         Intents.intended( | ||||||
|             CoreMatchers.allOf( |             CoreMatchers.allOf( | ||||||
|                 IntentMatchers.hasAction(Intent.ACTION_VIEW), |                 IntentMatchers.hasAction(Intent.ACTION_VIEW), | ||||||
|                 IntentMatchers.hasData(Urls.FAQ_URL), |                 IntentMatchers.hasData(Urls.FAQ_URL) | ||||||
|             ), |             ) | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -18,14 +18,12 @@ import fr.free.nrw.commons.auth.LoginActivity | ||||||
| import fr.free.nrw.commons.auth.SignupActivity | import fr.free.nrw.commons.auth.SignupActivity | ||||||
| import org.hamcrest.CoreMatchers | import org.hamcrest.CoreMatchers | ||||||
| import org.hamcrest.CoreMatchers.not | import org.hamcrest.CoreMatchers.not | ||||||
| import org.junit.After | import org.junit.* | ||||||
| import org.junit.Before |  | ||||||
| import org.junit.Rule |  | ||||||
| import org.junit.Test |  | ||||||
| import org.junit.runner.RunWith | import org.junit.runner.RunWith | ||||||
| 
 | 
 | ||||||
| @RunWith(AndroidJUnit4::class) | @RunWith(AndroidJUnit4::class) | ||||||
| class LoginActivityTest { | class LoginActivityTest { | ||||||
|  | 
 | ||||||
|     @get:Rule |     @get:Rule | ||||||
|     var activityRule = ActivityTestRule(LoginActivity::class.java) |     var activityRule = ActivityTestRule(LoginActivity::class.java) | ||||||
| 
 | 
 | ||||||
|  | @ -51,8 +49,8 @@ class LoginActivityTest { | ||||||
|         Intents.intended( |         Intents.intended( | ||||||
|             CoreMatchers.allOf( |             CoreMatchers.allOf( | ||||||
|                 IntentMatchers.hasAction(Intent.ACTION_VIEW), |                 IntentMatchers.hasAction(Intent.ACTION_VIEW), | ||||||
|                 IntentMatchers.hasData(BuildConfig.FORGOT_PASSWORD_URL), |                 IntentMatchers.hasData(BuildConfig.FORGOT_PASSWORD_URL) | ||||||
|             ), |             ) | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -21,23 +21,20 @@ import fr.free.nrw.commons.kvstore.JsonKvStore | ||||||
| import fr.free.nrw.commons.notification.NotificationActivity | import fr.free.nrw.commons.notification.NotificationActivity | ||||||
| import org.hamcrest.CoreMatchers | import org.hamcrest.CoreMatchers | ||||||
| import org.hamcrest.Matchers | import org.hamcrest.Matchers | ||||||
| import org.junit.After | import org.junit.* | ||||||
| import org.junit.Before |  | ||||||
| import org.junit.Rule |  | ||||||
| import org.junit.Test |  | ||||||
| import org.junit.runner.RunWith | import org.junit.runner.RunWith | ||||||
| 
 | 
 | ||||||
| @LargeTest | @LargeTest | ||||||
| @RunWith(AndroidJUnit4::class) | @RunWith(AndroidJUnit4::class) | ||||||
| class MainActivityTest { | class MainActivityTest { | ||||||
|  | 
 | ||||||
|     @get:Rule |     @get:Rule | ||||||
|     var activityRule: ActivityTestRule<*> = ActivityTestRule(LoginActivity::class.java) |     var activityRule: ActivityTestRule<*> = ActivityTestRule(LoginActivity::class.java) | ||||||
| 
 | 
 | ||||||
|     @get:Rule |     @get:Rule | ||||||
|     var mGrantPermissionRule: GrantPermissionRule = |     var mGrantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant( | ||||||
|         GrantPermissionRule.grant( |         "android.permission.ACCESS_FINE_LOCATION" | ||||||
|             "android.permission.ACCESS_FINE_LOCATION", |     ) | ||||||
|         ) |  | ||||||
| 
 | 
 | ||||||
|     private val device: UiDevice = |     private val device: UiDevice = | ||||||
|         UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) |         UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) | ||||||
|  | @ -51,8 +48,7 @@ class MainActivityTest { | ||||||
|         UITestHelper.loginUser() |         UITestHelper.loginUser() | ||||||
|         UITestHelper.skipWelcome() |         UITestHelper.skipWelcome() | ||||||
|         Intents.init() |         Intents.init() | ||||||
|         Intents |         Intents.intending(CoreMatchers.not(IntentMatchers.isInternal())) | ||||||
|             .intending(CoreMatchers.not(IntentMatchers.isInternal())) |  | ||||||
|             .respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null)) |             .respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null)) | ||||||
|         val context = InstrumentationRegistry.getInstrumentation().targetContext |         val context = InstrumentationRegistry.getInstrumentation().targetContext | ||||||
|         val storeName = context.packageName + "_preferences" |         val storeName = context.packageName + "_preferences" | ||||||
|  | @ -66,149 +62,169 @@ class MainActivityTest { | ||||||
| 
 | 
 | ||||||
|     @Test |     @Test | ||||||
|     fun testNearby() { |     fun testNearby() { | ||||||
|         Espresso |         Espresso.onView( | ||||||
|             .onView( |             Matchers.allOf( | ||||||
|                 Matchers.allOf( |                 childAtPosition( | ||||||
|                     childAtPosition( |                     childAtPosition( | ||||||
|                         childAtPosition( |                         ViewMatchers.withId(R.id.fragment_main_nav_tab_layout), | ||||||
|                             ViewMatchers.withId(R.id.fragment_main_nav_tab_layout), |                         0 | ||||||
|                             0, |  | ||||||
|                         ), |  | ||||||
|                         1, |  | ||||||
|                     ), |                     ), | ||||||
|                     ViewMatchers.isDisplayed(), |                     1 | ||||||
|                 ), |                 ), | ||||||
|             ).perform(ViewActions.click()) |                 ViewMatchers.isDisplayed() | ||||||
|         Espresso |             ) | ||||||
|             .onView(ViewMatchers.withId(R.id.fragmentContainer)) |         ).perform(ViewActions.click()) | ||||||
|  |         Espresso.onView(ViewMatchers.withId(R.id.fragmentContainer)) | ||||||
|             .check(matches(ViewMatchers.isDisplayed())) |             .check(matches(ViewMatchers.isDisplayed())) | ||||||
|         UITestHelper.sleep(10000) |         UITestHelper.sleep(10000) | ||||||
|         val actionMenuItemView2 = |         val actionMenuItemView2 = Espresso.onView( | ||||||
|             Espresso.onView( |             Matchers.allOf( | ||||||
|                 Matchers.allOf( |                 ViewMatchers.withId(R.id.list_sheet), ViewMatchers.withContentDescription("List"), | ||||||
|                     ViewMatchers.withId(R.id.list_sheet), |                 childAtPosition( | ||||||
|                     ViewMatchers.withContentDescription("List"), |  | ||||||
|                     childAtPosition( |                     childAtPosition( | ||||||
|                         childAtPosition( |                         ViewMatchers.withId(R.id.toolbar), | ||||||
|                             ViewMatchers.withId(R.id.toolbar), |                         1 | ||||||
|                             1, |  | ||||||
|                         ), |  | ||||||
|                         0, |  | ||||||
|                     ), |                     ), | ||||||
|                     ViewMatchers.isDisplayed(), |                     0 | ||||||
|                 ), |                 ), | ||||||
|  |                 ViewMatchers.isDisplayed() | ||||||
|             ) |             ) | ||||||
|  |         ) | ||||||
|         actionMenuItemView2.perform(ViewActions.click()) |         actionMenuItemView2.perform(ViewActions.click()) | ||||||
|         UITestHelper.sleep(1000) |         UITestHelper.sleep(1000) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Test |     @Test | ||||||
|     fun testExplore() { |     fun testExplore() { | ||||||
|         Espresso |         Espresso.onView( | ||||||
|             .onView( |             Matchers.allOf( | ||||||
|                 Matchers.allOf( |                 childAtPosition( | ||||||
|                     childAtPosition( |                     childAtPosition( | ||||||
|                         childAtPosition( |                         ViewMatchers.withId(R.id.fragment_main_nav_tab_layout), | ||||||
|                             ViewMatchers.withId(R.id.fragment_main_nav_tab_layout), |                         0 | ||||||
|                             0, |  | ||||||
|                         ), |  | ||||||
|                         2, |  | ||||||
|                     ), |                     ), | ||||||
|                     ViewMatchers.isDisplayed(), |                     2 | ||||||
|                 ), |                 ), | ||||||
|             ).perform(ViewActions.click()) |                 ViewMatchers.isDisplayed() | ||||||
|         Espresso |             ) | ||||||
|             .onView(ViewMatchers.withId(R.id.fragmentContainer)) |         ).perform(ViewActions.click()) | ||||||
|  |         Espresso.onView(ViewMatchers.withId(R.id.fragmentContainer)) | ||||||
|             .check(matches(ViewMatchers.isDisplayed())) |             .check(matches(ViewMatchers.isDisplayed())) | ||||||
|         UITestHelper.sleep(1000) |         UITestHelper.sleep(1000) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Test |     @Test | ||||||
|     fun testContributions() { |     fun testContributions() { | ||||||
|         Espresso |         Espresso.onView( | ||||||
|             .onView( |             Matchers.allOf( | ||||||
|                 Matchers.allOf( |                 childAtPosition( | ||||||
|                     childAtPosition( |                     childAtPosition( | ||||||
|                         childAtPosition( |                         ViewMatchers.withId(R.id.fragment_main_nav_tab_layout), | ||||||
|                             ViewMatchers.withId(R.id.fragment_main_nav_tab_layout), |                         0 | ||||||
|                             0, |  | ||||||
|                         ), |  | ||||||
|                         0, |  | ||||||
|                     ), |                     ), | ||||||
|                     ViewMatchers.isDisplayed(), |                     0 | ||||||
|                 ), |  | ||||||
|             ).perform(ViewActions.click()) |  | ||||||
|         Espresso |  | ||||||
|             .onView(ViewMatchers.withId(R.id.fragmentContainer)) |  | ||||||
|             .check(matches(ViewMatchers.isDisplayed())) |  | ||||||
|         Espresso |  | ||||||
|             .onView( |  | ||||||
|                 Matchers.allOf( |  | ||||||
|                     ViewMatchers.withId(R.id.contributionImage), |  | ||||||
|                     childAtPosition( |  | ||||||
|                         childAtPosition( |  | ||||||
|                             ViewMatchers.withId(R.id.contributionsList), |  | ||||||
|                             0, |  | ||||||
|                         ), |  | ||||||
|                         1, |  | ||||||
|                     ), |  | ||||||
|                     ViewMatchers.isDisplayed(), |  | ||||||
|                 ), |  | ||||||
|             ).perform(ViewActions.click()) |  | ||||||
|         val actionMenuItemView = |  | ||||||
|             Espresso.onView( |  | ||||||
|                 Matchers.allOf( |  | ||||||
|                     ViewMatchers.withId(R.id.menu_bookmark_current_image), |  | ||||||
|                     childAtPosition( |  | ||||||
|                         childAtPosition( |  | ||||||
|                             ViewMatchers.withId(R.id.toolbar), |  | ||||||
|                             1, |  | ||||||
|                         ), |  | ||||||
|                         0, |  | ||||||
|                     ), |  | ||||||
|                     ViewMatchers.isDisplayed(), |  | ||||||
|                 ), |                 ), | ||||||
|  |                 ViewMatchers.isDisplayed() | ||||||
|             ) |             ) | ||||||
|  |         ).perform(ViewActions.click()) | ||||||
|  |         Espresso.onView(ViewMatchers.withId(R.id.fragmentContainer)) | ||||||
|  |             .check(matches(ViewMatchers.isDisplayed())) | ||||||
|  |         Espresso.onView( | ||||||
|  |             Matchers.allOf( | ||||||
|  |                 ViewMatchers.withId(R.id.contributionImage), | ||||||
|  |                 childAtPosition( | ||||||
|  |                     childAtPosition( | ||||||
|  |                         ViewMatchers.withId(R.id.contributionsList), | ||||||
|  |                         0 | ||||||
|  |                     ), | ||||||
|  |                     1 | ||||||
|  |                 ), | ||||||
|  |                 ViewMatchers.isDisplayed() | ||||||
|  |             ) | ||||||
|  |         ).perform(ViewActions.click()) | ||||||
|  |         val actionMenuItemView = Espresso.onView( | ||||||
|  |             Matchers.allOf( | ||||||
|  |                 ViewMatchers.withId(R.id.menu_bookmark_current_image), | ||||||
|  |                 childAtPosition( | ||||||
|  |                     childAtPosition( | ||||||
|  |                         ViewMatchers.withId(R.id.toolbar), | ||||||
|  |                         1 | ||||||
|  |                     ), | ||||||
|  |                     0 | ||||||
|  |                 ), | ||||||
|  |                 ViewMatchers.isDisplayed() | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|         actionMenuItemView.perform(ViewActions.click()) |         actionMenuItemView.perform(ViewActions.click()) | ||||||
|         UITestHelper.sleep(3000) |         UITestHelper.sleep(3000) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Test |     @Test | ||||||
|     fun testBookmarks() { |     fun testBookmarks() { | ||||||
|         Espresso |         Espresso.onView( | ||||||
|             .onView( |             Matchers.allOf( | ||||||
|                 Matchers.allOf( |                 childAtPosition( | ||||||
|                     childAtPosition( |                     childAtPosition( | ||||||
|                         childAtPosition( |                         ViewMatchers.withId(R.id.fragment_main_nav_tab_layout), | ||||||
|                             ViewMatchers.withId(R.id.fragment_main_nav_tab_layout), |                         0 | ||||||
|                             0, |  | ||||||
|                         ), |  | ||||||
|                         3, |  | ||||||
|                     ), |                     ), | ||||||
|                     ViewMatchers.isDisplayed(), |                     3 | ||||||
|                 ), |                 ), | ||||||
|             ).perform(ViewActions.click()) |                 ViewMatchers.isDisplayed() | ||||||
|  |             ) | ||||||
|  |         ).perform(ViewActions.click()) | ||||||
|         UITestHelper.sleep(1000) |         UITestHelper.sleep(1000) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Test |     @Test | ||||||
|     fun testNotifications() { |     fun testNotifications() { | ||||||
|         Espresso |         Espresso.onView( | ||||||
|             .onView( |             Matchers.allOf( | ||||||
|                 Matchers.allOf( |                 ViewMatchers.withId(R.id.notifications), | ||||||
|                     ViewMatchers.withId(R.id.notifications), |                 childAtPosition( | ||||||
|                     childAtPosition( |                     childAtPosition( | ||||||
|                         childAtPosition( |                         ViewMatchers.withId(R.id.toolbar), | ||||||
|                             ViewMatchers.withId(R.id.toolbar), |                         1 | ||||||
|                             1, |  | ||||||
|                         ), |  | ||||||
|                         1, |  | ||||||
|                     ), |                     ), | ||||||
|                     ViewMatchers.isDisplayed(), |                     1 | ||||||
|                 ), |                 ), | ||||||
|             ).perform(ViewActions.click()) |                 ViewMatchers.isDisplayed() | ||||||
|  |             ) | ||||||
|  |         ).perform(ViewActions.click()) | ||||||
|         Intents.intended(IntentMatchers.hasComponent(NotificationActivity::class.java.name)) |         Intents.intended(IntentMatchers.hasComponent(NotificationActivity::class.java.name)) | ||||||
|         Espresso.pressBack() |         Espresso.pressBack() | ||||||
|         UITestHelper.sleep(1000) |         UITestHelper.sleep(1000) | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     fun testLimitedConnectionModeToggle() { | ||||||
|  |         val isEnabled = defaultKvStore | ||||||
|  |             .getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false) | ||||||
|  |         Espresso.onView( | ||||||
|  |             Matchers.allOf( | ||||||
|  |                 ViewMatchers.withId(R.id.toggle_limited_connection_mode), | ||||||
|  |                 childAtPosition( | ||||||
|  |                     childAtPosition( | ||||||
|  |                         ViewMatchers.withId(R.id.toolbar), | ||||||
|  |                         1 | ||||||
|  |                     ), | ||||||
|  |                     0 | ||||||
|  |                 ), | ||||||
|  |                 ViewMatchers.isDisplayed() | ||||||
|  |             ) | ||||||
|  |         ).perform(ViewActions.click()) | ||||||
|  |         UITestHelper.sleep(1000) | ||||||
|  |         if (isEnabled) { | ||||||
|  |             Assert.assertFalse( | ||||||
|  |                 defaultKvStore | ||||||
|  |                     .getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false) | ||||||
|  |             ) | ||||||
|  |         } else { | ||||||
|  |             Assert.assertTrue( | ||||||
|  |                 defaultKvStore | ||||||
|  |                     .getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false) | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  | @ -4,6 +4,7 @@ import android.app.Activity | ||||||
| import android.app.Instrumentation | import android.app.Instrumentation | ||||||
| import androidx.test.espresso.Espresso.onView | import androidx.test.espresso.Espresso.onView | ||||||
| import androidx.test.espresso.action.ViewActions | import androidx.test.espresso.action.ViewActions | ||||||
|  | import androidx.test.espresso.action.ViewActions.swipeRight | ||||||
| import androidx.test.espresso.intent.Intents | import androidx.test.espresso.intent.Intents | ||||||
| import androidx.test.espresso.intent.matcher.IntentMatchers | import androidx.test.espresso.intent.matcher.IntentMatchers | ||||||
| import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent | import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent | ||||||
|  | @ -25,6 +26,7 @@ import org.junit.runner.RunWith | ||||||
| 
 | 
 | ||||||
| @RunWith(AndroidJUnit4::class) | @RunWith(AndroidJUnit4::class) | ||||||
| class ProfileActivityTest { | class ProfileActivityTest { | ||||||
|  | 
 | ||||||
|     @get:Rule |     @get:Rule | ||||||
|     var activityRule = IntentsTestRule(LoginActivity::class.java) |     var activityRule = IntentsTestRule(LoginActivity::class.java) | ||||||
| 
 | 
 | ||||||
|  | @ -36,8 +38,7 @@ class ProfileActivityTest { | ||||||
|         device.freezeRotation() |         device.freezeRotation() | ||||||
|         UITestHelper.loginUser() |         UITestHelper.loginUser() | ||||||
|         UITestHelper.skipWelcome() |         UITestHelper.skipWelcome() | ||||||
|         Intents |         Intents.intending(CoreMatchers.not(IntentMatchers.isInternal())) | ||||||
|             .intending(CoreMatchers.not(IntentMatchers.isInternal())) |  | ||||||
|             .respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null)) |             .respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null)) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -49,19 +50,20 @@ class ProfileActivityTest { | ||||||
|                 childAtPosition( |                 childAtPosition( | ||||||
|                     childAtPosition( |                     childAtPosition( | ||||||
|                         withId(R.id.fragment_main_nav_tab_layout), |                         withId(R.id.fragment_main_nav_tab_layout), | ||||||
|                         0, |                         0 | ||||||
|                     ), |                     ), | ||||||
|                     4, |                     4 | ||||||
|                 ), |                 ), | ||||||
|                 ViewMatchers.isDisplayed(), |                 ViewMatchers.isDisplayed() | ||||||
|             ), |             ) | ||||||
|         ).perform(ViewActions.click()) |         ).perform(ViewActions.click()) | ||||||
|         onView(Matchers.allOf(withId(R.id.more_profile))).perform( |         onView(Matchers.allOf(withId(R.id.more_profile))).perform( | ||||||
|             ViewActions.scrollTo(), |             ViewActions.scrollTo(), | ||||||
|             ViewActions.click(), |             ViewActions.click() | ||||||
|         ) |         ) | ||||||
|         device.swipe(1033, 1346, 531, 1346, 20) |         device.swipe(1033,1346,531,1346,20) | ||||||
|         UITestHelper.sleep(5000) |         UITestHelper.sleep(5000) | ||||||
|         Intents.intended(hasComponent(ProfileActivity::class.java.name)) |         Intents.intended(hasComponent(ProfileActivity::class.java.name)) | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -9,6 +9,7 @@ import org.junit.runner.RunWith | ||||||
| 
 | 
 | ||||||
| @RunWith(AndroidJUnit4::class) | @RunWith(AndroidJUnit4::class) | ||||||
| class ReviewActivityTest { | class ReviewActivityTest { | ||||||
|  | 
 | ||||||
|     @get:Rule |     @get:Rule | ||||||
|     var activityRule: ActivityTestRule<*> = ActivityTestRule(ReviewActivity::class.java) |     var activityRule: ActivityTestRule<*> = ActivityTestRule(ReviewActivity::class.java) | ||||||
| 
 | 
 | ||||||
|  | @ -16,4 +17,5 @@ class ReviewActivityTest { | ||||||
|     fun orientationChange() { |     fun orientationChange() { | ||||||
|         UITestHelper.changeOrientation(activityRule) |         UITestHelper.changeOrientation(activityRule) | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  | @ -16,6 +16,7 @@ import org.junit.runner.RunWith | ||||||
| 
 | 
 | ||||||
| @RunWith(AndroidJUnit4::class) | @RunWith(AndroidJUnit4::class) | ||||||
| class SearchActivityTest { | class SearchActivityTest { | ||||||
|  | 
 | ||||||
|     @get:Rule |     @get:Rule | ||||||
|     var activityRule = ActivityTestRule(SearchActivity::class.java) |     var activityRule = ActivityTestRule(SearchActivity::class.java) | ||||||
| 
 | 
 | ||||||
|  | @ -30,22 +31,21 @@ class SearchActivityTest { | ||||||
| 
 | 
 | ||||||
|     @Test |     @Test | ||||||
|     fun exploreActivityTest() { |     fun exploreActivityTest() { | ||||||
|         val searchAutoComplete = |         val searchAutoComplete = Espresso.onView( | ||||||
|             Espresso.onView( |             Matchers.allOf( | ||||||
|                 Matchers.allOf( |                 UITestHelper.childAtPosition( | ||||||
|                     UITestHelper.childAtPosition( |                     Matchers.allOf( | ||||||
|                         Matchers.allOf( |                         ViewMatchers.withClassName(Matchers.`is`("android.widget.LinearLayout")), | ||||||
|  |                         UITestHelper.childAtPosition( | ||||||
|                             ViewMatchers.withClassName(Matchers.`is`("android.widget.LinearLayout")), |                             ViewMatchers.withClassName(Matchers.`is`("android.widget.LinearLayout")), | ||||||
|                             UITestHelper.childAtPosition( |                             1 | ||||||
|                                 ViewMatchers.withClassName(Matchers.`is`("android.widget.LinearLayout")), |                         ) | ||||||
|                                 1, |  | ||||||
|                             ), |  | ||||||
|                         ), |  | ||||||
|                         0, |  | ||||||
|                     ), |                     ), | ||||||
|                     ViewMatchers.isDisplayed(), |                     0 | ||||||
|                 ), |                 ), | ||||||
|  |                 ViewMatchers.isDisplayed() | ||||||
|             ) |             ) | ||||||
|  |         ) | ||||||
|         searchAutoComplete.perform(ViewActions.replaceText("cat"), ViewActions.closeSoftKeyboard()) |         searchAutoComplete.perform(ViewActions.replaceText("cat"), ViewActions.closeSoftKeyboard()) | ||||||
|         UITestHelper.sleep(5000) |         UITestHelper.sleep(5000) | ||||||
|         device.swipe(1000, 1400, 500, 1400, 20) |         device.swipe(1000, 1400, 500, 1400, 20) | ||||||
|  |  | ||||||
|  | @ -22,6 +22,7 @@ import org.junit.runner.RunWith | ||||||
| 
 | 
 | ||||||
| @RunWith(AndroidJUnit4::class) | @RunWith(AndroidJUnit4::class) | ||||||
| class SettingsActivityLoggedInTest { | class SettingsActivityLoggedInTest { | ||||||
|  | 
 | ||||||
|     @get:Rule |     @get:Rule | ||||||
|     var activityRule: ActivityTestRule<*> = ActivityTestRule(LoginActivity::class.java) |     var activityRule: ActivityTestRule<*> = ActivityTestRule(LoginActivity::class.java) | ||||||
| 
 | 
 | ||||||
|  | @ -34,32 +35,31 @@ class SettingsActivityLoggedInTest { | ||||||
|         device.freezeRotation() |         device.freezeRotation() | ||||||
|         UITestHelper.loginUser() |         UITestHelper.loginUser() | ||||||
|         UITestHelper.skipWelcome() |         UITestHelper.skipWelcome() | ||||||
|         Intents |         Intents.intending(CoreMatchers.not(IntentMatchers.isInternal())) | ||||||
|             .intending(CoreMatchers.not(IntentMatchers.isInternal())) |  | ||||||
|             .respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null)) |             .respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null)) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Test |     @Test | ||||||
|     fun testSettings() { |     fun testSettings() { | ||||||
|         Espresso |         Espresso.onView( | ||||||
|             .onView( |             Matchers.allOf( | ||||||
|                 Matchers.allOf( |                 ViewMatchers.withContentDescription("More"), | ||||||
|                     ViewMatchers.withContentDescription("More"), |                 UITestHelper.childAtPosition( | ||||||
|                     UITestHelper.childAtPosition( |                     UITestHelper.childAtPosition( | ||||||
|                         UITestHelper.childAtPosition( |                         ViewMatchers.withId(R.id.fragment_main_nav_tab_layout), | ||||||
|                             ViewMatchers.withId(R.id.fragment_main_nav_tab_layout), |                         0 | ||||||
|                             0, |  | ||||||
|                         ), |  | ||||||
|                         4, |  | ||||||
|                     ), |                     ), | ||||||
|                     ViewMatchers.isDisplayed(), |                     4 | ||||||
|                 ), |                 ), | ||||||
|             ).perform(ViewActions.click()) |                 ViewMatchers.isDisplayed() | ||||||
|  |             ) | ||||||
|  |         ).perform(ViewActions.click()) | ||||||
|         Espresso.onView(Matchers.allOf(ViewMatchers.withId(R.id.more_settings))).perform( |         Espresso.onView(Matchers.allOf(ViewMatchers.withId(R.id.more_settings))).perform( | ||||||
|             ViewActions.scrollTo(), |             ViewActions.scrollTo(), | ||||||
|             ViewActions.click(), |             ViewActions.click() | ||||||
|         ) |         ) | ||||||
|         Intents.intended(IntentMatchers.hasComponent(SettingsActivity::class.java.name)) |         Intents.intended(IntentMatchers.hasComponent(SettingsActivity::class.java.name)) | ||||||
|         UITestHelper.sleep(1000) |         UITestHelper.sleep(1000) | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  | @ -23,6 +23,7 @@ import org.junit.runner.RunWith | ||||||
| 
 | 
 | ||||||
| @RunWith(AndroidJUnit4::class) | @RunWith(AndroidJUnit4::class) | ||||||
| class SettingsActivityTest { | class SettingsActivityTest { | ||||||
|  | 
 | ||||||
|     private lateinit var defaultKvStore: JsonKvStore |     private lateinit var defaultKvStore: JsonKvStore | ||||||
| 
 | 
 | ||||||
|     @get:Rule |     @get:Rule | ||||||
|  | @ -43,24 +44,22 @@ class SettingsActivityTest { | ||||||
|     fun useAuthorNameTogglesOn() { |     fun useAuthorNameTogglesOn() { | ||||||
|         // Turn on "Use author name" preference if currently off |         // Turn on "Use author name" preference if currently off | ||||||
|         if (!defaultKvStore.getBoolean("useAuthorName", false)) { |         if (!defaultKvStore.getBoolean("useAuthorName", false)) { | ||||||
|             Espresso |             Espresso.onView( | ||||||
|                 .onView( |  | ||||||
|                     allOf( |  | ||||||
|                         withId(R.id.recycler_view), |  | ||||||
|                         childAtPosition(withId(android.R.id.list_container), 0), |  | ||||||
|                     ), |  | ||||||
|                 ).perform( |  | ||||||
|                     RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(6, click()), |  | ||||||
|                 ) |  | ||||||
|         } |  | ||||||
|         // Check authorName preference is enabled |  | ||||||
|         Espresso |  | ||||||
|             .onView( |  | ||||||
|                 allOf( |                 allOf( | ||||||
|                     withId(R.id.recycler_view), |                     withId(R.id.recycler_view), | ||||||
|                     childAtPosition(withId(android.R.id.list_container), 0), |                     childAtPosition(withId(android.R.id.list_container), 0) | ||||||
|                 ), |                 ) | ||||||
|             ).check(matches(isEnabled())) |             ).perform( | ||||||
|  |                 RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(6, click()) | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |         // Check authorName preference is enabled | ||||||
|  |         Espresso.onView( | ||||||
|  |             allOf( | ||||||
|  |                 withId(R.id.recycler_view), | ||||||
|  |                 childAtPosition(withId(android.R.id.list_container), 0) | ||||||
|  |             ) | ||||||
|  |         ).check(matches(isEnabled())) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Test |     @Test | ||||||
|  |  | ||||||
|  | @ -10,20 +10,17 @@ import androidx.test.espresso.action.ViewActions | ||||||
| import androidx.test.espresso.matcher.ViewMatchers | import androidx.test.espresso.matcher.ViewMatchers | ||||||
| import androidx.test.rule.ActivityTestRule | import androidx.test.rule.ActivityTestRule | ||||||
| import org.apache.commons.lang3.StringUtils | import org.apache.commons.lang3.StringUtils | ||||||
| import org.hamcrest.BaseMatcher | import org.hamcrest.* | ||||||
| import org.hamcrest.Description |  | ||||||
| import org.hamcrest.Matcher |  | ||||||
| import org.hamcrest.Matchers |  | ||||||
| import org.hamcrest.TypeSafeMatcher |  | ||||||
| import timber.log.Timber | import timber.log.Timber | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| class UITestHelper { | class UITestHelper { | ||||||
|     companion object { |     companion object { | ||||||
|         fun skipWelcome() { |         fun skipWelcome() { | ||||||
|             try { |             try { | ||||||
|                 onView(ViewMatchers.withId(R.id.button_ok)) |                 onView(ViewMatchers.withId(R.id.button_ok)) | ||||||
|                     .perform(ViewActions.click()) |                     .perform(ViewActions.click()) | ||||||
|                 // Skip tutorial |                 //Skip tutorial | ||||||
|                 onView(ViewMatchers.withId(R.id.finishTutorialButton)) |                 onView(ViewMatchers.withId(R.id.finishTutorialButton)) | ||||||
|                     .perform(ViewActions.click()) |                     .perform(ViewActions.click()) | ||||||
|             } catch (ignored: NoMatchingViewException) { |             } catch (ignored: NoMatchingViewException) { | ||||||
|  | @ -32,31 +29,27 @@ class UITestHelper { | ||||||
| 
 | 
 | ||||||
|         fun skipLogin() { |         fun skipLogin() { | ||||||
|             try { |             try { | ||||||
|                 // Skip Login |                 //Skip Login | ||||||
|                 val htmlTextView = |                 val htmlTextView = onView( | ||||||
|                     onView( |                     Matchers.allOf( | ||||||
|                         Matchers.allOf( |                         ViewMatchers.withId(R.id.skip_login), ViewMatchers.withText("Skip"), | ||||||
|                             ViewMatchers.withId(R.id.skip_login), |                         ViewMatchers.isDisplayed() | ||||||
|                             ViewMatchers.withText("Skip"), |  | ||||||
|                             ViewMatchers.isDisplayed(), |  | ||||||
|                         ), |  | ||||||
|                     ) |                     ) | ||||||
|  |                 ) | ||||||
|                 htmlTextView.perform(ViewActions.click()) |                 htmlTextView.perform(ViewActions.click()) | ||||||
| 
 | 
 | ||||||
|                 val appCompatButton = |                 val appCompatButton = onView( | ||||||
|                     onView( |                     Matchers.allOf( | ||||||
|                         Matchers.allOf( |                         ViewMatchers.withId(android.R.id.button1), ViewMatchers.withText("Yes"), | ||||||
|                             ViewMatchers.withId(android.R.id.button1), |                         childAtPosition( | ||||||
|                             ViewMatchers.withText("Yes"), |  | ||||||
|                             childAtPosition( |                             childAtPosition( | ||||||
|                                 childAtPosition( |                                 ViewMatchers.withId(R.id.buttonPanel), | ||||||
|                                     ViewMatchers.withId(R.id.buttonPanel), |                                 0 | ||||||
|                                     0, |  | ||||||
|                                 ), |  | ||||||
|                                 3, |  | ||||||
|                             ), |                             ), | ||||||
|                         ), |                             3 | ||||||
|  |                         ) | ||||||
|                     ) |                     ) | ||||||
|  |                 ) | ||||||
|                 appCompatButton.perform(ViewActions.scrollTo(), ViewActions.click()) |                 appCompatButton.perform(ViewActions.scrollTo(), ViewActions.click()) | ||||||
|             } catch (ignored: NoMatchingViewException) { |             } catch (ignored: NoMatchingViewException) { | ||||||
|             } |             } | ||||||
|  | @ -64,18 +57,18 @@ class UITestHelper { | ||||||
| 
 | 
 | ||||||
|         fun loginUser() { |         fun loginUser() { | ||||||
|             try { |             try { | ||||||
|                 // Perform Login |                 //Perform Login | ||||||
|                 sleep(3000) |                 sleep(3000) | ||||||
|                 onView(ViewMatchers.withId(R.id.login_username)) |                 onView(ViewMatchers.withId(R.id.login_username)) | ||||||
|                     .perform( |                     .perform( | ||||||
|                         ViewActions.replaceText(getTestUsername()), |                         ViewActions.replaceText(getTestUsername()), | ||||||
|                         ViewActions.closeSoftKeyboard(), |                         ViewActions.closeSoftKeyboard() | ||||||
|                     ) |                     ) | ||||||
|                 sleep(2000) |                 sleep(2000) | ||||||
|                 onView(ViewMatchers.withId(R.id.login_password)) |                 onView(ViewMatchers.withId(R.id.login_password)) | ||||||
|                     .perform( |                     .perform( | ||||||
|                         ViewActions.replaceText(getTestUserPassword()), |                         ViewActions.replaceText(getTestUserPassword()), | ||||||
|                         ViewActions.closeSoftKeyboard(), |                         ViewActions.closeSoftKeyboard() | ||||||
|                     ) |                     ) | ||||||
|                 sleep(2000) |                 sleep(2000) | ||||||
|                 onView(ViewMatchers.withId(R.id.login_button)) |                 onView(ViewMatchers.withId(R.id.login_button)) | ||||||
|  | @ -83,6 +76,7 @@ class UITestHelper { | ||||||
|                 sleep(10000) |                 sleep(10000) | ||||||
|             } catch (ignored: NoMatchingViewException) { |             } catch (ignored: NoMatchingViewException) { | ||||||
|             } |             } | ||||||
|  | 
 | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         fun logoutUser() { |         fun logoutUser() { | ||||||
|  | @ -93,38 +87,36 @@ class UITestHelper { | ||||||
|                         childAtPosition( |                         childAtPosition( | ||||||
|                             childAtPosition( |                             childAtPosition( | ||||||
|                                 ViewMatchers.withId(R.id.fragment_main_nav_tab_layout), |                                 ViewMatchers.withId(R.id.fragment_main_nav_tab_layout), | ||||||
|                                 0, |                                 0 | ||||||
|                             ), |                             ), | ||||||
|                             4, |                             4 | ||||||
|                         ), |                         ), | ||||||
|                         ViewMatchers.isDisplayed(), |                         ViewMatchers.isDisplayed() | ||||||
|                     ), |                     ) | ||||||
|                 ).perform(ViewActions.click()) |                 ).perform(ViewActions.click()) | ||||||
|                 onView( |                 onView( | ||||||
|                     Matchers.allOf( |                     Matchers.allOf( | ||||||
|                         ViewMatchers.withId(R.id.more_logout), |                         ViewMatchers.withId(R.id.more_logout), ViewMatchers.withText("Logout"), | ||||||
|                         ViewMatchers.withText("Logout"), |  | ||||||
|                         childAtPosition( |                         childAtPosition( | ||||||
|                             childAtPosition( |                             childAtPosition( | ||||||
|                                 ViewMatchers.withId(R.id.scroll_view_more_bottom_sheet), |                                 ViewMatchers.withId(R.id.scroll_view_more_bottom_sheet), | ||||||
|                                 0, |                                 0 | ||||||
|                             ), |                             ), | ||||||
|                             6, |                             6 | ||||||
|                         ), |                         ) | ||||||
|                     ), |                     ) | ||||||
|                 ).perform(ViewActions.scrollTo(), ViewActions.click()) |                 ).perform(ViewActions.scrollTo(), ViewActions.click()) | ||||||
|                 onView( |                 onView( | ||||||
|                     Matchers.allOf( |                     Matchers.allOf( | ||||||
|                         ViewMatchers.withId(android.R.id.button1), |                         ViewMatchers.withId(android.R.id.button1), ViewMatchers.withText("Yes"), | ||||||
|                         ViewMatchers.withText("Yes"), |  | ||||||
|                         childAtPosition( |                         childAtPosition( | ||||||
|                             childAtPosition( |                             childAtPosition( | ||||||
|                                 ViewMatchers.withId(R.id.buttonPanel), |                                 ViewMatchers.withId(R.id.buttonPanel), | ||||||
|                                 0, |                                 0 | ||||||
|                             ), |                             ), | ||||||
|                             3, |                             3 | ||||||
|                         ), |                         ) | ||||||
|                     ), |                     ) | ||||||
|                 ).perform(ViewActions.scrollTo(), ViewActions.click()) |                 ).perform(ViewActions.scrollTo(), ViewActions.click()) | ||||||
|                 sleep(5000) |                 sleep(5000) | ||||||
|             } catch (ignored: NoMatchingViewException) { |             } catch (ignored: NoMatchingViewException) { | ||||||
|  | @ -132,9 +124,9 @@ class UITestHelper { | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         fun childAtPosition( |         fun childAtPosition( | ||||||
|             parentMatcher: Matcher<View>, |             parentMatcher: Matcher<View>, position: Int | ||||||
|             position: Int, |  | ||||||
|         ): Matcher<View> { |         ): Matcher<View> { | ||||||
|  | 
 | ||||||
|             return object : TypeSafeMatcher<View>() { |             return object : TypeSafeMatcher<View>() { | ||||||
|                 override fun describeTo(description: Description) { |                 override fun describeTo(description: Description) { | ||||||
|                     description.appendText("Child at position $position in parent ") |                     description.appendText("Child at position $position in parent ") | ||||||
|  | @ -143,9 +135,8 @@ class UITestHelper { | ||||||
| 
 | 
 | ||||||
|                 public override fun matchesSafely(view: View): Boolean { |                 public override fun matchesSafely(view: View): Boolean { | ||||||
|                     val parent = view.parent |                     val parent = view.parent | ||||||
|                     return parent is ViewGroup && |                     return parent is ViewGroup && parentMatcher.matches(parent) | ||||||
|                         parentMatcher.matches(parent) && |                             && view == parent.getChildAt(position) | ||||||
|                         view == parent.getChildAt(position) |  | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  | @ -163,18 +154,14 @@ class UITestHelper { | ||||||
|             val username = BuildConfig.TEST_USERNAME |             val username = BuildConfig.TEST_USERNAME | ||||||
|             if (StringUtils.isEmpty(username) || username == "null") { |             if (StringUtils.isEmpty(username) || username == "null") { | ||||||
|                 throw NotImplementedError("Configure your beta account's username") |                 throw NotImplementedError("Configure your beta account's username") | ||||||
|             } else { |             } else return username | ||||||
|                 return username |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         private fun getTestUserPassword(): String { |         private fun getTestUserPassword(): String { | ||||||
|             val password = BuildConfig.TEST_PASSWORD |             val password = BuildConfig.TEST_PASSWORD | ||||||
|             if (StringUtils.isEmpty(password) || password == "null") { |             if (StringUtils.isEmpty(password) || password == "null") { | ||||||
|                 throw NotImplementedError("Configure your beta account's password") |                 throw NotImplementedError("Configure your beta account's password") | ||||||
|             } else { |             } else return password | ||||||
|                 return password |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         fun <T : Activity> changeOrientation(activityRule: ActivityTestRule<T>) { |         fun <T : Activity> changeOrientation(activityRule: ActivityTestRule<T>) { | ||||||
|  | @ -187,7 +174,6 @@ class UITestHelper { | ||||||
|         fun <T> first(matcher: Matcher<T>): Matcher<T>? { |         fun <T> first(matcher: Matcher<T>): Matcher<T>? { | ||||||
|             return object : BaseMatcher<T>() { |             return object : BaseMatcher<T>() { | ||||||
|                 var isFirst = true |                 var isFirst = true | ||||||
| 
 |  | ||||||
|                 override fun matches(item: Any): Boolean { |                 override fun matches(item: Any): Boolean { | ||||||
|                     if (isFirst && matcher.matches(item)) { |                     if (isFirst && matcher.matches(item)) { | ||||||
|                         isFirst = false |                         isFirst = false | ||||||
|  |  | ||||||
|  | @ -4,10 +4,7 @@ import android.app.Activity | ||||||
| import android.app.Instrumentation | import android.app.Instrumentation | ||||||
| import androidx.recyclerview.widget.RecyclerView | import androidx.recyclerview.widget.RecyclerView | ||||||
| import androidx.test.espresso.Espresso.onView | import androidx.test.espresso.Espresso.onView | ||||||
| import androidx.test.espresso.action.ViewActions.click | import androidx.test.espresso.action.ViewActions.* | ||||||
| import androidx.test.espresso.action.ViewActions.closeSoftKeyboard |  | ||||||
| import androidx.test.espresso.action.ViewActions.replaceText |  | ||||||
| import androidx.test.espresso.action.ViewActions.scrollTo |  | ||||||
| import androidx.test.espresso.contrib.RecyclerViewActions | import androidx.test.espresso.contrib.RecyclerViewActions | ||||||
| import androidx.test.espresso.intent.Intents | import androidx.test.espresso.intent.Intents | ||||||
| import androidx.test.espresso.intent.matcher.IntentMatchers | import androidx.test.espresso.intent.matcher.IntentMatchers | ||||||
|  | @ -18,7 +15,7 @@ import androidx.test.platform.app.InstrumentationRegistry | ||||||
| import androidx.test.rule.ActivityTestRule | import androidx.test.rule.ActivityTestRule | ||||||
| import androidx.test.rule.GrantPermissionRule | import androidx.test.rule.GrantPermissionRule | ||||||
| import androidx.test.uiautomator.UiDevice | import androidx.test.uiautomator.UiDevice | ||||||
| import fr.free.nrw.commons.locationpicker.LocationPickerActivity | import fr.free.nrw.commons.LocationPicker.LocationPickerActivity | ||||||
| import fr.free.nrw.commons.UITestHelper.Companion.childAtPosition | import fr.free.nrw.commons.UITestHelper.Companion.childAtPosition | ||||||
| import fr.free.nrw.commons.auth.LoginActivity | import fr.free.nrw.commons.auth.LoginActivity | ||||||
| import org.hamcrest.CoreMatchers | import org.hamcrest.CoreMatchers | ||||||
|  | @ -31,6 +28,7 @@ import org.junit.runner.RunWith | ||||||
| 
 | 
 | ||||||
| @RunWith(AndroidJUnit4::class) | @RunWith(AndroidJUnit4::class) | ||||||
| class UploadCancelledTest { | class UploadCancelledTest { | ||||||
|  | 
 | ||||||
|     @Rule |     @Rule | ||||||
|     @JvmField |     @JvmField | ||||||
|     var mActivityTestRule = ActivityTestRule(LoginActivity::class.java) |     var mActivityTestRule = ActivityTestRule(LoginActivity::class.java) | ||||||
|  | @ -39,7 +37,7 @@ class UploadCancelledTest { | ||||||
|     @JvmField |     @JvmField | ||||||
|     var mGrantPermissionRule: GrantPermissionRule = |     var mGrantPermissionRule: GrantPermissionRule = | ||||||
|         GrantPermissionRule.grant( |         GrantPermissionRule.grant( | ||||||
|             "android.permission.WRITE_EXTERNAL_STORAGE", |             "android.permission.WRITE_EXTERNAL_STORAGE" | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|     private val device: UiDevice = |     private val device: UiDevice = | ||||||
|  | @ -49,15 +47,15 @@ class UploadCancelledTest { | ||||||
|     fun setup() { |     fun setup() { | ||||||
|         try { |         try { | ||||||
|             Intents.init() |             Intents.init() | ||||||
|         } catch (_: IllegalStateException) { |         } catch (ex: IllegalStateException) { | ||||||
|  | 
 | ||||||
|         } |         } | ||||||
|         device.unfreezeRotation() |         device.unfreezeRotation() | ||||||
|         device.setOrientationNatural() |         device.setOrientationNatural() | ||||||
|         device.freezeRotation() |         device.freezeRotation() | ||||||
|         UITestHelper.loginUser() |         UITestHelper.loginUser() | ||||||
|         UITestHelper.skipWelcome() |         UITestHelper.skipWelcome() | ||||||
|         Intents |         Intents.intending(CoreMatchers.not(IntentMatchers.isInternal())) | ||||||
|             .intending(CoreMatchers.not(IntentMatchers.isInternal())) |  | ||||||
|             .respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null)) |             .respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null)) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -65,138 +63,131 @@ class UploadCancelledTest { | ||||||
|     fun teardown() { |     fun teardown() { | ||||||
|         try { |         try { | ||||||
|             Intents.release() |             Intents.release() | ||||||
|         } catch (_: IllegalStateException) { |         } catch (ex: IllegalStateException) { | ||||||
|  | 
 | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Test |     @Test | ||||||
|     fun uploadCancelledAfterLocationPickedTest() { |     fun uploadCancelledAfterLocationPickedTest() { | ||||||
|         val bottomNavigationItemView = | 
 | ||||||
|             onView( |         val bottomNavigationItemView = onView( | ||||||
|                 allOf( |             allOf( | ||||||
|  |                 childAtPosition( | ||||||
|                     childAtPosition( |                     childAtPosition( | ||||||
|                         childAtPosition( |                         withId(R.id.fragment_main_nav_tab_layout), | ||||||
|                             withId(R.id.fragment_main_nav_tab_layout), |                         0 | ||||||
|                             0, |  | ||||||
|                         ), |  | ||||||
|                         1, |  | ||||||
|                     ), |                     ), | ||||||
|                     isDisplayed(), |                     1 | ||||||
|                 ), |                 ), | ||||||
|  |                 isDisplayed() | ||||||
|             ) |             ) | ||||||
|  |         ) | ||||||
|         bottomNavigationItemView.perform(click()) |         bottomNavigationItemView.perform(click()) | ||||||
| 
 | 
 | ||||||
|         UITestHelper.sleep(12000) |         UITestHelper.sleep(12000) | ||||||
| 
 | 
 | ||||||
|         val actionMenuItemView = |         val actionMenuItemView = onView( | ||||||
|             onView( |             allOf( | ||||||
|                 allOf( |                 withId(R.id.list_sheet), | ||||||
|                     withId(R.id.list_sheet), |                 childAtPosition( | ||||||
|                     childAtPosition( |                     childAtPosition( | ||||||
|                         childAtPosition( |                         withId(R.id.toolbar), | ||||||
|                             withId(R.id.toolbar), |                         1 | ||||||
|                             1, |  | ||||||
|                         ), |  | ||||||
|                         0, |  | ||||||
|                     ), |                     ), | ||||||
|                     isDisplayed(), |                     0 | ||||||
|                 ), |                 ), | ||||||
|  |                 isDisplayed() | ||||||
|             ) |             ) | ||||||
|  |         ) | ||||||
|         actionMenuItemView.perform(click()) |         actionMenuItemView.perform(click()) | ||||||
| 
 | 
 | ||||||
|         val recyclerView = |         val recyclerView = onView( | ||||||
|             onView( |             allOf( | ||||||
|                 allOf( |                 withId(R.id.rv_nearby_list), | ||||||
|                     withId(R.id.rv_nearby_list), |  | ||||||
|                 ), |  | ||||||
|             ) |             ) | ||||||
|  |         ) | ||||||
|         recyclerView.perform( |         recyclerView.perform( | ||||||
|             RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>( |             RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>( | ||||||
|                 0, |                 0, | ||||||
|                 click(), |                 click() | ||||||
|             ), |             ) | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|         val linearLayout3 = |         val linearLayout3 = onView( | ||||||
|             onView( |             allOf( | ||||||
|                 allOf( |                 withId(R.id.cameraButton), | ||||||
|                     withId(R.id.cameraButton), |                 childAtPosition( | ||||||
|                     childAtPosition( |                     allOf( | ||||||
|                         allOf( |                         withId(R.id.nearby_button_layout), | ||||||
|                             withId(R.id.nearby_button_layout), |  | ||||||
|                         ), |  | ||||||
|                         0, |  | ||||||
|                     ), |                     ), | ||||||
|                     isDisplayed(), |                     0 | ||||||
|                 ), |                 ), | ||||||
|  |                 isDisplayed() | ||||||
|             ) |             ) | ||||||
|  |         ) | ||||||
|         linearLayout3.perform(click()) |         linearLayout3.perform(click()) | ||||||
| 
 | 
 | ||||||
|         val pasteSensitiveTextInputEditText = |         val pasteSensitiveTextInputEditText = onView( | ||||||
|             onView( |             allOf( | ||||||
|                 allOf( |                 withId(R.id.caption_item_edit_text), | ||||||
|                     withId(R.id.caption_item_edit_text), |                 childAtPosition( | ||||||
|                     childAtPosition( |                     childAtPosition( | ||||||
|                         childAtPosition( |                         withId(R.id.caption_item_edit_text_input_layout), | ||||||
|                             withId(R.id.caption_item_edit_text_input_layout), |                         0 | ||||||
|                             0, |  | ||||||
|                         ), |  | ||||||
|                         0, |  | ||||||
|                     ), |                     ), | ||||||
|                     isDisplayed(), |                     0 | ||||||
|                 ), |                 ), | ||||||
|  |                 isDisplayed() | ||||||
|             ) |             ) | ||||||
|  |         ) | ||||||
|         pasteSensitiveTextInputEditText.perform(replaceText("test"), closeSoftKeyboard()) |         pasteSensitiveTextInputEditText.perform(replaceText("test"), closeSoftKeyboard()) | ||||||
| 
 | 
 | ||||||
|         val pasteSensitiveTextInputEditText2 = |         val pasteSensitiveTextInputEditText2 = onView( | ||||||
|             onView( |             allOf( | ||||||
|                 allOf( |                 withId(R.id.description_item_edit_text), | ||||||
|                     withId(R.id.description_item_edit_text), |                 childAtPosition( | ||||||
|                     childAtPosition( |                     childAtPosition( | ||||||
|                         childAtPosition( |                         withId(R.id.description_item_edit_text_input_layout), | ||||||
|                             withId(R.id.description_item_edit_text_input_layout), |                         0 | ||||||
|                             0, |  | ||||||
|                         ), |  | ||||||
|                         0, |  | ||||||
|                     ), |                     ), | ||||||
|                     isDisplayed(), |                     0 | ||||||
|                 ), |                 ), | ||||||
|  |                 isDisplayed() | ||||||
|             ) |             ) | ||||||
|  |         ) | ||||||
|         pasteSensitiveTextInputEditText2.perform(replaceText("test"), closeSoftKeyboard()) |         pasteSensitiveTextInputEditText2.perform(replaceText("test"), closeSoftKeyboard()) | ||||||
| 
 | 
 | ||||||
|         val appCompatButton2 = |         val appCompatButton2 = onView( | ||||||
|             onView( |             allOf( | ||||||
|                 allOf( |                 withId(R.id.btn_next), | ||||||
|                     withId(R.id.btn_next), |                 childAtPosition( | ||||||
|                     childAtPosition( |                     childAtPosition( | ||||||
|                         childAtPosition( |                         withId(R.id.ll_container_media_detail), | ||||||
|                             withId(R.id.ll_container_media_detail), |                         2 | ||||||
|                             2, |  | ||||||
|                         ), |  | ||||||
|                         1, |  | ||||||
|                     ), |                     ), | ||||||
|                     isDisplayed(), |                     1 | ||||||
|                 ), |                 ), | ||||||
|  |                 isDisplayed() | ||||||
|             ) |             ) | ||||||
|  |         ) | ||||||
|         appCompatButton2.perform(click()) |         appCompatButton2.perform(click()) | ||||||
| 
 | 
 | ||||||
|         val appCompatButton3 = |         val appCompatButton3 = onView( | ||||||
|             onView( |             allOf( | ||||||
|                 allOf( |                 withId(android.R.id.button1), | ||||||
|                     withId(android.R.id.button1), |  | ||||||
|                 ), |  | ||||||
|             ) |             ) | ||||||
|  |         ) | ||||||
|         appCompatButton3.perform(scrollTo(), click()) |         appCompatButton3.perform(scrollTo(), click()) | ||||||
| 
 | 
 | ||||||
|         Intents.intended(IntentMatchers.hasComponent(LocationPickerActivity::class.java.name)) |         Intents.intended(IntentMatchers.hasComponent(LocationPickerActivity::class.java.name)) | ||||||
| 
 | 
 | ||||||
|         val floatingActionButton3 = |         val floatingActionButton3 = onView( | ||||||
|             onView( |             allOf( | ||||||
|                 allOf( |                 withId(R.id.location_chosen_button), | ||||||
|                     withId(R.id.location_chosen_button), |                 isDisplayed() | ||||||
|                     isDisplayed(), |  | ||||||
|                 ), |  | ||||||
|             ) |             ) | ||||||
|  |         ) | ||||||
|         UITestHelper.sleep(2000) |         UITestHelper.sleep(2000) | ||||||
|         floatingActionButton3.perform(click()) |         floatingActionButton3.perform(click()) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -19,10 +19,7 @@ import androidx.test.espresso.intent.Intents.intended | ||||||
| import androidx.test.espresso.intent.Intents.intending | import androidx.test.espresso.intent.Intents.intending | ||||||
| import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction | import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction | ||||||
| import androidx.test.espresso.intent.matcher.IntentMatchers.hasType | import androidx.test.espresso.intent.matcher.IntentMatchers.hasType | ||||||
| import androidx.test.espresso.matcher.ViewMatchers.isDisplayed | import androidx.test.espresso.matcher.ViewMatchers.* | ||||||
| import androidx.test.espresso.matcher.ViewMatchers.withId |  | ||||||
| import androidx.test.espresso.matcher.ViewMatchers.withParent |  | ||||||
| import androidx.test.espresso.matcher.ViewMatchers.withText |  | ||||||
| import androidx.test.ext.junit.runners.AndroidJUnit4 | import androidx.test.ext.junit.runners.AndroidJUnit4 | ||||||
| import androidx.test.filters.LargeTest | import androidx.test.filters.LargeTest | ||||||
| import androidx.test.rule.ActivityTestRule | import androidx.test.rule.ActivityTestRule | ||||||
|  | @ -32,29 +29,21 @@ import fr.free.nrw.commons.upload.UploadMediaDetailAdapter | ||||||
| import fr.free.nrw.commons.util.MyViewAction | import fr.free.nrw.commons.util.MyViewAction | ||||||
| import fr.free.nrw.commons.utils.ConfigUtils | import fr.free.nrw.commons.utils.ConfigUtils | ||||||
| import org.hamcrest.core.AllOf.allOf | import org.hamcrest.core.AllOf.allOf | ||||||
| import org.junit.After | import org.junit.* | ||||||
| import org.junit.Before |  | ||||||
| import org.junit.Ignore |  | ||||||
| import org.junit.Rule |  | ||||||
| import org.junit.Test |  | ||||||
| import org.junit.runner.RunWith | import org.junit.runner.RunWith | ||||||
| import timber.log.Timber | import timber.log.Timber | ||||||
| import java.io.File | import java.io.File | ||||||
| import java.io.FileOutputStream | import java.io.FileOutputStream | ||||||
| import java.io.IOException | import java.io.IOException | ||||||
| import java.text.SimpleDateFormat | import java.text.SimpleDateFormat | ||||||
| import java.util.Date | import java.util.* | ||||||
| import java.util.Random |  | ||||||
| 
 | 
 | ||||||
| @LargeTest | @LargeTest | ||||||
| @RunWith(AndroidJUnit4::class) | @RunWith(AndroidJUnit4::class) | ||||||
| class UploadTest { | class UploadTest { | ||||||
|     @get:Rule |     @get:Rule | ||||||
|     var permissionRule = |     var permissionRule = GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE, | ||||||
|         GrantPermissionRule.grant( |             Manifest.permission.ACCESS_FINE_LOCATION)!! | ||||||
|             Manifest.permission.WRITE_EXTERNAL_STORAGE, |  | ||||||
|             Manifest.permission.ACCESS_FINE_LOCATION, |  | ||||||
|         )!! |  | ||||||
| 
 | 
 | ||||||
|     @get:Rule |     @get:Rule | ||||||
|     var activityRule = ActivityTestRule(LoginActivity::class.java) |     var activityRule = ActivityTestRule(LoginActivity::class.java) | ||||||
|  | @ -71,7 +60,8 @@ class UploadTest { | ||||||
|     fun setup() { |     fun setup() { | ||||||
|         try { |         try { | ||||||
|             Intents.init() |             Intents.init() | ||||||
|         } catch (_: IllegalStateException) { |         } catch (ex: IllegalStateException) { | ||||||
|  | 
 | ||||||
|         } |         } | ||||||
|         UITestHelper.loginUser() |         UITestHelper.loginUser() | ||||||
|         UITestHelper.skipWelcome() |         UITestHelper.skipWelcome() | ||||||
|  | @ -104,13 +94,14 @@ class UploadTest { | ||||||
|         dismissWarning("Yes") |         dismissWarning("Yes") | ||||||
| 
 | 
 | ||||||
|         onView(allOf<View>(isDisplayed(), withId(R.id.tv_title))) |         onView(allOf<View>(isDisplayed(), withId(R.id.tv_title))) | ||||||
|             .perform(replaceText(commonsFileName)) |                 .perform(replaceText(commonsFileName)) | ||||||
| 
 | 
 | ||||||
|         onView(allOf<View>(isDisplayed(), withId(R.id.description_item_edit_text))) |         onView(allOf<View>(isDisplayed(), withId(R.id.description_item_edit_text))) | ||||||
|             .perform(replaceText(commonsFileName)) |                 .perform(replaceText(commonsFileName)) | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
|         onView(allOf(isDisplayed(), withId(R.id.btn_next))) |         onView(allOf(isDisplayed(), withId(R.id.btn_next))) | ||||||
|             .perform(click()) |                 .perform(click()) | ||||||
| 
 | 
 | ||||||
|         UITestHelper.sleep(5000) |         UITestHelper.sleep(5000) | ||||||
|         dismissWarning("Yes") |         dismissWarning("Yes") | ||||||
|  | @ -118,30 +109,29 @@ class UploadTest { | ||||||
|         UITestHelper.sleep(3000) |         UITestHelper.sleep(3000) | ||||||
| 
 | 
 | ||||||
|         onView(allOf(isDisplayed(), withId(R.id.et_search))) |         onView(allOf(isDisplayed(), withId(R.id.et_search))) | ||||||
|             .perform(replaceText("Uploaded with Mobile/Android Tests")) |                 .perform(replaceText("Uploaded with Mobile/Android Tests")) | ||||||
| 
 | 
 | ||||||
|         UITestHelper.sleep(3000) |         UITestHelper.sleep(3000) | ||||||
| 
 | 
 | ||||||
|         try { |         try { | ||||||
|             onView(allOf(isDisplayed(), UITestHelper.first(withParent(withId(R.id.rv_categories))))) |             onView(allOf(isDisplayed(), UITestHelper.first(withParent(withId(R.id.rv_categories))))) | ||||||
|                 .perform(click()) |                     .perform(click()) | ||||||
|         } catch (ignored: NoMatchingViewException) { |         } catch (ignored: NoMatchingViewException) { | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         onView(allOf(isDisplayed(), withId(R.id.btn_next))) |         onView(allOf(isDisplayed(), withId(R.id.btn_next))) | ||||||
|             .perform(click()) |                 .perform(click()) | ||||||
| 
 | 
 | ||||||
|         dismissWarning("Yes, Submit") |         dismissWarning("Yes, Submit") | ||||||
| 
 | 
 | ||||||
|         UITestHelper.sleep(500) |         UITestHelper.sleep(500) | ||||||
| 
 | 
 | ||||||
|         onView(allOf(isDisplayed(), withId(R.id.btn_submit))) |         onView(allOf(isDisplayed(), withId(R.id.btn_submit))) | ||||||
|             .perform(click()) |                 .perform(click()) | ||||||
| 
 | 
 | ||||||
|         UITestHelper.sleep(10000) |         UITestHelper.sleep(10000) | ||||||
| 
 | 
 | ||||||
|         val fileUrl = |         val fileUrl = "https://commons.wikimedia.beta.wmflabs.org/wiki/File:" + | ||||||
|             "https://commons.wikimedia.beta.wmflabs.org/wiki/File:" + |  | ||||||
|                 commonsFileName.replace(' ', '_') + ".jpg" |                 commonsFileName.replace(' ', '_') + ".jpg" | ||||||
|         Timber.i("File should be uploaded to $fileUrl") |         Timber.i("File should be uploaded to $fileUrl") | ||||||
|     } |     } | ||||||
|  | @ -149,8 +139,8 @@ class UploadTest { | ||||||
|     private fun dismissWarning(warningText: String) { |     private fun dismissWarning(warningText: String) { | ||||||
|         try { |         try { | ||||||
|             onView(withText(warningText)) |             onView(withText(warningText)) | ||||||
|                 .check(matches(isDisplayed())) |                     .check(matches(isDisplayed())) | ||||||
|                 .perform(click()) |                     .perform(click()) | ||||||
|         } catch (ignored: NoMatchingViewException) { |         } catch (ignored: NoMatchingViewException) { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | @ -177,10 +167,10 @@ class UploadTest { | ||||||
|         dismissWarning("Yes") |         dismissWarning("Yes") | ||||||
| 
 | 
 | ||||||
|         onView(allOf<View>(isDisplayed(), withId(R.id.tv_title))) |         onView(allOf<View>(isDisplayed(), withId(R.id.tv_title))) | ||||||
|             .perform(replaceText(commonsFileName)) |                 .perform(replaceText(commonsFileName)) | ||||||
| 
 | 
 | ||||||
|         onView(allOf(isDisplayed(), withId(R.id.btn_next))) |         onView(allOf(isDisplayed(), withId(R.id.btn_next))) | ||||||
|             .perform(click()) |                 .perform(click()) | ||||||
| 
 | 
 | ||||||
|         UITestHelper.sleep(10000) |         UITestHelper.sleep(10000) | ||||||
|         dismissWarning("Yes") |         dismissWarning("Yes") | ||||||
|  | @ -188,30 +178,29 @@ class UploadTest { | ||||||
|         UITestHelper.sleep(3000) |         UITestHelper.sleep(3000) | ||||||
| 
 | 
 | ||||||
|         onView(allOf(isDisplayed(), withId(R.id.et_search))) |         onView(allOf(isDisplayed(), withId(R.id.et_search))) | ||||||
|             .perform(replaceText("Test")) |                 .perform(replaceText("Test")) | ||||||
| 
 | 
 | ||||||
|         UITestHelper.sleep(3000) |         UITestHelper.sleep(3000) | ||||||
| 
 | 
 | ||||||
|         try { |         try { | ||||||
|             onView(allOf(isDisplayed(), UITestHelper.first(withParent(withId(R.id.rv_categories))))) |             onView(allOf(isDisplayed(), UITestHelper.first(withParent(withId(R.id.rv_categories))))) | ||||||
|                 .perform(click()) |                     .perform(click()) | ||||||
|         } catch (ignored: NoMatchingViewException) { |         } catch (ignored: NoMatchingViewException) { | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         onView(allOf(isDisplayed(), withId(R.id.btn_next))) |         onView(allOf(isDisplayed(), withId(R.id.btn_next))) | ||||||
|             .perform(click()) |                 .perform(click()) | ||||||
| 
 | 
 | ||||||
|         dismissWarning("Yes, Submit") |         dismissWarning("Yes, Submit") | ||||||
| 
 | 
 | ||||||
|         UITestHelper.sleep(500) |         UITestHelper.sleep(500) | ||||||
| 
 | 
 | ||||||
|         onView(allOf(isDisplayed(), withId(R.id.btn_submit))) |         onView(allOf(isDisplayed(), withId(R.id.btn_submit))) | ||||||
|             .perform(click()) |                 .perform(click()) | ||||||
| 
 | 
 | ||||||
|         UITestHelper.sleep(10000) |         UITestHelper.sleep(10000) | ||||||
| 
 | 
 | ||||||
|         val fileUrl = |         val fileUrl = "https://commons.wikimedia.beta.wmflabs.org/wiki/File:" + | ||||||
|             "https://commons.wikimedia.beta.wmflabs.org/wiki/File:" + |  | ||||||
|                 commonsFileName.replace(' ', '_') + ".jpg" |                 commonsFileName.replace(' ', '_') + ".jpg" | ||||||
|         Timber.i("File should be uploaded to $fileUrl") |         Timber.i("File should be uploaded to $fileUrl") | ||||||
|     } |     } | ||||||
|  | @ -238,29 +227,23 @@ class UploadTest { | ||||||
|         dismissWarningDialog() |         dismissWarningDialog() | ||||||
| 
 | 
 | ||||||
|         onView(allOf<View>(isDisplayed(), withId(R.id.tv_title))) |         onView(allOf<View>(isDisplayed(), withId(R.id.tv_title))) | ||||||
|             .perform(replaceText(commonsFileName)) |                 .perform(replaceText(commonsFileName)) | ||||||
| 
 | 
 | ||||||
|         onView(withId(R.id.rv_descriptions)).perform( |         onView(withId(R.id.rv_descriptions)).perform( | ||||||
|             RecyclerViewActions |                 RecyclerViewActions | ||||||
|                 .actionOnItemAtPosition<UploadMediaDetailAdapter.ViewHolder>( |                         .actionOnItemAtPosition<UploadMediaDetailAdapter.ViewHolder>(0, | ||||||
|                     0, |                                 MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Test description"))) | ||||||
|                     MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Test description"), |  | ||||||
|                 ), |  | ||||||
|         ) |  | ||||||
| 
 | 
 | ||||||
|         onView(withId(R.id.btn_add)) |         onView(withId(R.id.btn_add_description)) | ||||||
|             .perform(click()) |                 .perform(click()) | ||||||
| 
 | 
 | ||||||
|         onView(withId(R.id.rv_descriptions)).perform( |         onView(withId(R.id.rv_descriptions)).perform( | ||||||
|             RecyclerViewActions |                 RecyclerViewActions | ||||||
|                 .actionOnItemAtPosition<UploadMediaDetailAdapter.ViewHolder>( |                         .actionOnItemAtPosition<UploadMediaDetailAdapter.ViewHolder>(1, | ||||||
|                     1, |                                 MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Description"))) | ||||||
|                     MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Description"), |  | ||||||
|                 ), |  | ||||||
|         ) |  | ||||||
| 
 | 
 | ||||||
|         onView(allOf(isDisplayed(), withId(R.id.btn_next))) |         onView(allOf(isDisplayed(), withId(R.id.btn_next))) | ||||||
|             .perform(click()) |                 .perform(click()) | ||||||
| 
 | 
 | ||||||
|         UITestHelper.sleep(5000) |         UITestHelper.sleep(5000) | ||||||
|         dismissWarning("Yes") |         dismissWarning("Yes") | ||||||
|  | @ -268,30 +251,29 @@ class UploadTest { | ||||||
|         UITestHelper.sleep(3000) |         UITestHelper.sleep(3000) | ||||||
| 
 | 
 | ||||||
|         onView(allOf(isDisplayed(), withId(R.id.et_search))) |         onView(allOf(isDisplayed(), withId(R.id.et_search))) | ||||||
|             .perform(replaceText("Test")) |                 .perform(replaceText("Test")) | ||||||
| 
 | 
 | ||||||
|         UITestHelper.sleep(3000) |         UITestHelper.sleep(3000) | ||||||
| 
 | 
 | ||||||
|         try { |         try { | ||||||
|             onView(allOf(isDisplayed(), UITestHelper.first(withParent(withId(R.id.rv_categories))))) |             onView(allOf(isDisplayed(), UITestHelper.first(withParent(withId(R.id.rv_categories))))) | ||||||
|                 .perform(click()) |                     .perform(click()) | ||||||
|         } catch (ignored: NoMatchingViewException) { |         } catch (ignored: NoMatchingViewException) { | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         onView(allOf(isDisplayed(), withId(R.id.btn_next))) |         onView(allOf(isDisplayed(), withId(R.id.btn_next))) | ||||||
|             .perform(click()) |                 .perform(click()) | ||||||
| 
 | 
 | ||||||
|         dismissWarning("Yes, Submit") |         dismissWarning("Yes, Submit") | ||||||
| 
 | 
 | ||||||
|         UITestHelper.sleep(500) |         UITestHelper.sleep(500) | ||||||
| 
 | 
 | ||||||
|         onView(allOf(isDisplayed(), withId(R.id.btn_submit))) |         onView(allOf(isDisplayed(), withId(R.id.btn_submit))) | ||||||
|             .perform(click()) |                 .perform(click()) | ||||||
| 
 | 
 | ||||||
|         UITestHelper.sleep(10000) |         UITestHelper.sleep(10000) | ||||||
| 
 | 
 | ||||||
|         val fileUrl = |         val fileUrl = "https://commons.wikimedia.beta.wmflabs.org/wiki/File:" + | ||||||
|             "https://commons.wikimedia.beta.wmflabs.org/wiki/File:" + |  | ||||||
|                 commonsFileName.replace(' ', '_') + ".jpg" |                 commonsFileName.replace(' ', '_') + ".jpg" | ||||||
|         Timber.i("File should be uploaded to $fileUrl") |         Timber.i("File should be uploaded to $fileUrl") | ||||||
|     } |     } | ||||||
|  | @ -324,6 +306,7 @@ class UploadTest { | ||||||
|             } catch (e: IOException) { |             } catch (e: IOException) { | ||||||
|                 e.printStackTrace() |                 e.printStackTrace() | ||||||
|             } |             } | ||||||
|  | 
 | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -345,8 +328,8 @@ class UploadTest { | ||||||
|     private fun dismissWarningDialog() { |     private fun dismissWarningDialog() { | ||||||
|         try { |         try { | ||||||
|             onView(withText("Yes")) |             onView(withText("Yes")) | ||||||
|                 .check(matches(isDisplayed())) |                     .check(matches(isDisplayed())) | ||||||
|                 .perform(click()) |                     .perform(click()) | ||||||
|         } catch (ignored: NoMatchingViewException) { |         } catch (ignored: NoMatchingViewException) { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | @ -354,10 +337,10 @@ class UploadTest { | ||||||
|     private fun openGallery() { |     private fun openGallery() { | ||||||
|         // Open FAB |         // Open FAB | ||||||
|         onView(allOf<View>(withId(R.id.fab_plus), isDisplayed())) |         onView(allOf<View>(withId(R.id.fab_plus), isDisplayed())) | ||||||
|             .perform(click()) |                 .perform(click()) | ||||||
| 
 | 
 | ||||||
|         // Click gallery |         // Click gallery | ||||||
|         onView(allOf<View>(withId(R.id.fab_gallery), isDisplayed())) |         onView(allOf<View>(withId(R.id.fab_gallery), isDisplayed())) | ||||||
|             .perform(click()) |                 .perform(click()) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | @ -3,6 +3,7 @@ package fr.free.nrw.commons | ||||||
| import androidx.test.espresso.Espresso.onView | import androidx.test.espresso.Espresso.onView | ||||||
| import androidx.test.espresso.action.ViewActions | import androidx.test.espresso.action.ViewActions | ||||||
| import androidx.test.espresso.assertion.ViewAssertions.matches | import androidx.test.espresso.assertion.ViewAssertions.matches | ||||||
|  | import androidx.test.espresso.matcher.ViewMatchers | ||||||
| import androidx.test.espresso.matcher.ViewMatchers.isDisplayed | import androidx.test.espresso.matcher.ViewMatchers.isDisplayed | ||||||
| import androidx.test.espresso.matcher.ViewMatchers.withId | import androidx.test.espresso.matcher.ViewMatchers.withId | ||||||
| import androidx.test.ext.junit.runners.AndroidJUnit4 | import androidx.test.ext.junit.runners.AndroidJUnit4 | ||||||
|  | @ -17,12 +18,11 @@ import org.junit.Before | ||||||
| import org.junit.Rule | import org.junit.Rule | ||||||
| import org.junit.Test | import org.junit.Test | ||||||
| import org.junit.runner.RunWith | import org.junit.runner.RunWith | ||||||
| import org.hamcrest.MatcherAssert.assertThat |  | ||||||
| import org.hamcrest.CoreMatchers.equalTo |  | ||||||
| 
 | 
 | ||||||
| @LargeTest | @LargeTest | ||||||
| @RunWith(AndroidJUnit4::class) | @RunWith(AndroidJUnit4::class) | ||||||
| class WelcomeActivityTest { | class WelcomeActivityTest { | ||||||
|  | 
 | ||||||
|     @get:Rule |     @get:Rule | ||||||
|     var activityRule: ActivityTestRule<*> = ActivityTestRule(WelcomeActivity::class.java) |     var activityRule: ActivityTestRule<*> = ActivityTestRule(WelcomeActivity::class.java) | ||||||
| 
 | 
 | ||||||
|  | @ -61,7 +61,7 @@ class WelcomeActivityTest { | ||||||
|                 .perform(ViewActions.click()) |                 .perform(ViewActions.click()) | ||||||
|             onView(withId(R.id.finishTutorialButton)) |             onView(withId(R.id.finishTutorialButton)) | ||||||
|                 .perform(ViewActions.click()) |                 .perform(ViewActions.click()) | ||||||
|             assertThat(activityRule.activity.isDestroyed, equalTo(true)) |             assert(activityRule.activity.isDestroyed) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -71,10 +71,10 @@ class WelcomeActivityTest { | ||||||
|             .perform(ViewActions.click()) |             .perform(ViewActions.click()) | ||||||
|         onView(withId(R.id.welcomePager)) |         onView(withId(R.id.welcomePager)) | ||||||
|             .perform(ViewActions.swipeLeft()) |             .perform(ViewActions.swipeLeft()) | ||||||
|         assertThat(true, equalTo(true)) |         assert(true) | ||||||
|         onView(withId(R.id.welcomePager)) |         onView(withId(R.id.welcomePager)) | ||||||
|             .perform(ViewActions.swipeRight()) |             .perform(ViewActions.swipeRight()) | ||||||
|         assertThat(true, equalTo(true)) |         assert(true) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Test |     @Test | ||||||
|  | @ -86,13 +86,13 @@ class WelcomeActivityTest { | ||||||
|             .perform(ViewActions.swipeLeft()) |             .perform(ViewActions.swipeLeft()) | ||||||
|             .perform(ViewActions.swipeLeft()) |             .perform(ViewActions.swipeLeft()) | ||||||
|             .perform(ViewActions.swipeLeft()) |             .perform(ViewActions.swipeLeft()) | ||||||
|         assertThat(true, equalTo(true)) |         assert(true) | ||||||
|         onView(withId(R.id.welcomePager)) |         onView(withId(R.id.welcomePager)) | ||||||
|             .perform(ViewActions.swipeRight()) |             .perform(ViewActions.swipeRight()) | ||||||
|             .perform(ViewActions.swipeRight()) |             .perform(ViewActions.swipeRight()) | ||||||
|             .perform(ViewActions.swipeRight()) |             .perform(ViewActions.swipeRight()) | ||||||
|             .perform(ViewActions.swipeRight()) |             .perform(ViewActions.swipeRight()) | ||||||
|         assertThat(true, equalTo(true)) |         assert(true) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Test |     @Test | ||||||
|  | @ -103,10 +103,10 @@ class WelcomeActivityTest { | ||||||
|             if (viewPager.currentItem == 3) { |             if (viewPager.currentItem == 3) { | ||||||
|                 onView(withId(R.id.welcomePager)) |                 onView(withId(R.id.welcomePager)) | ||||||
|                     .perform(ViewActions.swipeLeft()) |                     .perform(ViewActions.swipeLeft()) | ||||||
|                 assertThat(true, equalTo(true)) |                 assert(true) | ||||||
|                 onView(withId(R.id.welcomePager)) |                 onView(withId(R.id.welcomePager)) | ||||||
|                     .perform(ViewActions.swipeRight()) |                     .perform(ViewActions.swipeRight()) | ||||||
|                 assertThat(true, equalTo(true)) |                 assert(false) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | @ -121,7 +121,7 @@ class WelcomeActivityTest { | ||||||
|                     .perform(ViewActions.click()) |                     .perform(ViewActions.click()) | ||||||
|                 onView(withId(R.id.finishTutorialButton)) |                 onView(withId(R.id.finishTutorialButton)) | ||||||
|                     .perform(ViewActions.click()) |                     .perform(ViewActions.click()) | ||||||
|                 assertThat(activityRule.activity.isDestroyed, equalTo(true)) |                 assert(activityRule.activity.isDestroyed) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -1,271 +0,0 @@ | ||||||
| package fr.free.nrw.commons.contributions |  | ||||||
| 
 |  | ||||||
| import android.content.res.Configuration |  | ||||||
| import android.os.Looper |  | ||||||
| import androidx.fragment.app.testing.FragmentScenario |  | ||||||
| import androidx.fragment.app.testing.launchFragmentInContainer |  | ||||||
| import androidx.lifecycle.Lifecycle |  | ||||||
| import androidx.test.ext.junit.runners.AndroidJUnit4 |  | ||||||
| import com.google.android.material.floatingactionbutton.FloatingActionButton |  | ||||||
| import fr.free.nrw.commons.Media |  | ||||||
| import fr.free.nrw.commons.OkHttpConnectionFactory |  | ||||||
| import fr.free.nrw.commons.R |  | ||||||
| import fr.free.nrw.commons.TestCommonsApplication |  | ||||||
| import fr.free.nrw.commons.createTestClient |  | ||||||
| import fr.free.nrw.commons.upload.WikidataPlace |  | ||||||
| import org.junit.Assert |  | ||||||
| import org.junit.Before |  | ||||||
| import org.junit.Test |  | ||||||
| import org.junit.runner.RunWith |  | ||||||
| import org.mockito.ArgumentMatchers.anyInt |  | ||||||
| import org.mockito.Mockito.mock |  | ||||||
| import org.mockito.Mockito.verify |  | ||||||
| import org.mockito.Mockito.`when` |  | ||||||
| import org.robolectric.Shadows |  | ||||||
| import org.robolectric.annotation.Config |  | ||||||
| import org.robolectric.annotation.LooperMode |  | ||||||
| import java.lang.reflect.Method |  | ||||||
| 
 |  | ||||||
| @RunWith(AndroidJUnit4::class) |  | ||||||
| @Config(sdk = [21], application = TestCommonsApplication::class) |  | ||||||
| @LooperMode(LooperMode.Mode.PAUSED) |  | ||||||
| class ContributionsListFragmentUnitTests { |  | ||||||
|     private lateinit var scenario: FragmentScenario<ContributionsListFragment> |  | ||||||
|     private lateinit var fragment: ContributionsListFragment |  | ||||||
| 
 |  | ||||||
|     private val adapter: ContributionsListAdapter = mock() |  | ||||||
|     private val contribution: Contribution = mock() |  | ||||||
|     private val media: Media = mock() |  | ||||||
|     private val wikidataPlace: WikidataPlace = mock() |  | ||||||
| 
 |  | ||||||
|     @Before |  | ||||||
|     fun setUp() { |  | ||||||
|         OkHttpConnectionFactory.CLIENT = createTestClient() |  | ||||||
| 
 |  | ||||||
|         scenario = |  | ||||||
|             launchFragmentInContainer( |  | ||||||
|                 initialState = Lifecycle.State.RESUMED, |  | ||||||
|                 themeResId = R.style.LightAppTheme, |  | ||||||
|             ) { |  | ||||||
|                 ContributionsListFragment() |  | ||||||
|                     .apply { |  | ||||||
|                         contributionsListPresenter = mock() |  | ||||||
|                         callback = mock() |  | ||||||
|                     }.also { |  | ||||||
|                         fragment = it |  | ||||||
|                     } |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|         scenario.onFragment { |  | ||||||
|             it.adapter = adapter |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Test |  | ||||||
|     @Throws(Exception::class) |  | ||||||
|     fun checkFragmentNotNull() { |  | ||||||
|         Shadows.shadowOf(Looper.getMainLooper()).idle() |  | ||||||
|         Assert.assertNotNull(fragment) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Test |  | ||||||
|     @Throws(Exception::class) |  | ||||||
|     fun testOnDetach() { |  | ||||||
|         Shadows.shadowOf(Looper.getMainLooper()).idle() |  | ||||||
|         fragment.onDetach() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Test |  | ||||||
|     @Throws(Exception::class) |  | ||||||
|     fun testGetContributionStateAt() { |  | ||||||
|         Shadows.shadowOf(Looper.getMainLooper()).idle() |  | ||||||
|         `when`(adapter.getContributionForPosition(anyInt())).thenReturn(contribution) |  | ||||||
|         fragment.getContributionStateAt(0) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Test |  | ||||||
|     @Throws(Exception::class) |  | ||||||
|     fun testOnScrollToTop() { |  | ||||||
|         Shadows.shadowOf(Looper.getMainLooper()).idle() |  | ||||||
|         fragment.rvContributionsList = mock() |  | ||||||
|         fragment.scrollToTop() |  | ||||||
|         verify(fragment.rvContributionsList)?.smoothScrollToPosition(0) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Test |  | ||||||
|     @Throws(Exception::class) |  | ||||||
|     fun testOnConfirmClicked() { |  | ||||||
|         Shadows.shadowOf(Looper.getMainLooper()).idle() |  | ||||||
|         `when`(contribution.media).thenReturn(media) |  | ||||||
|         `when`(media.wikiCode).thenReturn("") |  | ||||||
|         `when`(contribution.wikidataPlace).thenReturn(wikidataPlace) |  | ||||||
|         fragment.onConfirmClicked(contribution, true) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Test |  | ||||||
|     @Throws(Exception::class) |  | ||||||
|     fun testGetTotalMediaCount() { |  | ||||||
|         Shadows.shadowOf(Looper.getMainLooper()).idle() |  | ||||||
|         fragment.totalMediaCount |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Test |  | ||||||
|     @Throws(Exception::class) |  | ||||||
|     fun testGetMediaAtPositionCaseNonNull() { |  | ||||||
|         Shadows.shadowOf(Looper.getMainLooper()).idle() |  | ||||||
|         `when`(adapter.getContributionForPosition(anyInt())).thenReturn(contribution) |  | ||||||
|         `when`(contribution.media).thenReturn(media) |  | ||||||
|         fragment.getMediaAtPosition(0) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Test |  | ||||||
|     @Throws(Exception::class) |  | ||||||
|     fun testGetMediaAtPositionCaseNull() { |  | ||||||
|         Shadows.shadowOf(Looper.getMainLooper()).idle() |  | ||||||
|         `when`(adapter.getContributionForPosition(anyInt())).thenReturn(null) |  | ||||||
|         fragment.getMediaAtPosition(0) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Test |  | ||||||
|     @Throws(Exception::class) |  | ||||||
|     fun testShowAddImageToWikipediaInstructions() { |  | ||||||
|         Shadows.shadowOf(Looper.getMainLooper()).idle() |  | ||||||
|         val method: Method = |  | ||||||
|             ContributionsListFragment::class.java.getDeclaredMethod( |  | ||||||
|                 "showAddImageToWikipediaInstructions", |  | ||||||
|                 Contribution::class.java, |  | ||||||
|             ) |  | ||||||
|         method.isAccessible = true |  | ||||||
|         method.invoke(fragment, contribution) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Test |  | ||||||
|     @Throws(Exception::class) |  | ||||||
|     fun testAddImageToWikipedia() { |  | ||||||
|         Shadows.shadowOf(Looper.getMainLooper()).idle() |  | ||||||
|         fragment.addImageToWikipedia(contribution) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Test |  | ||||||
|     @Throws(Exception::class) |  | ||||||
|     fun testOpenMediaDetail() { |  | ||||||
|         Shadows.shadowOf(Looper.getMainLooper()).idle() |  | ||||||
|         fragment.openMediaDetail(0, true) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Test |  | ||||||
|     @Throws(Exception::class) |  | ||||||
|     fun testOnViewStateRestored() { |  | ||||||
|         Shadows.shadowOf(Looper.getMainLooper()).idle() |  | ||||||
|         fragment.onViewStateRestored(mock()) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Test |  | ||||||
|     @Throws(Exception::class) |  | ||||||
|     fun testOnSaveInstanceState() { |  | ||||||
|         Shadows.shadowOf(Looper.getMainLooper()).idle() |  | ||||||
|         fragment.onSaveInstanceState(mock()) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Test |  | ||||||
|     @Throws(Exception::class) |  | ||||||
|     fun testShowNoContributionsUI() { |  | ||||||
|         Shadows.shadowOf(Looper.getMainLooper()).idle() |  | ||||||
|         fragment.showNoContributionsUI(true) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Test |  | ||||||
|     @Throws(Exception::class) |  | ||||||
|     fun testShowProgress() { |  | ||||||
|         Shadows.shadowOf(Looper.getMainLooper()).idle() |  | ||||||
|         fragment.showProgress(true) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Test |  | ||||||
|     @Throws(Exception::class) |  | ||||||
|     fun testShowWelcomeTip() { |  | ||||||
|         Shadows.shadowOf(Looper.getMainLooper()).idle() |  | ||||||
|         fragment.showWelcomeTip(true) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Test |  | ||||||
|     @Throws(Exception::class) |  | ||||||
|     fun testAnimateFAB() { |  | ||||||
|         Shadows.shadowOf(Looper.getMainLooper()).idle() |  | ||||||
|         scenario.onFragment { |  | ||||||
|             it.requireView().findViewById<FloatingActionButton>(R.id.fab_plus).hide() |  | ||||||
|         } |  | ||||||
|         val method: Method = |  | ||||||
|             ContributionsListFragment::class.java.getDeclaredMethod( |  | ||||||
|                 "animateFAB", |  | ||||||
|                 Boolean::class.java, |  | ||||||
|             ) |  | ||||||
|         method.isAccessible = true |  | ||||||
|         method.invoke(fragment, true) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Test |  | ||||||
|     @Throws(Exception::class) |  | ||||||
|     fun testAnimateFABCaseShownAndOpen() { |  | ||||||
|         Shadows.shadowOf(Looper.getMainLooper()).idle() |  | ||||||
|         scenario.onFragment { |  | ||||||
|             it.requireView().findViewById<FloatingActionButton>(R.id.fab_plus).show() |  | ||||||
|         } |  | ||||||
|         val method: Method = |  | ||||||
|             ContributionsListFragment::class.java.getDeclaredMethod( |  | ||||||
|                 "animateFAB", |  | ||||||
|                 Boolean::class.java, |  | ||||||
|             ) |  | ||||||
|         method.isAccessible = true |  | ||||||
|         method.invoke(fragment, true) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Test |  | ||||||
|     @Throws(Exception::class) |  | ||||||
|     fun testAnimateFABCaseShownAndClose() { |  | ||||||
|         Shadows.shadowOf(Looper.getMainLooper()).idle() |  | ||||||
|         scenario.onFragment { |  | ||||||
|             it.requireView().findViewById<FloatingActionButton>(R.id.fab_plus).show() |  | ||||||
|         } |  | ||||||
|         val method: Method = |  | ||||||
|             ContributionsListFragment::class.java.getDeclaredMethod( |  | ||||||
|                 "animateFAB", |  | ||||||
|                 Boolean::class.java, |  | ||||||
|             ) |  | ||||||
|         method.isAccessible = true |  | ||||||
|         method.invoke(fragment, false) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Test |  | ||||||
|     @Throws(Exception::class) |  | ||||||
|     fun testSetListeners() { |  | ||||||
|         Shadows.shadowOf(Looper.getMainLooper()).idle() |  | ||||||
|         val method: Method = |  | ||||||
|             ContributionsListFragment::class.java.getDeclaredMethod( |  | ||||||
|                 "setListeners", |  | ||||||
|             ) |  | ||||||
|         method.isAccessible = true |  | ||||||
|         method.invoke(fragment) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Test |  | ||||||
|     @Throws(Exception::class) |  | ||||||
|     fun testInitializeAnimations() { |  | ||||||
|         Shadows.shadowOf(Looper.getMainLooper()).idle() |  | ||||||
|         val method: Method = |  | ||||||
|             ContributionsListFragment::class.java.getDeclaredMethod( |  | ||||||
|                 "initializeAnimations", |  | ||||||
|             ) |  | ||||||
|         method.isAccessible = true |  | ||||||
|         method.invoke(fragment) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Test |  | ||||||
|     @Throws(Exception::class) |  | ||||||
|     fun testOnConfigurationChanged() { |  | ||||||
|         Shadows.shadowOf(Looper.getMainLooper()).idle() |  | ||||||
|         val newConfig: Configuration = mock() |  | ||||||
|         newConfig.orientation = Configuration.ORIENTATION_LANDSCAPE |  | ||||||
|         fragment.onConfigurationChanged(newConfig) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -1,61 +0,0 @@ | ||||||
| package fr.free.nrw.commons.navtab |  | ||||||
| 
 |  | ||||||
| import android.os.Looper |  | ||||||
| import androidx.fragment.app.testing.FragmentScenario |  | ||||||
| import androidx.fragment.app.testing.launchFragmentInContainer |  | ||||||
| import androidx.lifecycle.Lifecycle |  | ||||||
| import androidx.test.ext.junit.runners.AndroidJUnit4 |  | ||||||
| import fr.free.nrw.commons.R |  | ||||||
| import fr.free.nrw.commons.TestCommonsApplication |  | ||||||
| import org.junit.Before |  | ||||||
| import org.junit.Test |  | ||||||
| import org.junit.runner.RunWith |  | ||||||
| import org.robolectric.Shadows |  | ||||||
| import org.robolectric.annotation.Config |  | ||||||
| import org.robolectric.annotation.LooperMode |  | ||||||
| 
 |  | ||||||
| @RunWith(AndroidJUnit4::class) |  | ||||||
| @Config(sdk = [21], application = TestCommonsApplication::class) |  | ||||||
| @LooperMode(LooperMode.Mode.PAUSED) |  | ||||||
| class MoreBottomSheetLoggedOutFragmentUnitTests { |  | ||||||
|     private lateinit var scenario: FragmentScenario<MoreBottomSheetLoggedOutFragment> |  | ||||||
| 
 |  | ||||||
|     @Before |  | ||||||
|     fun setUp() { |  | ||||||
|         scenario = |  | ||||||
|             launchFragmentInContainer( |  | ||||||
|                 initialState = Lifecycle.State.RESUMED, |  | ||||||
|                 themeResId = R.style.LightAppTheme, |  | ||||||
|             ) { |  | ||||||
|                 MoreBottomSheetLoggedOutFragment() |  | ||||||
|             } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Test |  | ||||||
|     @Throws(Exception::class) |  | ||||||
|     fun testOnSettingsClicked() { |  | ||||||
|         Shadows.shadowOf(Looper.getMainLooper()).idle() |  | ||||||
|         scenario.onFragment { it.onSettingsClicked() } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Test |  | ||||||
|     @Throws(Exception::class) |  | ||||||
|     fun testOnAboutClicked() { |  | ||||||
|         Shadows.shadowOf(Looper.getMainLooper()).idle() |  | ||||||
|         scenario.onFragment { it.onAboutClicked() } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Test |  | ||||||
|     @Throws(Exception::class) |  | ||||||
|     fun testOnFeedbackClicked() { |  | ||||||
|         Shadows.shadowOf(Looper.getMainLooper()).idle() |  | ||||||
|         scenario.onFragment { it.onFeedbackClicked() } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Test |  | ||||||
|     @Throws(Exception::class) |  | ||||||
|     fun testOnLogoutClicked() { |  | ||||||
|         Shadows.shadowOf(Looper.getMainLooper()).idle() |  | ||||||
|         scenario.onFragment { it.onLogoutClicked() } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -11,24 +11,21 @@ import org.junit.runner.RunWith | ||||||
| 
 | 
 | ||||||
| @RunWith(AndroidJUnit4::class) | @RunWith(AndroidJUnit4::class) | ||||||
| class PasteSensitiveTextInputEditTextTest { | class PasteSensitiveTextInputEditTextTest { | ||||||
|  | 
 | ||||||
|     private var context: Context? = null |     private var context: Context? = null | ||||||
|     private var textView: PasteSensitiveTextInputEditText? = null |     private var textView: PasteSensitiveTextInputEditText? = null | ||||||
| 
 | 
 | ||||||
|     @Before |     @Before | ||||||
|     fun setup() { |     fun setup() { | ||||||
|         context = ApplicationProvider.getApplicationContext() |         context = ApplicationProvider.getApplicationContext() | ||||||
|         textView = PasteSensitiveTextInputEditText(context!!) |         textView = PasteSensitiveTextInputEditText(context) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // this test has no real value, just % for test code coverage |     // this test has no real value, just % for test code coverage | ||||||
|     @Test |     @Test | ||||||
|     fun extractFormattingAttributeSet() { |     fun extractFormattingAttributeSet(){ | ||||||
|         val methodExtractFormattingAttribute = |         val methodExtractFormattingAttribute = textView!!.javaClass.getDeclaredMethod( | ||||||
|             textView!!.javaClass.getDeclaredMethod( |             "extractFormattingAttribute", Context::class.java, AttributeSet::class.java) | ||||||
|                 "extractFormattingAttribute", |  | ||||||
|                 Context::class.java, |  | ||||||
|                 AttributeSet::class.java, |  | ||||||
|             ) |  | ||||||
|         methodExtractFormattingAttribute.isAccessible = true |         methodExtractFormattingAttribute.isAccessible = true | ||||||
|         methodExtractFormattingAttribute.invoke(textView, context, null) |         methodExtractFormattingAttribute.invoke(textView, context, null) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -9,58 +9,56 @@ import org.hamcrest.Matcher | ||||||
| 
 | 
 | ||||||
| class MyViewAction { | class MyViewAction { | ||||||
|     companion object { |     companion object { | ||||||
|         fun typeTextInChildViewWithId( |         fun typeTextInChildViewWithId(id: Int, textToBeTyped: String): ViewAction { | ||||||
|             id: Int, |             return object : ViewAction { | ||||||
|             textToBeTyped: String, |                 override fun getConstraints(): Matcher<View>? { | ||||||
|         ): ViewAction = |                     return null | ||||||
|             object : ViewAction { |                 } | ||||||
|                 override fun getConstraints(): Matcher<View>? = null |  | ||||||
| 
 | 
 | ||||||
|                 override fun getDescription(): String = "Click on a child view with specified id." |                 override fun getDescription(): String { | ||||||
|  |                     return "Click on a child view with specified id." | ||||||
|  |                 } | ||||||
| 
 | 
 | ||||||
|                 override fun perform( |                 override fun perform(uiController: UiController, view: View) { | ||||||
|                     uiController: UiController, |  | ||||||
|                     view: View, |  | ||||||
|                 ) { |  | ||||||
|                     val v = view.findViewById<View>(id) as EditText |                     val v = view.findViewById<View>(id) as EditText | ||||||
|                     v.setText(textToBeTyped) |                     v.setText(textToBeTyped) | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|         fun selectSpinnerItemInChildViewWithId( |         fun selectSpinnerItemInChildViewWithId(id: Int, position: Int): ViewAction { | ||||||
|             id: Int, |             return object : ViewAction { | ||||||
|             position: Int, |                 override fun getConstraints(): Matcher<View>? { | ||||||
|         ): ViewAction = |                     return null | ||||||
|             object : ViewAction { |                 } | ||||||
|                 override fun getConstraints(): Matcher<View>? = null |  | ||||||
| 
 | 
 | ||||||
|                 override fun getDescription(): String = "Click on a child view with specified id." |                 override fun getDescription(): String { | ||||||
|  |                     return "Click on a child view with specified id." | ||||||
|  |                 } | ||||||
| 
 | 
 | ||||||
|                 override fun perform( |                 override fun perform(uiController: UiController, view: View) { | ||||||
|                     uiController: UiController, |  | ||||||
|                     view: View, |  | ||||||
|                 ) { |  | ||||||
|                     val v = view.findViewById<View>(id) as AppCompatSpinner |                     val v = view.findViewById<View>(id) as AppCompatSpinner | ||||||
|                     v.setSelection(position) |                     v.setSelection(position) | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|         fun clickItemWithId( |         fun clickItemWithId(id: Int, position: Int): ViewAction { | ||||||
|             id: Int, |             return object : ViewAction { | ||||||
|             position: Int, |                 override fun getConstraints(): Matcher<View>? { | ||||||
|         ): ViewAction = |                     return null | ||||||
|             object : ViewAction { |                 } | ||||||
|                 override fun getConstraints(): Matcher<View>? = null |  | ||||||
| 
 | 
 | ||||||
|                 override fun getDescription(): String = "Click on a child view with specified id." |                 override fun getDescription(): String { | ||||||
|  |                     return "Click on a child view with specified id." | ||||||
|  |                 } | ||||||
| 
 | 
 | ||||||
|                 override fun perform( |                 override fun perform(uiController: UiController, view: View) { | ||||||
|                     uiController: UiController, |  | ||||||
|                     view: View, |  | ||||||
|                 ) { |  | ||||||
|                     val v = view.findViewById<View>(id) as View |                     val v = view.findViewById<View>(id) as View | ||||||
|                     v.performClick() |                     v.performClick() | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | @ -1,259 +1,239 @@ | ||||||
| <?xml version="1.0" encoding="utf-8"?> | <?xml version="1.0" encoding="utf-8"?> | ||||||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android" | <manifest xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|   xmlns:tools="http://schemas.android.com/tools"> |     xmlns:tools="http://schemas.android.com/tools" | ||||||
|  |     package="fr.free.nrw.commons"> | ||||||
|  |     <uses-permission android:name="android.permission.INTERNET" /> | ||||||
|  |     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> | ||||||
|  |     <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" /> | ||||||
|  |     <uses-permission android:name="android.permission.READ_SYNC_STATS" /> | ||||||
|  |     <uses-permission android:name="android.permission.REORDER_TASKS" /> | ||||||
|  |     <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" /> | ||||||
|  |     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> | ||||||
|  |     <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> | ||||||
|  |     <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" /> | ||||||
|  |     <uses-permission android:name="android.permission.GET_ACCOUNTS" /> | ||||||
|  |     <uses-permission android:name="android.permission.USE_CREDENTIALS" /> | ||||||
|  |     <uses-permission android:name="android.permission.MANAGE_ACCOUNTS" /> | ||||||
|  |     <uses-permission android:name="com.google.android.apps.photos.permission.GOOGLE_PHOTOS" /> | ||||||
|  |     <uses-permission android:name="android.permission.SET_WALLPAPER"/> | ||||||
|  |     <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> | ||||||
| 
 | 
 | ||||||
|   <uses-permission android:name="android.permission.INTERNET" /> |  | ||||||
|   <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" |  | ||||||
|     android:maxSdkVersion="32" /> |  | ||||||
|   <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" /> |  | ||||||
|   <uses-permission android:name="android.permission.READ_SYNC_STATS" /> |  | ||||||
|   <uses-permission android:name="android.permission.REORDER_TASKS" /> |  | ||||||
|   <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" /> |  | ||||||
|   <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" |  | ||||||
|     android:maxSdkVersion="29"/> |  | ||||||
|   <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> |  | ||||||
|   <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" /> |  | ||||||
|   <!-- Permission needed up to Android 5.1, see https://github.com/commons-app/apps-android-commons/pull/5863 --> |  | ||||||
|   <uses-permission android:name="android.permission.GET_ACCOUNTS" |  | ||||||
|     android:maxSdkVersion="22"/> |  | ||||||
|   <uses-permission android:name="android.permission.USE_CREDENTIALS" /> |  | ||||||
|   <uses-permission android:name="android.permission.MANAGE_ACCOUNTS" /> |  | ||||||
|   <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> |  | ||||||
|   <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" |  | ||||||
|     android:minSdkVersion="33"/> |  | ||||||
|   <uses-permission android:name="com.google.android.apps.photos.permission.GOOGLE_PHOTOS" /> |  | ||||||
|   <uses-permission android:name="android.permission.SET_WALLPAPER" /> |  | ||||||
|   <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> |  | ||||||
|   <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"/> |  | ||||||
|   <uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" |  | ||||||
|     android:minSdkVersion="34"/> |  | ||||||
|   <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" /> |  | ||||||
| 
 | 
 | ||||||
|   <queries> |  | ||||||
| 
 | 
 | ||||||
|     <!-- Browser --> |     <!-- Needed only if your app targets Android 5.0 (API level 21) or higher. --> | ||||||
|     <intent> |     <uses-feature android:name="android.hardware.location.gps" /> | ||||||
|       <action android:name="android.intent.action.VIEW" /> |  | ||||||
| 
 | 
 | ||||||
|       <category android:name="android.intent.category.BROWSABLE" /> |     <application | ||||||
|  |         android:name=".CommonsApplication" | ||||||
|  |         android:icon="@mipmap/ic_launcher" | ||||||
|  |         android:label="@string/app_name" | ||||||
|  |         android:theme="@style/LightAppTheme" | ||||||
|  |         android:largeHeap="true" | ||||||
|  |         android:supportsRtl="true" | ||||||
|  |         tools:replace="android:appComponentFactory" | ||||||
|  |         android:appComponentFactory="commons" | ||||||
|  |         android:requestLegacyExternalStorage = "true" | ||||||
|  |         tools:ignore="GoogleAppIndexingWarning"> | ||||||
| 
 | 
 | ||||||
|       <data android:scheme="https" /> |         <activity | ||||||
|     </intent> |             android:name=".description.DescriptionEditActivity" | ||||||
|     <!-- Google Maps --> |             android:exported="true" /> | ||||||
|     <package android:name="com.google.android.apps.maps" /> |  | ||||||
|   </queries> <!-- Needed only if your app targets Android 5.0 (API level 21) or higher. --> |  | ||||||
|   <uses-feature android:name="android.hardware.location.gps" /> |  | ||||||
| 
 | 
 | ||||||
|   <application |         <activity android:name="org.acra.dialog.CrashReportDialog" | ||||||
|     android:name=".CommonsApplication" |             android:process=":acra" | ||||||
|     android:appComponentFactory="commons" |             android:launchMode="singleInstance" | ||||||
|     android:icon="@mipmap/ic_launcher" |             android:excludeFromRecents="true" | ||||||
|     android:label="@string/app_name" |             android:finishOnTaskLaunch="true" /> | ||||||
|     android:largeHeap="true" |  | ||||||
|     android:requestLegacyExternalStorage="true" |  | ||||||
|     android:supportsRtl="true" |  | ||||||
|     android:theme="@style/LightAppTheme" |  | ||||||
|     tools:ignore="GoogleAppIndexingWarning" |  | ||||||
|     tools:replace="android:appComponentFactory"> |  | ||||||
|     <activity |  | ||||||
|       android:name=".activity.SingleWebViewActivity" |  | ||||||
|       android:exported="false" /> |  | ||||||
|     <activity |  | ||||||
|       android:name=".nearby.WikidataFeedback" |  | ||||||
|       android:exported="false" /> |  | ||||||
|     <activity |  | ||||||
|       android:name=".upload.UploadProgressActivity" |  | ||||||
|       android:exported="false" /> |  | ||||||
|     <activity |  | ||||||
|       android:name=".description.DescriptionEditActivity" |  | ||||||
|       android:exported="true" |  | ||||||
|       android:theme="@style/EditActivityTheme" /> |  | ||||||
|     <activity |  | ||||||
|       android:name=".edit.EditActivity" |  | ||||||
|       android:exported="false" /> |  | ||||||
|     <activity |  | ||||||
|       android:name="org.acra.dialog.CrashReportDialog" |  | ||||||
|       android:excludeFromRecents="true" |  | ||||||
|       android:finishOnTaskLaunch="true" |  | ||||||
|       android:launchMode="singleInstance" |  | ||||||
|       android:process=":acra" /> |  | ||||||
|     <activity |  | ||||||
|       android:name=".media.ZoomableActivity" |  | ||||||
|       android:configChanges="screenSize|keyboard|orientation" |  | ||||||
|       android:label="Zoomable Activity" |  | ||||||
|       android:parentActivityName=".customselector.ui.selector.CustomSelectorActivity" /> |  | ||||||
|     <activity |  | ||||||
|       android:name=".auth.LoginActivity" |  | ||||||
|       android:windowSoftInputMode="adjustPan" |  | ||||||
|       android:exported="true"> |  | ||||||
|       <intent-filter> |  | ||||||
|         <category android:name="android.intent.category.LAUNCHER" /> |  | ||||||
| 
 | 
 | ||||||
|         <action android:name="android.intent.action.MAIN" /> |         <activity | ||||||
|       </intent-filter> |             android:name=".media.ZoomableActivity" /> | ||||||
| 
 | 
 | ||||||
|       <meta-data |         <activity android:name=".auth.LoginActivity"> | ||||||
|         android:name="android.app.shortcuts" |             <intent-filter> | ||||||
|         android:resource="@xml/shortcuts" /> |                 <category android:name="android.intent.category.LAUNCHER" /> | ||||||
|     </activity> |  | ||||||
|     <activity android:name=".WelcomeActivity" /> |  | ||||||
|     <activity |  | ||||||
|       android:name=".upload.UploadActivity" |  | ||||||
|       android:configChanges="orientation|screenSize|keyboard" |  | ||||||
|       android:exported="true" |  | ||||||
|       android:hardwareAccelerated="false" |  | ||||||
|       android:icon="@mipmap/ic_launcher" |  | ||||||
|       android:windowSoftInputMode="adjustPan"> |  | ||||||
|       <intent-filter android:label="@string/intent_share_upload_label"> |  | ||||||
|         <action android:name="android.intent.action.SEND" /> |  | ||||||
| 
 | 
 | ||||||
|         <category android:name="android.intent.category.DEFAULT" /> |                 <action android:name="android.intent.action.MAIN" /> | ||||||
|  |             </intent-filter> | ||||||
| 
 | 
 | ||||||
|         <data android:mimeType="image/*" /> |             <meta-data android:name="android.app.shortcuts" | ||||||
|         <data android:mimeType="audio/ogg" /> |                 android:resource="@xml/shortcuts" /> | ||||||
|       </intent-filter> |  | ||||||
|       <intent-filter android:label="@string/intent_share_upload_label"> |  | ||||||
|         <action android:name="android.intent.action.SEND_MULTIPLE" /> |  | ||||||
| 
 | 
 | ||||||
|         <category android:name="android.intent.category.DEFAULT" /> |         </activity> | ||||||
|  |         <activity android:name=".WelcomeActivity" /> | ||||||
| 
 | 
 | ||||||
|         <data android:mimeType="image/*" /> |         <activity | ||||||
|         <data android:mimeType="audio/ogg" /> |             android:hardwareAccelerated="false" | ||||||
|       </intent-filter> |             android:name=".upload.UploadActivity" | ||||||
|     </activity> |             android:configChanges="orientation|screenSize|keyboard" | ||||||
|     <activity |             android:icon="@mipmap/ic_launcher" | ||||||
|       android:name=".contributions.MainActivity" |             android:label="@string/app_name" | ||||||
|       android:configChanges="screenSize|keyboard|orientation" |             android:windowSoftInputMode="adjustResize" | ||||||
|       android:icon="@mipmap/ic_launcher" |             > | ||||||
|       /> |             <intent-filter android:label="@string/intent_share_upload_label"> | ||||||
|     <activity |                 <action android:name="android.intent.action.SEND" /> | ||||||
|       android:name=".settings.SettingsActivity" |  | ||||||
|       android:label="@string/title_activity_settings" /> |  | ||||||
|     <activity |  | ||||||
|       android:name=".AboutActivity" |  | ||||||
|       android:label="@string/title_activity_about" |  | ||||||
|       android:parentActivityName=".contributions.MainActivity" /> |  | ||||||
|     <activity |  | ||||||
|       android:name=".auth.SignupActivity" |  | ||||||
|       android:configChanges="orientation|screenLayout|screenSize" |  | ||||||
|       android:label="@string/title_activity_signup" /> |  | ||||||
|     <activity |  | ||||||
|       android:name=".notification.NotificationActivity" |  | ||||||
|       android:label="@string/navigation_item_notification" /> |  | ||||||
|     <activity |  | ||||||
|       android:name=".quiz.QuizActivity" |  | ||||||
|       android:label="@string/quiz" /> |  | ||||||
|     <activity |  | ||||||
|       android:name=".quiz.QuizResultActivity" |  | ||||||
|       android:label="@string/result" /> |  | ||||||
|     <activity |  | ||||||
|       android:name=".customselector.ui.selector.CustomSelectorActivity" |  | ||||||
|       android:configChanges="screenSize|keyboard|orientation" |  | ||||||
|       android:label="@string/title_activity_custom_selector" |  | ||||||
|       android:parentActivityName=".contributions.MainActivity" /> |  | ||||||
|     <activity |  | ||||||
|       android:name=".category.CategoryDetailsActivity" |  | ||||||
|       android:configChanges="screenSize|keyboard|orientation" |  | ||||||
|       android:label="@string/title_activity_featured_images" |  | ||||||
|       android:parentActivityName=".contributions.MainActivity" /> |  | ||||||
|     <activity |  | ||||||
|       android:name=".explore.depictions.WikidataItemDetailsActivity" |  | ||||||
|       android:configChanges="screenSize|keyboard|orientation" |  | ||||||
|       android:label="@string/title_activity_featured_images" |  | ||||||
|       android:parentActivityName=".contributions.MainActivity" /> |  | ||||||
|     <activity |  | ||||||
|       android:name=".explore.SearchActivity" |  | ||||||
|       android:configChanges="orientation|keyboardHidden|screenSize" |  | ||||||
|       android:label="@string/title_activity_search" |  | ||||||
|       android:launchMode="singleTop" |  | ||||||
|       android:parentActivityName=".contributions.MainActivity" /> |  | ||||||
|     <activity |  | ||||||
|       android:name=".profile.ProfileActivity" |  | ||||||
|       android:configChanges="orientation|screenSize|keyboard" |  | ||||||
|       android:label="@string/Profile" /> |  | ||||||
|     <activity |  | ||||||
|       android:name=".review.ReviewActivity" |  | ||||||
|       android:label="@string/title_activity_review" /> |  | ||||||
|     <activity |  | ||||||
|       android:name=".locationpicker.LocationPickerActivity" |  | ||||||
|       android:label="Location Picker" /> |  | ||||||
| 
 | 
 | ||||||
|     <service |                 <category android:name="android.intent.category.DEFAULT" /> | ||||||
|       android:name=".auth.WikiAccountAuthenticatorService" |  | ||||||
|       android:exported="true" |  | ||||||
|       android:process=":auth"> |  | ||||||
|       <intent-filter> |  | ||||||
|         <action android:name="android.accounts.AccountAuthenticator" /> |  | ||||||
|       </intent-filter> |  | ||||||
| 
 | 
 | ||||||
|       <meta-data |                 <data android:mimeType="image/*" /> | ||||||
|         android:name="android.accounts.AccountAuthenticator" |                 <data android:mimeType="audio/ogg" /> | ||||||
|         android:resource="@xml/authenticator" /> |             </intent-filter> | ||||||
|     </service> |             <intent-filter android:label="@string/intent_share_upload_label"> | ||||||
|     <service |                 <action android:name="android.intent.action.SEND_MULTIPLE" /> | ||||||
|       android:name="org.acra.sender.SenderService" |  | ||||||
|       android:exported="false" |  | ||||||
|       android:process=":acra" /> |  | ||||||
| 
 | 
 | ||||||
|     <service |                 <category android:name="android.intent.category.DEFAULT" /> | ||||||
|       android:name="androidx.work.impl.foreground.SystemForegroundService" |  | ||||||
|       android:foregroundServiceType="dataSync" /> |  | ||||||
| 
 | 
 | ||||||
|     <provider |                 <data android:mimeType="image/*" /> | ||||||
|       android:name=".filepicker.ExtendedFileProvider" |                 <data android:mimeType="audio/ogg" /> | ||||||
|       android:authorities="${applicationId}.provider" |             </intent-filter> | ||||||
|       android:exported="false" |         </activity> | ||||||
|       android:grantUriPermissions="true"> |         <activity | ||||||
|       <meta-data |             android:name=".contributions.MainActivity" | ||||||
|         android:name="android.support.FILE_PROVIDER_PATHS" |             android:icon="@mipmap/ic_launcher" | ||||||
|         android:resource="@xml/provider_paths" /> |             android:label="@string/app_name" | ||||||
|     </provider> |             android:configChanges="screenSize|keyboard|orientation" /> | ||||||
|     <provider |         <activity | ||||||
|       android:name=".category.CategoryContentProvider" |             android:name=".settings.SettingsActivity" | ||||||
|       android:authorities="${applicationId}.categories.contentprovider" |             android:label="@string/title_activity_settings" /> | ||||||
|       android:exported="false" |         <activity | ||||||
|       android:label="@string/provider_categories" |             android:name=".AboutActivity" | ||||||
|       android:syncable="false" /> |             android:label="@string/title_activity_about" | ||||||
|     <provider |             android:parentActivityName=".contributions.MainActivity" /> | ||||||
|       android:name=".explore.recentsearches.RecentSearchesContentProvider" |  | ||||||
|       android:authorities="${applicationId}.explore.recentsearches.contentprovider" |  | ||||||
|       android:exported="false" |  | ||||||
|       android:label="@string/provider_searches" |  | ||||||
|       android:syncable="false" /> |  | ||||||
|     <provider |  | ||||||
|       android:name=".recentlanguages.RecentLanguagesContentProvider" |  | ||||||
|       android:authorities="${applicationId}.recentlanguages.contentprovider" |  | ||||||
|       android:exported="false" |  | ||||||
|       android:label="@string/provider_recent_languages" |  | ||||||
|       android:syncable="false" /> |  | ||||||
|     <provider |  | ||||||
|       android:name=".bookmarks.pictures.BookmarkPicturesContentProvider" |  | ||||||
|       android:authorities="${applicationId}.bookmarks.contentprovider" |  | ||||||
|       android:exported="false" |  | ||||||
|       android:label="@string/provider_bookmarks" |  | ||||||
|       android:syncable="false" /> |  | ||||||
|     <provider |  | ||||||
|       android:name=".bookmarks.items.BookmarkItemsContentProvider" |  | ||||||
|       android:authorities="${applicationId}.bookmarks.items.contentprovider" |  | ||||||
|       android:exported="false" |  | ||||||
|       android:label="@string/provider_bookmarks_location" |  | ||||||
|       android:syncable="false" /> |  | ||||||
| 
 | 
 | ||||||
|     <receiver |         <activity | ||||||
|       android:name=".widget.PicOfDayAppWidget" |             android:name=".auth.SignupActivity" | ||||||
|       android:exported="true"> |             android:configChanges="orientation|screenLayout|screenSize" | ||||||
|       <intent-filter> |             android:label="@string/title_activity_signup" /> | ||||||
|         <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> |  | ||||||
|       </intent-filter> |  | ||||||
| 
 | 
 | ||||||
|       <meta-data |         <activity | ||||||
|         android:name="android.appwidget.provider" |             android:name=".notification.NotificationActivity" | ||||||
|         android:resource="@xml/pic_of_day_app_widget_info" /> |             android:label="@string/navigation_item_notification" /> | ||||||
|     </receiver> |  | ||||||
| 
 | 
 | ||||||
|     <uses-library |         <activity android:name=".quiz.QuizActivity" | ||||||
|       android:name="org.apache.http.legacy" |             android:label="@string/quiz"/> | ||||||
|       android:required="false" /> | 
 | ||||||
|   </application> |         <activity android:name=".quiz.QuizResultActivity" | ||||||
|  |             android:label="@string/result"/> | ||||||
|  | 
 | ||||||
|  |         <activity | ||||||
|  |             android:name=".customselector.ui.selector.CustomSelectorActivity" | ||||||
|  |             android:label="@string/title_activity_custom_selector" | ||||||
|  |             android:configChanges="screenSize|keyboard|orientation" | ||||||
|  |             android:parentActivityName=".contributions.MainActivity" /> | ||||||
|  | 
 | ||||||
|  |         <activity | ||||||
|  |             android:name=".category.CategoryDetailsActivity" | ||||||
|  |             android:label="@string/title_activity_featured_images" | ||||||
|  |             android:configChanges="screenSize|keyboard|orientation" | ||||||
|  |             android:parentActivityName=".contributions.MainActivity" /> | ||||||
|  | 
 | ||||||
|  |         <activity | ||||||
|  |             android:name=".explore.depictions.WikidataItemDetailsActivity" | ||||||
|  |             android:label="@string/title_activity_featured_images" | ||||||
|  |             android:configChanges="screenSize|keyboard|orientation" | ||||||
|  |             android:parentActivityName=".contributions.MainActivity" /> | ||||||
|  | 
 | ||||||
|  |         <activity | ||||||
|  |             android:name=".explore.SearchActivity" | ||||||
|  |             android:label="@string/title_activity_search" | ||||||
|  |             android:launchMode="singleTop" | ||||||
|  |             android:configChanges="orientation|keyboardHidden|screenSize" | ||||||
|  |             android:parentActivityName=".contributions.MainActivity" | ||||||
|  |             /> | ||||||
|  | 
 | ||||||
|  |         <activity | ||||||
|  |             android:name=".profile.ProfileActivity" | ||||||
|  |             android:configChanges="orientation|screenSize|keyboard" | ||||||
|  |             android:label="@string/Profile" /> | ||||||
|  | 
 | ||||||
|  |         <activity | ||||||
|  |             android:name=".review.ReviewActivity" | ||||||
|  |             android:label="@string/title_activity_review" /> | ||||||
|  | 
 | ||||||
|  |         <activity | ||||||
|  |           android:name=".LocationPicker.LocationPickerActivity" | ||||||
|  |           android:label="Location Picker" /> | ||||||
|  | 
 | ||||||
|  |         <service | ||||||
|  |             android:name=".auth.WikiAccountAuthenticatorService" | ||||||
|  |             android:exported="true" | ||||||
|  |             android:process=":auth"> | ||||||
|  |             <intent-filter> | ||||||
|  |                 <action android:name="android.accounts.AccountAuthenticator" /> | ||||||
|  |             </intent-filter> | ||||||
|  |             <meta-data | ||||||
|  |                 android:name="android.accounts.AccountAuthenticator" | ||||||
|  |                 android:resource="@xml/authenticator" /> | ||||||
|  |         </service> | ||||||
|  | 
 | ||||||
|  |         <service | ||||||
|  |             android:name="org.acra.sender.SenderService" | ||||||
|  |             android:exported="false" | ||||||
|  |             android:process=":acra" /> | ||||||
|  | 
 | ||||||
|  |         <provider | ||||||
|  |             android:name=".filepicker.ExtendedFileProvider" | ||||||
|  |             android:authorities="${applicationId}.provider" | ||||||
|  |             android:exported="false" | ||||||
|  |             android:grantUriPermissions="true"> | ||||||
|  |             <meta-data | ||||||
|  |                 android:name="android.support.FILE_PROVIDER_PATHS" | ||||||
|  |                 android:resource="@xml/provider_paths" /> | ||||||
|  |         </provider> | ||||||
|  | 
 | ||||||
|  |         <provider | ||||||
|  |             android:name=".category.CategoryContentProvider" | ||||||
|  |             android:authorities="${applicationId}.categories.contentprovider" | ||||||
|  |             android:exported="false" | ||||||
|  |             android:label="@string/provider_categories" | ||||||
|  |             android:syncable="false" /> | ||||||
|  | 
 | ||||||
|  |         <provider | ||||||
|  |             android:name=".explore.recentsearches.RecentSearchesContentProvider" | ||||||
|  |             android:authorities="${applicationId}.explore.recentsearches.contentprovider" | ||||||
|  |             android:exported="false" | ||||||
|  |             android:label="@string/provider_searches" | ||||||
|  |             android:syncable="false" /> | ||||||
|  | 
 | ||||||
|  |         <provider | ||||||
|  |           android:name=".recentlanguages.RecentLanguagesContentProvider" | ||||||
|  |           android:authorities="${applicationId}.recentlanguages.contentprovider" | ||||||
|  |           android:exported="false" | ||||||
|  |           android:label="@string/provider_recent_languages" | ||||||
|  |           android:syncable="false" /> | ||||||
|  | 
 | ||||||
|  |         <provider | ||||||
|  |             android:name=".bookmarks.pictures.BookmarkPicturesContentProvider" | ||||||
|  |             android:authorities="${applicationId}.bookmarks.contentprovider" | ||||||
|  |             android:exported="false" | ||||||
|  |             android:label="@string/provider_bookmarks" | ||||||
|  |             android:syncable="false" /> | ||||||
|  | 
 | ||||||
|  |         <provider | ||||||
|  |             android:name=".bookmarks.locations.BookmarkLocationsContentProvider" | ||||||
|  |             android:authorities="${applicationId}.bookmarks.locations.contentprovider" | ||||||
|  |             android:exported="false" | ||||||
|  |             android:label="@string/provider_bookmarks_location" | ||||||
|  |             android:syncable="false" /> | ||||||
|  | 
 | ||||||
|  |         <provider | ||||||
|  |           android:name=".bookmarks.items.BookmarkItemsContentProvider" | ||||||
|  |           android:authorities="${applicationId}.bookmarks.items.contentprovider" | ||||||
|  |           android:exported="false" | ||||||
|  |           android:label="@string/provider_bookmarks_location" | ||||||
|  |           android:syncable="false" /> | ||||||
|  | 
 | ||||||
|  |       <receiver android:name=".widget.PicOfDayAppWidget"> | ||||||
|  |             <intent-filter> | ||||||
|  |                 <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> | ||||||
|  |             </intent-filter> | ||||||
|  | 
 | ||||||
|  |             <meta-data | ||||||
|  |                 android:name="android.appwidget.provider" | ||||||
|  |                 android:resource="@xml/pic_of_day_app_widget_info" /> | ||||||
|  |         </receiver> | ||||||
|  | 
 | ||||||
|  |         <uses-library android:name="org.apache.http.legacy" android:required="false" /> | ||||||
|  | 
 | ||||||
|  |     </application> | ||||||
| 
 | 
 | ||||||
| </manifest> | </manifest> | ||||||
|  |  | ||||||
							
								
								
									
										177
									
								
								app/src/main/java/fr/free/nrw/commons/AboutActivity.java
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										177
									
								
								app/src/main/java/fr/free/nrw/commons/AboutActivity.java
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,177 @@ | ||||||
|  | package fr.free.nrw.commons; | ||||||
|  | 
 | ||||||
|  | import android.annotation.SuppressLint; | ||||||
|  | import android.app.AlertDialog; | ||||||
|  | import android.content.Intent; | ||||||
|  | import android.net.Uri; | ||||||
|  | import android.os.Bundle; | ||||||
|  | import android.view.Menu; | ||||||
|  | import android.view.MenuInflater; | ||||||
|  | import android.view.MenuItem; | ||||||
|  | import android.view.View; | ||||||
|  | import android.widget.ArrayAdapter; | ||||||
|  | import android.widget.LinearLayout; | ||||||
|  | import android.widget.Spinner; | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import fr.free.nrw.commons.databinding.ActivityAboutBinding; | ||||||
|  | import fr.free.nrw.commons.theme.BaseActivity; | ||||||
|  | import fr.free.nrw.commons.utils.ConfigUtils; | ||||||
|  | import java.util.Collections; | ||||||
|  | import java.util.List; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Represents about screen of this app | ||||||
|  |  */ | ||||||
|  | public class AboutActivity extends BaseActivity { | ||||||
|  | 
 | ||||||
|  |     /* | ||||||
|  |       This View Binding class is auto-generated for each xml file. The format is usually the name | ||||||
|  |       of the file with PascalCasing (The underscore characters will be ignored). | ||||||
|  |       More information is available at https://developer.android.com/topic/libraries/view-binding | ||||||
|  |      */ | ||||||
|  |     private ActivityAboutBinding binding; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * This method helps in the creation About screen | ||||||
|  |      * | ||||||
|  |      * @param savedInstanceState Data bundle | ||||||
|  |      */ | ||||||
|  |     @Override | ||||||
|  |     @SuppressLint("StringFormatInvalid") | ||||||
|  |     public void onCreate(Bundle savedInstanceState) { | ||||||
|  |         super.onCreate(savedInstanceState); | ||||||
|  | 
 | ||||||
|  |         /* | ||||||
|  |           Instead of just setting the view with the xml file. We need to use View Binding class. | ||||||
|  |          */ | ||||||
|  |         binding = ActivityAboutBinding.inflate(getLayoutInflater()); | ||||||
|  |         final View view = binding.getRoot(); | ||||||
|  |         setContentView(view); | ||||||
|  | 
 | ||||||
|  |         setSupportActionBar(binding.toolbarBinding.toolbar); | ||||||
|  |         getSupportActionBar().setDisplayHomeAsUpEnabled(true); | ||||||
|  |         final String aboutText = getString(R.string.about_license); | ||||||
|  |         /* | ||||||
|  |           We can then access all the views by just using the id names like this. | ||||||
|  |           camelCasing is used with underscore characters being ignored. | ||||||
|  |          */ | ||||||
|  |         binding.aboutLicense.setHtmlText(aboutText); | ||||||
|  | 
 | ||||||
|  |         @SuppressLint("StringFormatMatches") | ||||||
|  |         String improveText = String.format(getString(R.string.about_improve), Urls.NEW_ISSUE_URL); | ||||||
|  |         binding.aboutImprove.setHtmlText(improveText); | ||||||
|  |         binding.aboutVersion.setText(ConfigUtils.getVersionNameWithSha(getApplicationContext())); | ||||||
|  | 
 | ||||||
|  |         Utils.setUnderlinedText(binding.aboutFaq, R.string.about_faq, getApplicationContext()); | ||||||
|  |         Utils.setUnderlinedText(binding.aboutRateUs, R.string.about_rate_us, getApplicationContext()); | ||||||
|  |         Utils.setUnderlinedText(binding.aboutUserGuide, R.string.user_guide, getApplicationContext()); | ||||||
|  |         Utils.setUnderlinedText(binding.aboutPrivacyPolicy, R.string.about_privacy_policy, getApplicationContext()); | ||||||
|  |         Utils.setUnderlinedText(binding.aboutTranslate, R.string.about_translate, getApplicationContext()); | ||||||
|  |         Utils.setUnderlinedText(binding.aboutCredits, R.string.about_credits, getApplicationContext()); | ||||||
|  | 
 | ||||||
|  |         /* | ||||||
|  |           To set listeners, we can create a separate method and use lambda syntax. | ||||||
|  |         */ | ||||||
|  |         binding.facebookLaunchIcon.setOnClickListener(this::launchFacebook); | ||||||
|  |         binding.githubLaunchIcon.setOnClickListener(this::launchGithub); | ||||||
|  |         binding.websiteLaunchIcon.setOnClickListener(this::launchWebsite); | ||||||
|  |         binding.aboutRateUs.setOnClickListener(this::launchRatings); | ||||||
|  |         binding.aboutCredits.setOnClickListener(this::launchCredits); | ||||||
|  |         binding.aboutPrivacyPolicy.setOnClickListener(this::launchPrivacyPolicy); | ||||||
|  |         binding.aboutUserGuide.setOnClickListener(this::launchUserGuide); | ||||||
|  |         binding.aboutFaq.setOnClickListener(this::launchFrequentlyAskedQuesions); | ||||||
|  |         binding.aboutTranslate.setOnClickListener(this::launchTranslate); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public boolean onSupportNavigateUp() { | ||||||
|  |         onBackPressed(); | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void launchFacebook(View view) { | ||||||
|  |         Intent intent; | ||||||
|  |         try { | ||||||
|  |             intent = new Intent(Intent.ACTION_VIEW, Uri.parse(Urls.FACEBOOK_APP_URL)); | ||||||
|  |             intent.setPackage(Urls.FACEBOOK_PACKAGE_NAME); | ||||||
|  |             startActivity(intent); | ||||||
|  |         } catch (Exception e) { | ||||||
|  |             Utils.handleWebUrl(this, Uri.parse(Urls.FACEBOOK_WEB_URL)); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void launchGithub(View view) { | ||||||
|  |         Utils.handleWebUrl(this, Uri.parse(Urls.GITHUB_REPO_URL)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void launchWebsite(View view) { | ||||||
|  |         Utils.handleWebUrl(this, Uri.parse(Urls.WEBSITE_URL)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void launchRatings(View view){ | ||||||
|  |         Utils.rateApp(this); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void launchCredits(View view) { | ||||||
|  |         Utils.handleWebUrl(this, Uri.parse(Urls.CREDITS_URL)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void launchUserGuide(View view) { | ||||||
|  |         Utils.handleWebUrl(this, Uri.parse(Urls.USER_GUIDE_URL)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void launchPrivacyPolicy(View view) { | ||||||
|  |         Utils.handleWebUrl(this, Uri.parse(BuildConfig.PRIVACY_POLICY_URL)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void launchFrequentlyAskedQuesions(View view) { | ||||||
|  |         Utils.handleWebUrl(this, Uri.parse(Urls.FAQ_URL)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public boolean onCreateOptionsMenu(Menu menu) { | ||||||
|  |         MenuInflater inflater = getMenuInflater(); | ||||||
|  |         inflater.inflate(R.menu.menu_about, menu); | ||||||
|  |         return super.onCreateOptionsMenu(menu); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public boolean onOptionsItemSelected(MenuItem item) { | ||||||
|  |         switch (item.getItemId()) { | ||||||
|  |             case R.id.share_app_icon: | ||||||
|  |                 String shareText = String.format(getString(R.string.share_text), Urls.PLAY_STORE_URL_PREFIX + this.getPackageName()); | ||||||
|  |                 Intent sendIntent = new Intent(); | ||||||
|  |                 sendIntent.setAction(Intent.ACTION_SEND); | ||||||
|  |                 sendIntent.putExtra(Intent.EXTRA_TEXT, shareText); | ||||||
|  |                 sendIntent.setType("text/plain"); | ||||||
|  |                 startActivity(Intent.createChooser(sendIntent, getString(R.string.share_via))); | ||||||
|  |                 return true; | ||||||
|  |             default: | ||||||
|  |                 return super.onOptionsItemSelected(item); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void launchTranslate(View view) { | ||||||
|  |         @NonNull List<String> sortedLocalizedNamesRef = CommonsApplication.getInstance().getLanguageLookUpTable().getCanonicalNames(); | ||||||
|  |         Collections.sort(sortedLocalizedNamesRef); | ||||||
|  |         final ArrayAdapter<String> languageAdapter = new ArrayAdapter<>(AboutActivity.this, | ||||||
|  |                 android.R.layout.simple_spinner_dropdown_item, sortedLocalizedNamesRef); | ||||||
|  |         final Spinner spinner = new Spinner(AboutActivity.this); | ||||||
|  |         spinner.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT)); | ||||||
|  |         spinner.setAdapter(languageAdapter); | ||||||
|  |         spinner.setGravity(17); | ||||||
|  |         spinner.setPadding(50,0,0,0); | ||||||
|  |         AlertDialog.Builder builder = new AlertDialog.Builder(AboutActivity.this); | ||||||
|  |         builder.setView(spinner); | ||||||
|  |         builder.setTitle(R.string.about_translate_title) | ||||||
|  |                 .setMessage(R.string.about_translate_message) | ||||||
|  |                 .setPositiveButton(R.string.about_translate_proceed, (dialog, which) -> { | ||||||
|  |                     String langCode = CommonsApplication.getInstance().getLanguageLookUpTable().getCodes().get(spinner.getSelectedItemPosition()); | ||||||
|  |                     Utils.handleWebUrl(AboutActivity.this, Uri.parse(Urls.TRANSLATE_WIKI_URL + langCode)); | ||||||
|  |                 }); | ||||||
|  |         builder.setNegativeButton(R.string.about_translate_cancel, (dialog, which) -> dialog.cancel()); | ||||||
|  |         builder.create().show(); | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -1,207 +0,0 @@ | ||||||
| package fr.free.nrw.commons |  | ||||||
| 
 |  | ||||||
| import android.annotation.SuppressLint |  | ||||||
| import android.content.ActivityNotFoundException |  | ||||||
| import android.content.Intent |  | ||||||
| import android.content.Intent.ACTION_VIEW |  | ||||||
| import android.net.Uri |  | ||||||
| import android.os.Bundle |  | ||||||
| import android.view.Menu |  | ||||||
| import android.view.MenuItem |  | ||||||
| import android.view.View |  | ||||||
| import android.widget.ArrayAdapter |  | ||||||
| import android.widget.LinearLayout |  | ||||||
| import android.widget.Spinner |  | ||||||
| import fr.free.nrw.commons.CommonsApplication.Companion.instance |  | ||||||
| import fr.free.nrw.commons.databinding.ActivityAboutBinding |  | ||||||
| import fr.free.nrw.commons.theme.BaseActivity |  | ||||||
| import fr.free.nrw.commons.utils.ConfigUtils.getVersionNameWithSha |  | ||||||
| import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog |  | ||||||
| import java.util.Collections |  | ||||||
| import androidx.core.net.toUri |  | ||||||
| import fr.free.nrw.commons.utils.applyEdgeToEdgeTopInsets |  | ||||||
| import fr.free.nrw.commons.utils.handleWebUrl |  | ||||||
| import fr.free.nrw.commons.utils.setUnderlinedText |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Represents about screen of this app |  | ||||||
|  */ |  | ||||||
| class AboutActivity : BaseActivity() { |  | ||||||
|     /* |  | ||||||
|          This View Binding class is auto-generated for each xml file. The format is usually the name |  | ||||||
|          of the file with PascalCasing (The underscore characters will be ignored). |  | ||||||
|          More information is available at https://developer.android.com/topic/libraries/view-binding |  | ||||||
|         */ |  | ||||||
|     private var binding: ActivityAboutBinding? = null |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * This method helps in the creation About screen |  | ||||||
|      * |  | ||||||
|      * @param savedInstanceState Data bundle |  | ||||||
|      */ |  | ||||||
|     @SuppressLint("StringFormatInvalid")  //TODO: |  | ||||||
|     public override fun onCreate(savedInstanceState: Bundle?) { |  | ||||||
|         super.onCreate(savedInstanceState) |  | ||||||
| 
 |  | ||||||
|         /* |  | ||||||
|           Instead of just setting the view with the xml file. We need to use View Binding class. |  | ||||||
|          */ |  | ||||||
|         binding = ActivityAboutBinding.inflate(layoutInflater) |  | ||||||
|         val view: View = binding!!.root |  | ||||||
|         applyEdgeToEdgeTopInsets(binding!!.toolbarLayout) |  | ||||||
|         setContentView(view) |  | ||||||
| 
 |  | ||||||
|         setSupportActionBar(binding!!.toolbarBinding.toolbar) |  | ||||||
|         supportActionBar!!.setDisplayHomeAsUpEnabled(true) |  | ||||||
|         val aboutText = getString(R.string.about_license) |  | ||||||
|         /* |  | ||||||
|           We can then access all the views by just using the id names like this. |  | ||||||
|           camelCasing is used with underscore characters being ignored. |  | ||||||
|          */ |  | ||||||
|         binding!!.aboutLicense.setHtmlText(aboutText) |  | ||||||
| 
 |  | ||||||
|         @SuppressLint("StringFormatMatches") // TODO: |  | ||||||
|         val improveText = |  | ||||||
|             String.format(getString(R.string.about_improve), Urls.NEW_ISSUE_URL) |  | ||||||
|         binding!!.aboutImprove.setHtmlText(improveText) |  | ||||||
|         binding!!.aboutVersion.text = applicationContext.getVersionNameWithSha() |  | ||||||
| 
 |  | ||||||
|         binding!!.aboutFaq.setUnderlinedText(R.string.about_faq) |  | ||||||
|         binding!!.aboutRateUs.setUnderlinedText(R.string.about_rate_us) |  | ||||||
|         binding!!.aboutUserGuide.setUnderlinedText(R.string.user_guide) |  | ||||||
|         binding!!.aboutPrivacyPolicy.setUnderlinedText(R.string.about_privacy_policy) |  | ||||||
|         binding!!.aboutTranslate.setUnderlinedText(R.string.about_translate) |  | ||||||
|         binding!!.aboutCredits.setUnderlinedText(R.string.about_credits) |  | ||||||
| 
 |  | ||||||
|         /* |  | ||||||
|           To set listeners, we can create a separate method and use lambda syntax. |  | ||||||
|         */ |  | ||||||
|         binding!!.facebookLaunchIcon.setOnClickListener(::launchFacebook) |  | ||||||
|         binding!!.githubLaunchIcon.setOnClickListener(::launchGithub) |  | ||||||
|         binding!!.websiteLaunchIcon.setOnClickListener(::launchWebsite) |  | ||||||
|         binding!!.aboutRateUs.setOnClickListener(::launchRatings) |  | ||||||
|         binding!!.aboutCredits.setOnClickListener(::launchCredits) |  | ||||||
|         binding!!.aboutPrivacyPolicy.setOnClickListener(::launchPrivacyPolicy) |  | ||||||
|         binding!!.aboutUserGuide.setOnClickListener(::launchUserGuide) |  | ||||||
|         binding!!.aboutFaq.setOnClickListener(::launchFrequentlyAskedQuesions) |  | ||||||
|         binding!!.aboutTranslate.setOnClickListener(::launchTranslate) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override fun onSupportNavigateUp(): Boolean { |  | ||||||
|         onBackPressed() |  | ||||||
|         return true |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fun launchFacebook(view: View?) { |  | ||||||
|         val intent: Intent |  | ||||||
|         try { |  | ||||||
|             intent = Intent(ACTION_VIEW, Urls.FACEBOOK_APP_URL.toUri()) |  | ||||||
|             intent.setPackage(Urls.FACEBOOK_PACKAGE_NAME) |  | ||||||
|             startActivity(intent) |  | ||||||
|         } catch (e: Exception) { |  | ||||||
|             handleWebUrl(this, Urls.FACEBOOK_WEB_URL.toUri()) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fun launchGithub(view: View?) { |  | ||||||
|         val intent: Intent |  | ||||||
|         try { |  | ||||||
|             intent = Intent(ACTION_VIEW, Urls.GITHUB_REPO_URL.toUri()) |  | ||||||
|             intent.setPackage(Urls.GITHUB_PACKAGE_NAME) |  | ||||||
|             startActivity(intent) |  | ||||||
|         } catch (e: Exception) { |  | ||||||
|             handleWebUrl(this, Urls.GITHUB_REPO_URL.toUri()) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fun launchWebsite(view: View?) { |  | ||||||
|         handleWebUrl(this, Urls.WEBSITE_URL.toUri()) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fun launchRatings(view: View?) { |  | ||||||
|         try { |  | ||||||
|             startActivity( |  | ||||||
|                 Intent( |  | ||||||
|                     ACTION_VIEW, |  | ||||||
|                     (Urls.PLAY_STORE_PREFIX + packageName).toUri() |  | ||||||
|                 ) |  | ||||||
|             ) |  | ||||||
|         } catch (_: ActivityNotFoundException) { |  | ||||||
|             handleWebUrl(this, (Urls.PLAY_STORE_URL_PREFIX + packageName).toUri()) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fun launchCredits(view: View?) { |  | ||||||
|         handleWebUrl(this, Urls.CREDITS_URL.toUri()) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fun launchUserGuide(view: View?) { |  | ||||||
|         handleWebUrl(this, Urls.USER_GUIDE_URL.toUri()) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fun launchPrivacyPolicy(view: View?) { |  | ||||||
|         handleWebUrl(this, BuildConfig.PRIVACY_POLICY_URL.toUri()) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fun launchFrequentlyAskedQuesions(view: View?) { |  | ||||||
|         handleWebUrl(this, Urls.FAQ_URL.toUri()) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override fun onCreateOptionsMenu(menu: Menu): Boolean { |  | ||||||
|         val inflater = menuInflater |  | ||||||
|         inflater.inflate(R.menu.menu_about, menu) |  | ||||||
|         return super.onCreateOptionsMenu(menu) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override fun onOptionsItemSelected(item: MenuItem): Boolean { |  | ||||||
|         when (item.itemId) { |  | ||||||
|             R.id.share_app_icon -> { |  | ||||||
|                 val shareText = String.format( |  | ||||||
|                     getString(R.string.share_text), |  | ||||||
|                     Urls.PLAY_STORE_URL_PREFIX + this.packageName |  | ||||||
|                 ) |  | ||||||
|                 val sendIntent = Intent() |  | ||||||
|                 sendIntent.setAction(Intent.ACTION_SEND) |  | ||||||
|                 sendIntent.putExtra(Intent.EXTRA_TEXT, shareText) |  | ||||||
|                 sendIntent.setType("text/plain") |  | ||||||
|                 startActivity(Intent.createChooser(sendIntent, getString(R.string.share_via))) |  | ||||||
|                 return true |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             else -> return super.onOptionsItemSelected(item) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fun launchTranslate(view: View?) { |  | ||||||
|         val sortedLocalizedNamesRef = instance.languageLookUpTable!!.getCanonicalNames() |  | ||||||
|         Collections.sort(sortedLocalizedNamesRef) |  | ||||||
|         val languageAdapter = ArrayAdapter( |  | ||||||
|             this@AboutActivity, |  | ||||||
|             android.R.layout.simple_spinner_dropdown_item, sortedLocalizedNamesRef |  | ||||||
|         ) |  | ||||||
|         val spinner = Spinner(this@AboutActivity) |  | ||||||
|         spinner.layoutParams = |  | ||||||
|             LinearLayout.LayoutParams( |  | ||||||
|                 LinearLayout.LayoutParams.WRAP_CONTENT, |  | ||||||
|                 LinearLayout.LayoutParams.WRAP_CONTENT |  | ||||||
|             ) |  | ||||||
|         spinner.adapter = languageAdapter |  | ||||||
|         spinner.gravity = 17 |  | ||||||
|         spinner.setPadding(50, 0, 0, 0) |  | ||||||
| 
 |  | ||||||
|         val positiveButtonRunnable = Runnable { |  | ||||||
|             val langCode = instance.languageLookUpTable!!.getCodes()[spinner.selectedItemPosition] |  | ||||||
|             handleWebUrl(this@AboutActivity, (Urls.TRANSLATE_WIKI_URL + langCode).toUri()) |  | ||||||
|         } |  | ||||||
|         showAlertDialog( |  | ||||||
|             this, |  | ||||||
|             getString(R.string.about_translate_title), |  | ||||||
|             getString(R.string.about_translate_message), |  | ||||||
|             getString(R.string.about_translate_proceed), |  | ||||||
|             getString(R.string.about_translate_cancel), |  | ||||||
|             positiveButtonRunnable, |  | ||||||
|             {}, |  | ||||||
|             spinner |  | ||||||
|         ) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -1,63 +0,0 @@ | ||||||
| package fr.free.nrw.commons |  | ||||||
| 
 |  | ||||||
| import android.content.Context |  | ||||||
| import android.graphics.Bitmap |  | ||||||
| import android.graphics.Canvas |  | ||||||
| import android.graphics.drawable.BitmapDrawable |  | ||||||
| import android.graphics.drawable.Drawable |  | ||||||
| import fr.free.nrw.commons.location.LatLng |  | ||||||
| import fr.free.nrw.commons.nearby.Place |  | ||||||
| 
 |  | ||||||
| class BaseMarker { |  | ||||||
|     private var _position: LatLng = LatLng(0.0, 0.0, 0f) |  | ||||||
|     private var _title: String = "" |  | ||||||
|     private var _place: Place = Place() |  | ||||||
|     private var _icon: Bitmap? = null |  | ||||||
| 
 |  | ||||||
|     var position: LatLng |  | ||||||
|         get() = _position |  | ||||||
|         set(value) { |  | ||||||
|             _position = value |  | ||||||
|         } |  | ||||||
|     var title: String |  | ||||||
|         get() = _title |  | ||||||
|         set(value) { |  | ||||||
|             _title = value |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|     var place: Place |  | ||||||
|         get() = _place |  | ||||||
|         set(value) { |  | ||||||
|             _place = value |  | ||||||
|         } |  | ||||||
|     var icon: Bitmap? |  | ||||||
|         get() = _icon |  | ||||||
|         set(value) { |  | ||||||
|             _icon = value |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|     constructor() { |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fun fromResource( |  | ||||||
|         context: Context, |  | ||||||
|         drawableResId: Int, |  | ||||||
|     ) { |  | ||||||
|         val drawable: Drawable = context.resources.getDrawable(drawableResId) |  | ||||||
|         icon = |  | ||||||
|             if (drawable is BitmapDrawable) { |  | ||||||
|                 drawable.bitmap |  | ||||||
|             } else { |  | ||||||
|                 val bitmap = |  | ||||||
|                     Bitmap.createBitmap( |  | ||||||
|                         drawable.intrinsicWidth, |  | ||||||
|                         drawable.intrinsicHeight, |  | ||||||
|                         Bitmap.Config.ARGB_8888, |  | ||||||
|                     ) |  | ||||||
|                 val canvas = Canvas(bitmap) |  | ||||||
|                 drawable.setBounds(0, 0, canvas.width, canvas.height) |  | ||||||
|                 drawable.draw(canvas) |  | ||||||
|                 bitmap |  | ||||||
|             } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										18
									
								
								app/src/main/java/fr/free/nrw/commons/BasePresenter.java
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								app/src/main/java/fr/free/nrw/commons/BasePresenter.java
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,18 @@ | ||||||
|  | package fr.free.nrw.commons; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Base presenter, enforcing contracts to atach and detach view | ||||||
|  |  */ | ||||||
|  | public interface BasePresenter<T> { | ||||||
|  |     /** | ||||||
|  |      * Until a view is attached, it is open to listen events from the presenter | ||||||
|  |      */ | ||||||
|  |     void onAttachView(@NonNull T view); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Detaching a view makes sure that the view no more receives events from the presenter | ||||||
|  |      */ | ||||||
|  |     void onDetachView(); | ||||||
|  | } | ||||||
|  | @ -1,10 +0,0 @@ | ||||||
| package fr.free.nrw.commons |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Base presenter, enforcing contracts to attach and detach view |  | ||||||
|  */ |  | ||||||
| interface BasePresenter<T> { |  | ||||||
|     fun onAttachView(view: T) |  | ||||||
| 
 |  | ||||||
|     fun onDetachView() |  | ||||||
| } |  | ||||||
|  | @ -10,7 +10,6 @@ object BetaConstants { | ||||||
|      * production server where beta server does not work |      * production server where beta server does not work | ||||||
|      */ |      */ | ||||||
|     const val COMMONS_URL = "https://commons.wikimedia.org/" |     const val COMMONS_URL = "https://commons.wikimedia.org/" | ||||||
| 
 |  | ||||||
|     /** |     /** | ||||||
|      * Commons production's depicts property which is used in beta for some specific GET calls on |      * Commons production's depicts property which is used in beta for some specific GET calls on | ||||||
|      * production server where beta server does not work |      * production server where beta server does not work | ||||||
|  |  | ||||||
|  | @ -1,33 +0,0 @@ | ||||||
| package fr.free.nrw.commons |  | ||||||
| 
 |  | ||||||
| import android.os.Parcel |  | ||||||
| import android.os.Parcelable |  | ||||||
| 
 |  | ||||||
| class CameraPosition( |  | ||||||
|     val latitude: Double, |  | ||||||
|     val longitude: Double, |  | ||||||
|     val zoom: Double, |  | ||||||
| ) : Parcelable { |  | ||||||
|     constructor(parcel: Parcel) : this( |  | ||||||
|         parcel.readDouble(), |  | ||||||
|         parcel.readDouble(), |  | ||||||
|         parcel.readDouble(), |  | ||||||
|     ) |  | ||||||
| 
 |  | ||||||
|     override fun writeToParcel( |  | ||||||
|         parcel: Parcel, |  | ||||||
|         flags: Int, |  | ||||||
|     ) { |  | ||||||
|         parcel.writeDouble(latitude) |  | ||||||
|         parcel.writeDouble(longitude) |  | ||||||
|         parcel.writeDouble(zoom) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override fun describeContents(): Int = 0 |  | ||||||
| 
 |  | ||||||
|     companion object CREATOR : Parcelable.Creator<CameraPosition> { |  | ||||||
|         override fun createFromParcel(parcel: Parcel): CameraPosition = CameraPosition(parcel) |  | ||||||
| 
 |  | ||||||
|         override fun newArray(size: Int): Array<CameraPosition?> = arrayOfNulls(size) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										86
									
								
								app/src/main/java/fr/free/nrw/commons/CommonsAppAdapter.java
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								app/src/main/java/fr/free/nrw/commons/CommonsAppAdapter.java
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,86 @@ | ||||||
|  | package fr.free.nrw.commons; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | 
 | ||||||
|  | import org.wikipedia.AppAdapter; | ||||||
|  | import org.wikipedia.dataclient.SharedPreferenceCookieManager; | ||||||
|  | import org.wikipedia.dataclient.WikiSite; | ||||||
|  | import org.wikipedia.json.GsonMarshaller; | ||||||
|  | import org.wikipedia.json.GsonUnmarshaller; | ||||||
|  | import org.wikipedia.login.LoginResult; | ||||||
|  | 
 | ||||||
|  | import fr.free.nrw.commons.auth.SessionManager; | ||||||
|  | import fr.free.nrw.commons.kvstore.JsonKvStore; | ||||||
|  | import okhttp3.OkHttpClient; | ||||||
|  | 
 | ||||||
|  | public class CommonsAppAdapter extends AppAdapter { | ||||||
|  |     private final int DEFAULT_THUMB_SIZE = 640; | ||||||
|  |     private final String COOKIE_STORE_NAME = "cookie_store"; | ||||||
|  | 
 | ||||||
|  |     private final SessionManager sessionManager; | ||||||
|  |     private final JsonKvStore preferences; | ||||||
|  | 
 | ||||||
|  |     CommonsAppAdapter(@NonNull SessionManager sessionManager, @NonNull JsonKvStore preferences) { | ||||||
|  |         this.sessionManager = sessionManager; | ||||||
|  |         this.preferences = preferences; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public String getMediaWikiBaseUrl() { | ||||||
|  |         return BuildConfig.COMMONS_URL; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public String getRestbaseUriFormat() { | ||||||
|  |         return BuildConfig.COMMONS_URL; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public OkHttpClient getOkHttpClient(@NonNull WikiSite wikiSite) { | ||||||
|  |         return OkHttpConnectionFactory.getClient(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public int getDesiredLeadImageDp() { | ||||||
|  |         return DEFAULT_THUMB_SIZE; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public boolean isLoggedIn() { | ||||||
|  |         return sessionManager.isUserLoggedIn(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public String getUserName() { | ||||||
|  |         return sessionManager.getUserName(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public String getPassword() { | ||||||
|  |         return sessionManager.getPassword(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void updateAccount(@NonNull LoginResult result) { | ||||||
|  |         sessionManager.updateAccount(result); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public SharedPreferenceCookieManager getCookies() { | ||||||
|  |         if (!preferences.contains(COOKIE_STORE_NAME)) { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |         return GsonUnmarshaller.unmarshal(SharedPreferenceCookieManager.class, | ||||||
|  |                 preferences.getString(COOKIE_STORE_NAME, null)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void setCookies(@NonNull SharedPreferenceCookieManager cookies) { | ||||||
|  |         preferences.putString(COOKIE_STORE_NAME, GsonMarshaller.marshal(cookies)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public boolean logErrorsInsteadOfCrashing() { | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										365
									
								
								app/src/main/java/fr/free/nrw/commons/CommonsApplication.java
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										365
									
								
								app/src/main/java/fr/free/nrw/commons/CommonsApplication.java
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,365 @@ | ||||||
|  | package fr.free.nrw.commons; | ||||||
|  | 
 | ||||||
|  | import static fr.free.nrw.commons.data.DBOpenHelper.CONTRIBUTIONS_TABLE; | ||||||
|  | import static org.acra.ReportField.ANDROID_VERSION; | ||||||
|  | import static org.acra.ReportField.APP_VERSION_CODE; | ||||||
|  | import static org.acra.ReportField.APP_VERSION_NAME; | ||||||
|  | import static org.acra.ReportField.PHONE_MODEL; | ||||||
|  | import static org.acra.ReportField.STACK_TRACE; | ||||||
|  | import static org.acra.ReportField.USER_COMMENT; | ||||||
|  | 
 | ||||||
|  | import android.annotation.SuppressLint; | ||||||
|  | import android.app.NotificationChannel; | ||||||
|  | import android.app.NotificationManager; | ||||||
|  | import android.content.Context; | ||||||
|  | import android.database.sqlite.SQLiteDatabase; | ||||||
|  | import android.database.sqlite.SQLiteException; | ||||||
|  | import android.os.Build; | ||||||
|  | import android.os.Process; | ||||||
|  | import android.util.Log; | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.multidex.BuildConfig; | ||||||
|  | import androidx.multidex.MultiDexApplication; | ||||||
|  | import com.facebook.drawee.backends.pipeline.Fresco; | ||||||
|  | import com.facebook.imagepipeline.core.ImagePipeline; | ||||||
|  | import com.facebook.imagepipeline.core.ImagePipelineConfig; | ||||||
|  | import com.mapbox.mapboxsdk.Mapbox; | ||||||
|  | import com.squareup.leakcanary.LeakCanary; | ||||||
|  | import com.squareup.leakcanary.RefWatcher; | ||||||
|  | import fr.free.nrw.commons.auth.SessionManager; | ||||||
|  | import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table; | ||||||
|  | import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao; | ||||||
|  | import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao; | ||||||
|  | import fr.free.nrw.commons.category.CategoryDao; | ||||||
|  | import fr.free.nrw.commons.concurrency.BackgroundPoolExceptionHandler; | ||||||
|  | import fr.free.nrw.commons.concurrency.ThreadPoolService; | ||||||
|  | import fr.free.nrw.commons.contributions.ContributionDao; | ||||||
|  | import fr.free.nrw.commons.data.DBOpenHelper; | ||||||
|  | import fr.free.nrw.commons.di.ApplicationlessInjection; | ||||||
|  | import fr.free.nrw.commons.kvstore.JsonKvStore; | ||||||
|  | import fr.free.nrw.commons.logging.FileLoggingTree; | ||||||
|  | import fr.free.nrw.commons.logging.LogUtils; | ||||||
|  | import fr.free.nrw.commons.media.CustomOkHttpNetworkFetcher; | ||||||
|  | import fr.free.nrw.commons.settings.Prefs; | ||||||
|  | import fr.free.nrw.commons.upload.FileUtils; | ||||||
|  | import fr.free.nrw.commons.utils.ConfigUtils; | ||||||
|  | import io.reactivex.Completable; | ||||||
|  | import io.reactivex.android.schedulers.AndroidSchedulers; | ||||||
|  | import io.reactivex.internal.functions.Functions; | ||||||
|  | import io.reactivex.plugins.RxJavaPlugins; | ||||||
|  | import io.reactivex.schedulers.Schedulers; | ||||||
|  | import java.io.File; | ||||||
|  | import java.util.HashMap; | ||||||
|  | import java.util.HashSet; | ||||||
|  | import java.util.Map; | ||||||
|  | import java.util.Set; | ||||||
|  | import javax.inject.Inject; | ||||||
|  | import javax.inject.Named; | ||||||
|  | import org.acra.ACRA; | ||||||
|  | import org.acra.annotation.AcraCore; | ||||||
|  | import org.acra.annotation.AcraDialog; | ||||||
|  | import org.acra.annotation.AcraMailSender; | ||||||
|  | import org.acra.data.StringFormat; | ||||||
|  | import org.wikipedia.AppAdapter; | ||||||
|  | import org.wikipedia.language.AppLanguageLookUpTable; | ||||||
|  | import timber.log.Timber; | ||||||
|  | 
 | ||||||
|  | @AcraCore( | ||||||
|  |     buildConfigClass = BuildConfig.class, | ||||||
|  |     resReportSendSuccessToast = R.string.crash_dialog_ok_toast, | ||||||
|  |     reportFormat = StringFormat.KEY_VALUE_LIST, | ||||||
|  |     reportContent = {USER_COMMENT, APP_VERSION_CODE, APP_VERSION_NAME, ANDROID_VERSION, PHONE_MODEL, | ||||||
|  |         STACK_TRACE} | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | @AcraMailSender( | ||||||
|  |     mailTo = "commons-app-android-private@googlegroups.com", | ||||||
|  |     reportAsFile = false | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | @AcraDialog( | ||||||
|  |     resTheme = R.style.Theme_AppCompat_Dialog, | ||||||
|  |     resText = R.string.crash_dialog_text, | ||||||
|  |     resTitle = R.string.crash_dialog_title, | ||||||
|  |     resCommentPrompt = R.string.crash_dialog_comment_prompt | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | public class CommonsApplication extends MultiDexApplication { | ||||||
|  | 
 | ||||||
|  |     public static final String IS_LIMITED_CONNECTION_MODE_ENABLED = "is_limited_connection_mode_enabled"; | ||||||
|  |     @Inject | ||||||
|  |     SessionManager sessionManager; | ||||||
|  |     @Inject | ||||||
|  |     DBOpenHelper dbOpenHelper; | ||||||
|  | 
 | ||||||
|  |     @Inject | ||||||
|  |     @Named("default_preferences") | ||||||
|  |     JsonKvStore defaultPrefs; | ||||||
|  | 
 | ||||||
|  |     @Inject | ||||||
|  |     CustomOkHttpNetworkFetcher customOkHttpNetworkFetcher; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Constants begin | ||||||
|  |      */ | ||||||
|  |     public static final int OPEN_APPLICATION_DETAIL_SETTINGS = 1001; | ||||||
|  | 
 | ||||||
|  |     public static final String DEFAULT_EDIT_SUMMARY = "Uploaded using [[COM:MOA|Commons Mobile App]]"; | ||||||
|  | 
 | ||||||
|  |     public static final String FEEDBACK_EMAIL = "commons-app-android@googlegroups.com"; | ||||||
|  | 
 | ||||||
|  |     public static final String FEEDBACK_EMAIL_SUBJECT = "Commons Android App Feedback"; | ||||||
|  | 
 | ||||||
|  |     public static final String NOTIFICATION_CHANNEL_ID_ALL = "CommonsNotificationAll"; | ||||||
|  | 
 | ||||||
|  |     public static final String FEEDBACK_EMAIL_TEMPLATE_HEADER = "-- Technical information --"; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Constants End | ||||||
|  |      */ | ||||||
|  | 
 | ||||||
|  |     private RefWatcher refWatcher; | ||||||
|  | 
 | ||||||
|  |     private static CommonsApplication INSTANCE; | ||||||
|  | 
 | ||||||
|  |     public static CommonsApplication getInstance() { | ||||||
|  |         return INSTANCE; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private AppLanguageLookUpTable languageLookUpTable; | ||||||
|  | 
 | ||||||
|  |     public AppLanguageLookUpTable getLanguageLookUpTable() { | ||||||
|  |         return languageLookUpTable; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Inject | ||||||
|  |     ContributionDao contributionDao; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      *  In-memory list of contributions whose uploads have been paused by the user | ||||||
|  |      */ | ||||||
|  |     public static Map<String, Boolean> pauseUploads = new HashMap<>(); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Used to declare and initialize various components and dependencies | ||||||
|  |      */ | ||||||
|  |     @Override | ||||||
|  |     public void onCreate() { | ||||||
|  |         super.onCreate(); | ||||||
|  | 
 | ||||||
|  |         INSTANCE = this; | ||||||
|  |         ACRA.init(this); | ||||||
|  |         Mapbox.getInstance(this, getString(R.string.mapbox_commons_app_token)); | ||||||
|  | 
 | ||||||
|  |         ApplicationlessInjection | ||||||
|  |             .getInstance(this) | ||||||
|  |             .getCommonsApplicationComponent() | ||||||
|  |             .inject(this); | ||||||
|  | 
 | ||||||
|  |         AppAdapter.set(new CommonsAppAdapter(sessionManager, defaultPrefs)); | ||||||
|  | 
 | ||||||
|  |         initTimber(); | ||||||
|  | 
 | ||||||
|  |         if (!defaultPrefs.getBoolean("has_user_manually_removed_location")) { | ||||||
|  |             Set<String> defaultExifTagsSet = defaultPrefs.getStringSet(Prefs.MANAGED_EXIF_TAGS); | ||||||
|  |             if (null == defaultExifTagsSet) { | ||||||
|  |                 defaultExifTagsSet = new HashSet<>(); | ||||||
|  |             } | ||||||
|  |             defaultExifTagsSet.add(getString(R.string.exif_tag_location)); | ||||||
|  |             defaultPrefs.putStringSet(Prefs.MANAGED_EXIF_TAGS, defaultExifTagsSet); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  | //        Set DownsampleEnabled to True to downsample the image in case it's heavy | ||||||
|  |         ImagePipelineConfig config = ImagePipelineConfig.newBuilder(this) | ||||||
|  |             .setNetworkFetcher(customOkHttpNetworkFetcher) | ||||||
|  |             .setDownsampleEnabled(true) | ||||||
|  |             .build(); | ||||||
|  |         try { | ||||||
|  |             Fresco.initialize(this, config); | ||||||
|  |         } catch (Exception e) { | ||||||
|  |             Timber.e(e); | ||||||
|  |             // TODO: Remove when we're able to initialize Fresco in test builds. | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         createNotificationChannel(this); | ||||||
|  | 
 | ||||||
|  |         languageLookUpTable = new AppLanguageLookUpTable(this); | ||||||
|  | 
 | ||||||
|  |         // This handler will catch exceptions thrown from Observables after they are disposed, | ||||||
|  |         // or from Observables that are (deliberately or not) missing an onError handler. | ||||||
|  |         RxJavaPlugins.setErrorHandler(Functions.emptyConsumer()); | ||||||
|  | 
 | ||||||
|  |         if (setupLeakCanary() == RefWatcher.DISABLED) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         // Fire progress callbacks for every 3% of uploaded content | ||||||
|  |         System.setProperty("in.yuvi.http.fluent.PROGRESS_TRIGGER_THRESHOLD", "3.0"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Plants debug and file logging tree. Timber lets you plant your own logging trees. | ||||||
|  |      */ | ||||||
|  |     private void initTimber() { | ||||||
|  |         boolean isBeta = ConfigUtils.isBetaFlavour(); | ||||||
|  |         String logFileName = | ||||||
|  |             isBeta ? "CommonsBetaAppLogs" : "CommonsAppLogs"; | ||||||
|  |         String logDirectory = LogUtils.getLogDirectory(); | ||||||
|  |         //Delete stale logs if they have exceeded the specified size | ||||||
|  |         deleteStaleLogs(logFileName, logDirectory); | ||||||
|  | 
 | ||||||
|  |         FileLoggingTree tree = new FileLoggingTree( | ||||||
|  |             Log.VERBOSE, | ||||||
|  |             logFileName, | ||||||
|  |             logDirectory, | ||||||
|  |             1000, | ||||||
|  |             getFileLoggingThreadPool()); | ||||||
|  | 
 | ||||||
|  |         Timber.plant(tree); | ||||||
|  |         Timber.plant(new Timber.DebugTree()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Deletes the logs zip file at the specified directory and file locations specified in the | ||||||
|  |      * params | ||||||
|  |      * | ||||||
|  |      * @param logFileName | ||||||
|  |      * @param logDirectory | ||||||
|  |      */ | ||||||
|  |     private void deleteStaleLogs(String logFileName, String logDirectory) { | ||||||
|  |         try { | ||||||
|  |             File file = new File(logDirectory + "/zip/" + logFileName + ".zip"); | ||||||
|  |             if (file.exists() && file.getTotalSpace() > 1000000) {// In Kbs | ||||||
|  |                 file.delete(); | ||||||
|  |             } | ||||||
|  |         } catch (Exception e) { | ||||||
|  |             Timber.e(e); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static boolean isRoboUnitTest() { | ||||||
|  |         return "robolectric".equals(Build.FINGERPRINT); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private ThreadPoolService getFileLoggingThreadPool() { | ||||||
|  |         return new ThreadPoolService.Builder("file-logging-thread") | ||||||
|  |             .setPriority(Process.THREAD_PRIORITY_LOWEST) | ||||||
|  |             .setPoolSize(1) | ||||||
|  |             .setExceptionHandler(new BackgroundPoolExceptionHandler()) | ||||||
|  |             .build(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static void createNotificationChannel(@NonNull Context context) { | ||||||
|  |         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | ||||||
|  |             NotificationManager manager = (NotificationManager) context | ||||||
|  |                 .getSystemService(Context.NOTIFICATION_SERVICE); | ||||||
|  |             NotificationChannel channel = manager | ||||||
|  |                 .getNotificationChannel(NOTIFICATION_CHANNEL_ID_ALL); | ||||||
|  |             if (channel == null) { | ||||||
|  |                 channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID_ALL, | ||||||
|  |                     context.getString(R.string.notifications_channel_name_all), | ||||||
|  |                     NotificationManager.IMPORTANCE_DEFAULT); | ||||||
|  |                 manager.createNotificationChannel(channel); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public String getUserAgent() { | ||||||
|  |         return "Commons/" + ConfigUtils.getVersionNameWithSha(this) | ||||||
|  |             + " (https://mediawiki.org/wiki/Apps/Commons) Android/" + Build.VERSION.RELEASE; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Helps in setting up LeakCanary library | ||||||
|  |      * | ||||||
|  |      * @return instance of LeakCanary | ||||||
|  |      */ | ||||||
|  |     protected RefWatcher setupLeakCanary() { | ||||||
|  |         if (LeakCanary.isInAnalyzerProcess(this)) { | ||||||
|  |             return RefWatcher.DISABLED; | ||||||
|  |         } | ||||||
|  |         return LeakCanary.install(this); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Provides a way to get member refWatcher | ||||||
|  |      * | ||||||
|  |      * @param context Application context | ||||||
|  |      * @return application member refWatcher | ||||||
|  |      */ | ||||||
|  |     public static RefWatcher getRefWatcher(Context context) { | ||||||
|  |         CommonsApplication application = (CommonsApplication) context.getApplicationContext(); | ||||||
|  |         return application.refWatcher; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * clears data of current application | ||||||
|  |      * | ||||||
|  |      * @param context        Application context | ||||||
|  |      * @param logoutListener Implementation of interface LogoutListener | ||||||
|  |      */ | ||||||
|  |     @SuppressLint("CheckResult") | ||||||
|  |     public void clearApplicationData(Context context, LogoutListener logoutListener) { | ||||||
|  |         File cacheDirectory = context.getCacheDir(); | ||||||
|  |         File applicationDirectory = new File(cacheDirectory.getParent()); | ||||||
|  |         if (applicationDirectory.exists()) { | ||||||
|  |             String[] fileNames = applicationDirectory.list(); | ||||||
|  |             for (String fileName : fileNames) { | ||||||
|  |                 if (!fileName.equals("lib")) { | ||||||
|  |                     FileUtils.deleteFile(new File(applicationDirectory, fileName)); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         sessionManager.logout() | ||||||
|  |             .andThen(Completable.fromAction(() -> { | ||||||
|  |                     Timber.d("All accounts have been removed"); | ||||||
|  |                     clearImageCache(); | ||||||
|  |                     //TODO: fix preference manager | ||||||
|  |                     defaultPrefs.clearAll(); | ||||||
|  |                     defaultPrefs.putBoolean("firstrun", false); | ||||||
|  |                     updateAllDatabases(); | ||||||
|  |                 } | ||||||
|  |             )) | ||||||
|  |             .subscribeOn(Schedulers.io()) | ||||||
|  |             .observeOn(AndroidSchedulers.mainThread()) | ||||||
|  |             .subscribe(logoutListener::onLogoutComplete, Timber::e); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Clear all images cache held by Fresco | ||||||
|  |      */ | ||||||
|  |     private void clearImageCache() { | ||||||
|  |         ImagePipeline imagePipeline = Fresco.getImagePipeline(); | ||||||
|  |         imagePipeline.clearCaches(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Deletes all tables and re-creates them. | ||||||
|  |      */ | ||||||
|  |     private void updateAllDatabases() { | ||||||
|  |         dbOpenHelper.getReadableDatabase().close(); | ||||||
|  |         SQLiteDatabase db = dbOpenHelper.getWritableDatabase(); | ||||||
|  | 
 | ||||||
|  |         CategoryDao.Table.onDelete(db); | ||||||
|  |         dbOpenHelper.deleteTable(db, | ||||||
|  |             CONTRIBUTIONS_TABLE);//Delete the contributions table in the existing db on older versions | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             contributionDao.deleteAll(); | ||||||
|  |         } catch (SQLiteException e) { | ||||||
|  |             Timber.e(e); | ||||||
|  |         } | ||||||
|  |         BookmarkPicturesDao.Table.onDelete(db); | ||||||
|  |         BookmarkLocationsDao.Table.onDelete(db); | ||||||
|  |         Table.onDelete(db); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Interface used to get log-out events | ||||||
|  |      */ | ||||||
|  |     public interface LogoutListener { | ||||||
|  | 
 | ||||||
|  |         void onLogoutComplete(); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,417 +0,0 @@ | ||||||
| package fr.free.nrw.commons |  | ||||||
| 
 |  | ||||||
| import android.annotation.SuppressLint |  | ||||||
| import android.app.Activity |  | ||||||
| import android.app.NotificationChannel |  | ||||||
| import android.app.NotificationManager |  | ||||||
| import android.content.Context |  | ||||||
| import android.content.Intent |  | ||||||
| import android.database.sqlite.SQLiteException |  | ||||||
| import android.os.Build |  | ||||||
| import android.os.Process |  | ||||||
| import android.util.Log |  | ||||||
| import androidx.multidex.MultiDexApplication |  | ||||||
| import com.facebook.drawee.backends.pipeline.Fresco |  | ||||||
| import com.facebook.imagepipeline.core.ImagePipelineConfig |  | ||||||
| import fr.free.nrw.commons.auth.LoginActivity |  | ||||||
| import fr.free.nrw.commons.auth.SessionManager |  | ||||||
| import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable |  | ||||||
| import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable |  | ||||||
| import fr.free.nrw.commons.category.CategoryDao |  | ||||||
| import fr.free.nrw.commons.concurrency.BackgroundPoolExceptionHandler |  | ||||||
| import fr.free.nrw.commons.concurrency.ThreadPoolService |  | ||||||
| import fr.free.nrw.commons.contributions.ContributionDao |  | ||||||
| import fr.free.nrw.commons.data.DBOpenHelper |  | ||||||
| import fr.free.nrw.commons.di.ApplicationlessInjection |  | ||||||
| import fr.free.nrw.commons.kvstore.JsonKvStore |  | ||||||
| import fr.free.nrw.commons.language.AppLanguageLookUpTable |  | ||||||
| import fr.free.nrw.commons.logging.FileLoggingTree |  | ||||||
| import fr.free.nrw.commons.logging.LogUtils |  | ||||||
| import fr.free.nrw.commons.media.CustomOkHttpNetworkFetcher |  | ||||||
| import fr.free.nrw.commons.settings.Prefs |  | ||||||
| import fr.free.nrw.commons.upload.FileUtils |  | ||||||
| import fr.free.nrw.commons.utils.ConfigUtils.getVersionNameWithSha |  | ||||||
| import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour |  | ||||||
| import fr.free.nrw.commons.wikidata.cookies.CommonsCookieJar |  | ||||||
| import io.reactivex.Completable |  | ||||||
| import io.reactivex.android.schedulers.AndroidSchedulers |  | ||||||
| import io.reactivex.internal.functions.Functions |  | ||||||
| import io.reactivex.plugins.RxJavaPlugins |  | ||||||
| import io.reactivex.schedulers.Schedulers |  | ||||||
| import org.acra.ACRA.init |  | ||||||
| import org.acra.ReportField |  | ||||||
| import org.acra.annotation.AcraCore |  | ||||||
| import org.acra.annotation.AcraDialog |  | ||||||
| import org.acra.annotation.AcraMailSender |  | ||||||
| import org.acra.data.StringFormat |  | ||||||
| import timber.log.Timber |  | ||||||
| import timber.log.Timber.DebugTree |  | ||||||
| import java.io.File |  | ||||||
| import javax.inject.Inject |  | ||||||
| import javax.inject.Named |  | ||||||
| 
 |  | ||||||
| @AcraCore( |  | ||||||
|     buildConfigClass = BuildConfig::class, |  | ||||||
|     resReportSendSuccessToast = R.string.crash_dialog_ok_toast, |  | ||||||
|     reportFormat = StringFormat.KEY_VALUE_LIST, |  | ||||||
|     reportContent = [ReportField.USER_COMMENT, ReportField.APP_VERSION_CODE, ReportField.APP_VERSION_NAME, ReportField.ANDROID_VERSION, ReportField.PHONE_MODEL, ReportField.STACK_TRACE] |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| @AcraMailSender(mailTo = "commons-app-android-private@googlegroups.com", reportAsFile = false) |  | ||||||
| 
 |  | ||||||
| @AcraDialog( |  | ||||||
|     resTheme = R.style.Theme_AppCompat_Dialog, |  | ||||||
|     resText = R.string.crash_dialog_text, |  | ||||||
|     resTitle = R.string.crash_dialog_title, |  | ||||||
|     resCommentPrompt = R.string.crash_dialog_comment_prompt |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| class CommonsApplication : MultiDexApplication() { |  | ||||||
| 
 |  | ||||||
|     @Inject |  | ||||||
|     lateinit var sessionManager: SessionManager |  | ||||||
| 
 |  | ||||||
|     @Inject |  | ||||||
|     lateinit var dbOpenHelper: DBOpenHelper |  | ||||||
| 
 |  | ||||||
|     @Inject |  | ||||||
|     @field:Named("default_preferences") |  | ||||||
|     lateinit var defaultPrefs: JsonKvStore |  | ||||||
| 
 |  | ||||||
|     @Inject |  | ||||||
|     lateinit var cookieJar: CommonsCookieJar |  | ||||||
| 
 |  | ||||||
|     @Inject |  | ||||||
|     lateinit var customOkHttpNetworkFetcher: CustomOkHttpNetworkFetcher |  | ||||||
| 
 |  | ||||||
|     var languageLookUpTable: AppLanguageLookUpTable? = null |  | ||||||
|         private set |  | ||||||
| 
 |  | ||||||
|     @Inject |  | ||||||
|     lateinit var contributionDao: ContributionDao |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Used to declare and initialize various components and dependencies |  | ||||||
|      */ |  | ||||||
|     override fun onCreate() { |  | ||||||
|         super.onCreate() |  | ||||||
| 
 |  | ||||||
|         instance = this |  | ||||||
|         init(this) |  | ||||||
| 
 |  | ||||||
|         ApplicationlessInjection |  | ||||||
|             .getInstance(this) |  | ||||||
|             .commonsApplicationComponent |  | ||||||
|             .inject(this) |  | ||||||
| 
 |  | ||||||
|         initTimber() |  | ||||||
| 
 |  | ||||||
|         if (!defaultPrefs.getBoolean("has_user_manually_removed_location")) { |  | ||||||
|             var defaultExifTagsSet = defaultPrefs.getStringSet(Prefs.MANAGED_EXIF_TAGS) |  | ||||||
|             if (null == defaultExifTagsSet) { |  | ||||||
|                 defaultExifTagsSet = HashSet() |  | ||||||
|             } |  | ||||||
|             defaultExifTagsSet.add(getString(R.string.exif_tag_location)) |  | ||||||
|             defaultPrefs.putStringSet(Prefs.MANAGED_EXIF_TAGS, defaultExifTagsSet) |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         //        Set DownsampleEnabled to True to downsample the image in case it's heavy |  | ||||||
|         val config = ImagePipelineConfig.newBuilder(this) |  | ||||||
|             .setNetworkFetcher(customOkHttpNetworkFetcher) |  | ||||||
|             .setDownsampleEnabled(true) |  | ||||||
|             .build() |  | ||||||
|         try { |  | ||||||
|             Fresco.initialize(this, config) |  | ||||||
|         } catch (e: Exception) { |  | ||||||
|             Timber.e(e) |  | ||||||
|             // TODO: Remove when we're able to initialize Fresco in test builds. |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         createNotificationChannel(this) |  | ||||||
| 
 |  | ||||||
|         languageLookUpTable = AppLanguageLookUpTable(this) |  | ||||||
| 
 |  | ||||||
|         // This handler will catch exceptions thrown from Observables after they are disposed, |  | ||||||
|         // or from Observables that are (deliberately or not) missing an onError handler. |  | ||||||
|         RxJavaPlugins.setErrorHandler(Functions.emptyConsumer()) |  | ||||||
| 
 |  | ||||||
|         // Fire progress callbacks for every 3% of uploaded content |  | ||||||
|         System.setProperty("in.yuvi.http.fluent.PROGRESS_TRIGGER_THRESHOLD", "3.0") |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Plants debug and file logging tree. Timber lets you plant your own logging trees. |  | ||||||
|      */ |  | ||||||
|     private fun initTimber() { |  | ||||||
|         val isBeta = isBetaFlavour |  | ||||||
|         val logFileName = |  | ||||||
|             if (isBeta) "CommonsBetaAppLogs" else "CommonsAppLogs" |  | ||||||
|         val logDirectory = LogUtils.getLogDirectory() |  | ||||||
|         //Delete stale logs if they have exceeded the specified size |  | ||||||
|         deleteStaleLogs(logFileName, logDirectory) |  | ||||||
| 
 |  | ||||||
|         val tree = FileLoggingTree( |  | ||||||
|             Log.VERBOSE, |  | ||||||
|             logFileName, |  | ||||||
|             logDirectory, |  | ||||||
|             1000, |  | ||||||
|             fileLoggingThreadPool |  | ||||||
|         ) |  | ||||||
| 
 |  | ||||||
|         Timber.plant(tree) |  | ||||||
|         Timber.plant(DebugTree()) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Deletes the logs zip file at the specified directory and file locations specified in the |  | ||||||
|      * params |  | ||||||
|      * |  | ||||||
|      * @param logFileName |  | ||||||
|      * @param logDirectory |  | ||||||
|      */ |  | ||||||
|     private fun deleteStaleLogs(logFileName: String, logDirectory: String) { |  | ||||||
|         try { |  | ||||||
|             val file = File("$logDirectory/zip/$logFileName.zip") |  | ||||||
|             if (file.exists() && file.totalSpace > 1000000) { // In Kbs |  | ||||||
|                 file.delete() |  | ||||||
|             } |  | ||||||
|         } catch (e: Exception) { |  | ||||||
|             Timber.e(e) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private val fileLoggingThreadPool: ThreadPoolService |  | ||||||
|         get() = ThreadPoolService.Builder("file-logging-thread") |  | ||||||
|             .setPriority(Process.THREAD_PRIORITY_LOWEST) |  | ||||||
|             .setPoolSize(1) |  | ||||||
|             .setExceptionHandler(BackgroundPoolExceptionHandler()) |  | ||||||
|             .build() |  | ||||||
| 
 |  | ||||||
|     val userAgent: String |  | ||||||
|         get() = ("Commons/" + this.getVersionNameWithSha() |  | ||||||
|                 + " (https://mediawiki.org/wiki/Apps/Commons) Android/" + Build.VERSION.RELEASE) |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * clears data of current application |  | ||||||
|      * |  | ||||||
|      * @param context        Application context |  | ||||||
|      * @param logoutListener Implementation of interface LogoutListener |  | ||||||
|      */ |  | ||||||
|     @SuppressLint("CheckResult") |  | ||||||
|     fun clearApplicationData(context: Context, logoutListener: LogoutListener) { |  | ||||||
|         val cacheDirectory = context.cacheDir |  | ||||||
|         val applicationDirectory = File(cacheDirectory.parent) |  | ||||||
|         if (applicationDirectory.exists()) { |  | ||||||
|             val fileNames = applicationDirectory.list() |  | ||||||
|             for (fileName in fileNames) { |  | ||||||
|                 if (fileName != "lib") { |  | ||||||
|                     FileUtils.deleteFile(File(applicationDirectory, fileName)) |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         sessionManager.logout() |  | ||||||
|             .andThen(Completable.fromAction { cookieJar.clear() }) |  | ||||||
|             .andThen(Completable.fromAction { |  | ||||||
|                 Timber.d("All accounts have been removed") |  | ||||||
|                 clearImageCache() |  | ||||||
|                 //TODO: fix preference manager |  | ||||||
|                 defaultPrefs.clearAll() |  | ||||||
|                 defaultPrefs.putBoolean("firstrun", false) |  | ||||||
|                 updateAllDatabases() |  | ||||||
|             }) |  | ||||||
|             .subscribeOn(Schedulers.io()) |  | ||||||
|             .observeOn(AndroidSchedulers.mainThread()) |  | ||||||
|             .subscribe({ logoutListener.onLogoutComplete() }, { t: Throwable? -> Timber.e(t) }) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Clear all images cache held by Fresco |  | ||||||
|      */ |  | ||||||
|     private fun clearImageCache() { |  | ||||||
|         val imagePipeline = Fresco.getImagePipeline() |  | ||||||
|         imagePipeline.clearCaches() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Deletes all tables and re-creates them. |  | ||||||
|      */ |  | ||||||
|     private fun updateAllDatabases() { |  | ||||||
|         dbOpenHelper.readableDatabase.close() |  | ||||||
|         val db = dbOpenHelper.writableDatabase |  | ||||||
| 
 |  | ||||||
|         CategoryDao.Table.onDelete(db) |  | ||||||
|         dbOpenHelper.deleteTable( |  | ||||||
|             db, |  | ||||||
|             DBOpenHelper.CONTRIBUTIONS_TABLE |  | ||||||
|         ) //Delete the contributions table in the existing db on older versions |  | ||||||
| 
 |  | ||||||
|         dbOpenHelper.deleteTable( |  | ||||||
|             db, |  | ||||||
|             DBOpenHelper.BOOKMARKS_LOCATIONS |  | ||||||
|         ) |  | ||||||
| 
 |  | ||||||
|         try { |  | ||||||
|             contributionDao.deleteAll() |  | ||||||
|         } catch (e: SQLiteException) { |  | ||||||
|             Timber.e(e) |  | ||||||
|         } |  | ||||||
|         BookmarksTable.onDelete(db) |  | ||||||
|         BookmarkItemsTable.onDelete(db) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Interface used to get log-out events |  | ||||||
|      */ |  | ||||||
|     interface LogoutListener { |  | ||||||
|         fun onLogoutComplete() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * This listener is responsible for handling post-logout actions, specifically invoking the LoginActivity |  | ||||||
|      * with relevant intent parameters. It does not perform the actual logout operation. |  | ||||||
|      */ |  | ||||||
|     open class BaseLogoutListener : LogoutListener { |  | ||||||
|         var ctx: Context |  | ||||||
|         var loginMessage: String? = null |  | ||||||
|         var userName: String? = null |  | ||||||
| 
 |  | ||||||
|         /** |  | ||||||
|          * Constructor for BaseLogoutListener. |  | ||||||
|          * |  | ||||||
|          * @param ctx Application context |  | ||||||
|          */ |  | ||||||
|         constructor(ctx: Context) { |  | ||||||
|             this.ctx = ctx |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         /** |  | ||||||
|          * Constructor for BaseLogoutListener |  | ||||||
|          * |  | ||||||
|          * @param ctx           The application context, used for invoking the LoginActivity and passing relevant intent parameters as part of the post-logout process. |  | ||||||
|          * @param loginMessage  Message to be displayed on the login page |  | ||||||
|          * @param loginUsername Username to be pre-filled on the login page |  | ||||||
|          */ |  | ||||||
|         constructor( |  | ||||||
|             ctx: Context, loginMessage: String?, |  | ||||||
|             loginUsername: String? |  | ||||||
|         ) { |  | ||||||
|             this.ctx = ctx |  | ||||||
|             this.loginMessage = loginMessage |  | ||||||
|             this.userName = loginUsername |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         override fun onLogoutComplete() { |  | ||||||
|             Timber.d("Logout complete callback received.") |  | ||||||
|             val loginIntent = Intent(ctx, LoginActivity::class.java) |  | ||||||
|             loginIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) |  | ||||||
|                 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) |  | ||||||
| 
 |  | ||||||
|             if (loginMessage != null) { |  | ||||||
|                 loginIntent.putExtra(LOGIN_MESSAGE_INTENT_KEY, loginMessage) |  | ||||||
|             } |  | ||||||
|             if (userName != null) { |  | ||||||
|                 loginIntent.putExtra(LOGIN_USERNAME_INTENT_KEY, userName) |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             ctx.startActivity(loginIntent) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * This class is an extension of BaseLogoutListener, providing additional functionality or customization |  | ||||||
|      * for the logout process. It includes specific actions to be taken during logout, such as handling redirection to the login screen. |  | ||||||
|      */ |  | ||||||
|     class ActivityLogoutListener : BaseLogoutListener { |  | ||||||
|         var activity: Activity |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|         /** |  | ||||||
|          * Constructor for ActivityLogoutListener. |  | ||||||
|          * |  | ||||||
|          * @param activity The activity context from which the logout is initiated. Used to perform actions such as finishing the activity. |  | ||||||
|          * @param ctx           The application context, used for invoking the LoginActivity and passing relevant intent parameters as part of the post-logout process. |  | ||||||
|          */ |  | ||||||
|         constructor(activity: Activity, ctx: Context) : super(ctx) { |  | ||||||
|             this.activity = activity |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         /** |  | ||||||
|          * Constructor for ActivityLogoutListener with additional parameters for the login screen. |  | ||||||
|          * |  | ||||||
|          * @param activity      The activity context from which the logout is initiated. Used to perform actions such as finishing the activity. |  | ||||||
|          * @param ctx           The application context, used for invoking the LoginActivity and passing relevant intent parameters as part of the post-logout process. |  | ||||||
|          * @param loginMessage  Message to be displayed on the login page after logout. |  | ||||||
|          * @param loginUsername Username to be pre-filled on the login page after logout. |  | ||||||
|          */ |  | ||||||
|         constructor( |  | ||||||
|             activity: Activity, ctx: Context?, |  | ||||||
|             loginMessage: String?, loginUsername: String? |  | ||||||
|         ) : super(activity, loginMessage, loginUsername) { |  | ||||||
|             this.activity = activity |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         override fun onLogoutComplete() { |  | ||||||
|             super.onLogoutComplete() |  | ||||||
|             activity.finish() |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     companion object { |  | ||||||
| 
 |  | ||||||
|         const val LOGIN_MESSAGE_INTENT_KEY: String = "loginMessage" |  | ||||||
|         const val LOGIN_USERNAME_INTENT_KEY: String = "loginUsername" |  | ||||||
| 
 |  | ||||||
|         const val IS_LIMITED_CONNECTION_MODE_ENABLED: String = "is_limited_connection_mode_enabled" |  | ||||||
| 
 |  | ||||||
|         /** |  | ||||||
|          * Constants begin |  | ||||||
|          */ |  | ||||||
|         const val OPEN_APPLICATION_DETAIL_SETTINGS: Int = 1001 |  | ||||||
| 
 |  | ||||||
|         const val DEFAULT_EDIT_SUMMARY: String = "Uploaded using [[COM:MOA|Commons Mobile App]]" |  | ||||||
| 
 |  | ||||||
|         const val FEEDBACK_EMAIL: String = "commons-app-android@googlegroups.com" |  | ||||||
| 
 |  | ||||||
|         const val FEEDBACK_EMAIL_SUBJECT: String = "Commons Android App Feedback" |  | ||||||
| 
 |  | ||||||
|         const val REPORT_EMAIL: String = "commons-app-android-private@googlegroups.com" |  | ||||||
| 
 |  | ||||||
|         const val REPORT_EMAIL_SUBJECT: String = "Report a violation" |  | ||||||
| 
 |  | ||||||
|         const val NOTIFICATION_CHANNEL_ID_ALL: String = "CommonsNotificationAll" |  | ||||||
| 
 |  | ||||||
|         const val FEEDBACK_EMAIL_TEMPLATE_HEADER: String = "-- Technical information --" |  | ||||||
| 
 |  | ||||||
|         /** |  | ||||||
|          * Constants End |  | ||||||
|          */ |  | ||||||
| 
 |  | ||||||
|         @JvmStatic |  | ||||||
|         lateinit var instance: CommonsApplication |  | ||||||
|             private set |  | ||||||
| 
 |  | ||||||
|         @JvmField |  | ||||||
|         var isPaused: Boolean = false |  | ||||||
| 
 |  | ||||||
|         @JvmStatic |  | ||||||
|         fun createNotificationChannel(context: Context) { |  | ||||||
|             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { |  | ||||||
|                 val manager = context |  | ||||||
|                     .getSystemService(NOTIFICATION_SERVICE) as NotificationManager |  | ||||||
|                 var channel = manager |  | ||||||
|                     .getNotificationChannel(NOTIFICATION_CHANNEL_ID_ALL) |  | ||||||
|                 if (channel == null) { |  | ||||||
|                     channel = NotificationChannel( |  | ||||||
|                         NOTIFICATION_CHANNEL_ID_ALL, |  | ||||||
|                         context.getString(R.string.notifications_channel_name_all), |  | ||||||
|                         NotificationManager.IMPORTANCE_DEFAULT |  | ||||||
|                     ) |  | ||||||
|                     manager.createNotificationChannel(channel) |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
							
								
								
									
										79
									
								
								app/src/main/java/fr/free/nrw/commons/License.java
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								app/src/main/java/fr/free/nrw/commons/License.java
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,79 @@ | ||||||
|  | package fr.free.nrw.commons; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.Nullable; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * represents Licence object | ||||||
|  |  */ | ||||||
|  | public class License { | ||||||
|  |     private String key; | ||||||
|  |     private String template; | ||||||
|  |     private String url; | ||||||
|  |     private String name; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Constructs a new instance of License. | ||||||
|  |      * | ||||||
|  |      * @param key       license key | ||||||
|  |      * @param template  license template | ||||||
|  |      * @param url       license URL | ||||||
|  |      * @param name      licence name | ||||||
|  |      * | ||||||
|  |      * @throws RuntimeException if License.key or Licence.template is null | ||||||
|  |      */ | ||||||
|  |     public License(String key, String template, String url, String name) { | ||||||
|  |         if (key == null) { | ||||||
|  |             throw new RuntimeException("License.key must not be null"); | ||||||
|  |         } | ||||||
|  |         if (template == null) { | ||||||
|  |             throw new RuntimeException("License.template must not be null"); | ||||||
|  |         } | ||||||
|  |         this.key = key; | ||||||
|  |         this.template = template; | ||||||
|  |         this.url = url; | ||||||
|  |         this.name = name; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Gets the license key. | ||||||
|  |      * @return license key as a String. | ||||||
|  |      */ | ||||||
|  |     public String getKey() { | ||||||
|  |         return key; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Gets the license template. | ||||||
|  |      * @return license template as a String. | ||||||
|  |      */ | ||||||
|  |     public String getTemplate() { | ||||||
|  |         return template; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Gets the license name. If name is null, return license key. | ||||||
|  |      * @return license name as string. if name null, license key as String | ||||||
|  |      */ | ||||||
|  |     public String getName() { | ||||||
|  |         if (name == null) { | ||||||
|  |             // hack | ||||||
|  |             return getKey(); | ||||||
|  |         } else { | ||||||
|  |             return name; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Gets the license URL | ||||||
|  |      * | ||||||
|  |      * @param language license language | ||||||
|  |      * @return URL | ||||||
|  |      */ | ||||||
|  |     public @Nullable String getUrl(String language) { | ||||||
|  |         if (url == null) { | ||||||
|  |             return null; | ||||||
|  |         } else { | ||||||
|  |             return url.replace("$lang", language); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,65 @@ | ||||||
|  | package fr.free.nrw.commons.LocationPicker; | ||||||
|  | 
 | ||||||
|  | import android.app.Activity; | ||||||
|  | import android.content.Intent; | ||||||
|  | import com.mapbox.mapboxsdk.camera.CameraPosition; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Helper class for starting the activity | ||||||
|  |  */ | ||||||
|  | public final class LocationPicker { | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Getting camera position from the intent using constants | ||||||
|  |      * | ||||||
|  |      * @param data intent | ||||||
|  |      * @return CameraPosition | ||||||
|  |      */ | ||||||
|  |     public static CameraPosition getCameraPosition(final Intent data) { | ||||||
|  |         return data.getParcelableExtra(LocationPickerConstants.MAP_CAMERA_POSITION); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static class IntentBuilder { | ||||||
|  | 
 | ||||||
|  |         private final Intent intent; | ||||||
|  | 
 | ||||||
|  |         /** | ||||||
|  |          * Creates a new builder that creates an intent to launch the place picker activity. | ||||||
|  |          */ | ||||||
|  |         public IntentBuilder() { | ||||||
|  |             intent = new Intent(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         /** | ||||||
|  |          * Gets and puts location in intent | ||||||
|  |          * @param position CameraPosition | ||||||
|  |          * @return LocationPicker.IntentBuilder | ||||||
|  |          */ | ||||||
|  |         public LocationPicker.IntentBuilder defaultLocation( | ||||||
|  |             final CameraPosition position) { | ||||||
|  |           intent.putExtra(LocationPickerConstants.MAP_CAMERA_POSITION, position); | ||||||
|  |           return this; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         /** | ||||||
|  |          * Gets and puts activity name in intent | ||||||
|  |          * @param activity activity key | ||||||
|  |          * @return LocationPicker.IntentBuilder | ||||||
|  |          */ | ||||||
|  |         public LocationPicker.IntentBuilder activityKey( | ||||||
|  |             final String activity) { | ||||||
|  |           intent.putExtra(LocationPickerConstants.ACTIVITY_KEY, activity); | ||||||
|  |           return this; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         /** | ||||||
|  |          * Gets and sets the activity | ||||||
|  |          * @param activity Activity | ||||||
|  |          * @return Intent | ||||||
|  |          */ | ||||||
|  |        public Intent build(final Activity activity) { | ||||||
|  |           intent.setClass(activity, LocationPickerActivity.class); | ||||||
|  |           return intent; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,450 @@ | ||||||
|  | package fr.free.nrw.commons.LocationPicker; | ||||||
|  | 
 | ||||||
|  | import static com.mapbox.mapboxsdk.style.layers.Property.NONE; | ||||||
|  | import static com.mapbox.mapboxsdk.style.layers.Property.VISIBLE; | ||||||
|  | import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconAllowOverlap; | ||||||
|  | import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconIgnorePlacement; | ||||||
|  | import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconImage; | ||||||
|  | import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.visibility; | ||||||
|  | import static fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.LAST_LOCATION; | ||||||
|  | import static fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.LAST_ZOOM; | ||||||
|  | 
 | ||||||
|  | import android.content.Intent; | ||||||
|  | import android.graphics.BitmapFactory; | ||||||
|  | import android.os.Bundle; | ||||||
|  | import android.text.Html; | ||||||
|  | import android.text.method.LinkMovementMethod; | ||||||
|  | import android.view.View; | ||||||
|  | import android.view.Window; | ||||||
|  | import android.view.animation.OvershootInterpolator; | ||||||
|  | import android.widget.Button; | ||||||
|  | import android.widget.ImageView; | ||||||
|  | import android.widget.TextView; | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.annotation.Nullable; | ||||||
|  | import androidx.appcompat.app.ActionBar; | ||||||
|  | import androidx.appcompat.app.AppCompatActivity; | ||||||
|  | import androidx.appcompat.widget.AppCompatTextView; | ||||||
|  | import androidx.constraintlayout.widget.ConstraintLayout; | ||||||
|  | import androidx.lifecycle.Observer; | ||||||
|  | import androidx.lifecycle.ViewModelProvider; | ||||||
|  | import com.google.android.material.floatingactionbutton.FloatingActionButton; | ||||||
|  | import com.mapbox.android.core.permissions.PermissionsManager; | ||||||
|  | import com.mapbox.geojson.Point; | ||||||
|  | import com.mapbox.mapboxsdk.camera.CameraPosition; | ||||||
|  | import com.mapbox.mapboxsdk.camera.CameraPosition.Builder; | ||||||
|  | import com.mapbox.mapboxsdk.camera.CameraUpdateFactory; | ||||||
|  | import com.mapbox.mapboxsdk.geometry.LatLng; | ||||||
|  | import com.mapbox.mapboxsdk.location.LocationComponent; | ||||||
|  | import com.mapbox.mapboxsdk.location.LocationComponentActivationOptions; | ||||||
|  | import com.mapbox.mapboxsdk.location.modes.CameraMode; | ||||||
|  | import com.mapbox.mapboxsdk.location.modes.RenderMode; | ||||||
|  | import com.mapbox.mapboxsdk.maps.MapView; | ||||||
|  | import com.mapbox.mapboxsdk.maps.MapboxMap; | ||||||
|  | import com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraIdleListener; | ||||||
|  | import com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraMoveStartedListener; | ||||||
|  | import com.mapbox.mapboxsdk.maps.OnMapReadyCallback; | ||||||
|  | import com.mapbox.mapboxsdk.maps.Style; | ||||||
|  | import com.mapbox.mapboxsdk.maps.UiSettings; | ||||||
|  | import com.mapbox.mapboxsdk.style.layers.Layer; | ||||||
|  | import com.mapbox.mapboxsdk.style.layers.SymbolLayer; | ||||||
|  | import com.mapbox.mapboxsdk.style.sources.GeoJsonSource; | ||||||
|  | import fr.free.nrw.commons.R; | ||||||
|  | import fr.free.nrw.commons.Utils; | ||||||
|  | import fr.free.nrw.commons.kvstore.JsonKvStore; | ||||||
|  | import fr.free.nrw.commons.theme.BaseActivity; | ||||||
|  | import javax.inject.Inject; | ||||||
|  | import javax.inject.Named; | ||||||
|  | import org.jetbrains.annotations.NotNull; | ||||||
|  | import timber.log.Timber; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Helps to pick location and return the result with an intent | ||||||
|  |  */ | ||||||
|  | public class LocationPickerActivity extends BaseActivity implements OnMapReadyCallback, | ||||||
|  |     OnCameraMoveStartedListener, OnCameraIdleListener, Observer<CameraPosition> { | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * DROPPED_MARKER_LAYER_ID : id for layer | ||||||
|  |      */ | ||||||
|  |     private static final String DROPPED_MARKER_LAYER_ID = "DROPPED_MARKER_LAYER_ID"; | ||||||
|  |     /** | ||||||
|  |      * cameraPosition : position of picker | ||||||
|  |      */ | ||||||
|  |     private CameraPosition cameraPosition; | ||||||
|  |     /** | ||||||
|  |      * markerImage : picker image | ||||||
|  |      */ | ||||||
|  |     private ImageView markerImage; | ||||||
|  |     /** | ||||||
|  |      * mapboxMap : map | ||||||
|  |      */ | ||||||
|  |     private MapboxMap mapboxMap; | ||||||
|  |     /** | ||||||
|  |      * mapView : view of the map | ||||||
|  |      */ | ||||||
|  |     private MapView mapView; | ||||||
|  |     /** | ||||||
|  |      * tvAttribution : credit | ||||||
|  |      */ | ||||||
|  |     private AppCompatTextView tvAttribution; | ||||||
|  |     /** | ||||||
|  |      * activity : activity key | ||||||
|  |      */ | ||||||
|  |     private String activity; | ||||||
|  |     /** | ||||||
|  |      * modifyLocationButton : button for start editing location | ||||||
|  |      */ | ||||||
|  |     Button modifyLocationButton; | ||||||
|  |     /** | ||||||
|  |      * showInMapButton : button for showing in map | ||||||
|  |      */ | ||||||
|  |     TextView showInMapButton; | ||||||
|  |     /** | ||||||
|  |      * placeSelectedButton : fab for selecting location | ||||||
|  |      */ | ||||||
|  |     FloatingActionButton placeSelectedButton; | ||||||
|  |     /** | ||||||
|  |      * droppedMarkerLayer : Layer for static screen | ||||||
|  |      */ | ||||||
|  |     private Layer droppedMarkerLayer; | ||||||
|  |     /** | ||||||
|  |      * shadow : imageview of shadow | ||||||
|  |      */ | ||||||
|  |     private ImageView shadow; | ||||||
|  |     /** | ||||||
|  |      * largeToolbarText : textView of shadow | ||||||
|  |      */ | ||||||
|  |     private TextView largeToolbarText; | ||||||
|  |     /** | ||||||
|  |      * smallToolbarText : textView of shadow | ||||||
|  |      */ | ||||||
|  |     private TextView smallToolbarText; | ||||||
|  |     /** | ||||||
|  |      * applicationKvStore : for storing values | ||||||
|  |      */ | ||||||
|  |     @Inject | ||||||
|  |     @Named("default_preferences") | ||||||
|  |     public | ||||||
|  |     JsonKvStore applicationKvStore; | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     protected void onCreate(@Nullable final Bundle savedInstanceState) { | ||||||
|  |         super.onCreate(savedInstanceState); | ||||||
|  | 
 | ||||||
|  |         getWindow().requestFeature(Window.FEATURE_ACTION_BAR); | ||||||
|  |         final ActionBar actionBar = getSupportActionBar(); | ||||||
|  |         if (actionBar != null) { | ||||||
|  |             actionBar.hide(); | ||||||
|  |         } | ||||||
|  |         setContentView(R.layout.activity_location_picker); | ||||||
|  | 
 | ||||||
|  |         if (savedInstanceState == null) { | ||||||
|  |             cameraPosition = getIntent() | ||||||
|  |                 .getParcelableExtra(LocationPickerConstants.MAP_CAMERA_POSITION); | ||||||
|  |             activity = getIntent().getStringExtra(LocationPickerConstants.ACTIVITY_KEY); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         final LocationPickerViewModel viewModel = new ViewModelProvider(this) | ||||||
|  |             .get(LocationPickerViewModel.class); | ||||||
|  |         viewModel.getResult().observe(this, this); | ||||||
|  | 
 | ||||||
|  |         bindViews(); | ||||||
|  |         addBackButtonListener(); | ||||||
|  |         addPlaceSelectedButton(); | ||||||
|  |         addCredits(); | ||||||
|  |         getToolbarUI(); | ||||||
|  | 
 | ||||||
|  |         if (activity.equals("UploadActivity")) { | ||||||
|  |             placeSelectedButton.setVisibility(View.GONE); | ||||||
|  |             modifyLocationButton.setVisibility(View.VISIBLE); | ||||||
|  |             showInMapButton.setVisibility(View.VISIBLE); | ||||||
|  |             largeToolbarText.setText(getResources().getString(R.string.image_location)); | ||||||
|  |             smallToolbarText.setText(getResources(). | ||||||
|  |                 getString(R.string.check_whether_location_is_correct)); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         mapView.onCreate(savedInstanceState); | ||||||
|  |         mapView.getMapAsync(this); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * For showing credits | ||||||
|  |      */ | ||||||
|  |     private void addCredits() { | ||||||
|  |         tvAttribution.setText(Html.fromHtml(getString(R.string.map_attribution))); | ||||||
|  |         tvAttribution.setMovementMethod(LinkMovementMethod.getInstance()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Clicking back button destroy locationPickerActivity | ||||||
|  |      */ | ||||||
|  |     private void addBackButtonListener() { | ||||||
|  |         final ImageView backButton = findViewById(R.id.mapbox_place_picker_toolbar_back_button); | ||||||
|  |         backButton.setOnClickListener(view -> finish()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Binds mapView and location picker icon | ||||||
|  |      */ | ||||||
|  |     private void bindViews() { | ||||||
|  |         mapView = findViewById(R.id.map_view); | ||||||
|  |         markerImage = findViewById(R.id.location_picker_image_view_marker); | ||||||
|  |         tvAttribution = findViewById(R.id.tv_attribution); | ||||||
|  |         modifyLocationButton = findViewById(R.id.modify_location); | ||||||
|  |         showInMapButton = findViewById(R.id.show_in_map); | ||||||
|  |         showInMapButton.setText(getResources().getString(R.string.show_in_map_app).toUpperCase()); | ||||||
|  |         shadow = findViewById(R.id.location_picker_image_view_shadow); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Binds the listeners | ||||||
|  |      */ | ||||||
|  |     private void bindListeners() { | ||||||
|  |         mapboxMap.addOnCameraMoveStartedListener( | ||||||
|  |             this); | ||||||
|  |         mapboxMap.addOnCameraIdleListener( | ||||||
|  |             this); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Gets toolbar color | ||||||
|  |      */ | ||||||
|  |     private void getToolbarUI() { | ||||||
|  |         final ConstraintLayout toolbar = findViewById(R.id.location_picker_toolbar); | ||||||
|  |         largeToolbarText = findViewById(R.id.location_picker_toolbar_primary_text_view); | ||||||
|  |         smallToolbarText = findViewById(R.id.location_picker_toolbar_secondary_text_view); | ||||||
|  |         toolbar.setBackgroundColor(getResources().getColor(R.color.primaryColor)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Takes action when map is ready to show | ||||||
|  |      * @param mapboxMap map | ||||||
|  |      */ | ||||||
|  |     @Override | ||||||
|  |     public void onMapReady(final MapboxMap mapboxMap) { | ||||||
|  |         this.mapboxMap = mapboxMap; | ||||||
|  |         mapboxMap.setStyle(Style.MAPBOX_STREETS, this::onStyleLoaded); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Initializes dropped marker and layer | ||||||
|  |      * Handles camera position based on options | ||||||
|  |      * Enables location components | ||||||
|  |      * | ||||||
|  |      * @param style style | ||||||
|  |      */ | ||||||
|  |     private void onStyleLoaded(final Style style) { | ||||||
|  |         if (modifyLocationButton.getVisibility() == View.VISIBLE) { | ||||||
|  |             initDroppedMarker(style); | ||||||
|  |             adjustCameraBasedOnOptions(); | ||||||
|  |             enableLocationComponent(style); | ||||||
|  |             if (style.getLayer(DROPPED_MARKER_LAYER_ID) != null) { | ||||||
|  |                 final GeoJsonSource source = style.getSourceAs("dropped-marker-source-id"); | ||||||
|  |                 if (source != null) { | ||||||
|  |                     source.setGeoJson(Point.fromLngLat(cameraPosition.target.getLongitude(), | ||||||
|  |                         cameraPosition.target.getLatitude())); | ||||||
|  |                 } | ||||||
|  |                 droppedMarkerLayer = style.getLayer(DROPPED_MARKER_LAYER_ID); | ||||||
|  |                 if (droppedMarkerLayer != null) { | ||||||
|  |                     droppedMarkerLayer.setProperties(visibility(VISIBLE)); | ||||||
|  |                     markerImage.setVisibility(View.GONE); | ||||||
|  |                     shadow.setVisibility(View.GONE); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             adjustCameraBasedOnOptions(); | ||||||
|  |             enableLocationComponent(style); | ||||||
|  |             bindListeners(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         modifyLocationButton.setOnClickListener(v -> onClickModifyLocation()); | ||||||
|  |         showInMapButton.setOnClickListener(v -> showInMap()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Handles onclick event of modifyLocationButton | ||||||
|  |      */ | ||||||
|  |     private void onClickModifyLocation() { | ||||||
|  |         placeSelectedButton.setVisibility(View.VISIBLE); | ||||||
|  |         modifyLocationButton.setVisibility(View.GONE); | ||||||
|  |         showInMapButton.setVisibility(View.GONE); | ||||||
|  |         droppedMarkerLayer.setProperties(visibility(NONE)); | ||||||
|  |         markerImage.setVisibility(View.VISIBLE); | ||||||
|  |         shadow.setVisibility(View.VISIBLE); | ||||||
|  |         largeToolbarText.setText(getResources().getString(R.string.choose_a_location)); | ||||||
|  |         smallToolbarText.setText(getResources().getString(R.string.pan_and_zoom_to_adjust)); | ||||||
|  |         bindListeners(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Show the location in map app | ||||||
|  |      */ | ||||||
|  |     public void showInMap(){ | ||||||
|  |         Utils.handleGeoCoordinates(this, | ||||||
|  |             new fr.free.nrw.commons.location.LatLng(cameraPosition.target.getLatitude(), | ||||||
|  |                 cameraPosition.target.getLongitude(), 0.0f)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Initialize Dropped Marker and layer without showing | ||||||
|  |      * @param loadedMapStyle style | ||||||
|  |      */ | ||||||
|  |     private void initDroppedMarker(@NonNull final Style loadedMapStyle) { | ||||||
|  |         // Add the marker image to map | ||||||
|  |         loadedMapStyle.addImage("dropped-icon-image", BitmapFactory.decodeResource( | ||||||
|  |             getResources(), R.drawable.map_default_map_marker)); | ||||||
|  |         loadedMapStyle.addSource(new GeoJsonSource("dropped-marker-source-id")); | ||||||
|  |         loadedMapStyle.addLayer(new SymbolLayer(DROPPED_MARKER_LAYER_ID, | ||||||
|  |             "dropped-marker-source-id").withProperties( | ||||||
|  |             iconImage("dropped-icon-image"), | ||||||
|  |             visibility(NONE), | ||||||
|  |             iconAllowOverlap(true), | ||||||
|  |             iconIgnorePlacement(true) | ||||||
|  |         )); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * move the location to the current media coordinates | ||||||
|  |      */ | ||||||
|  |     private void adjustCameraBasedOnOptions() { | ||||||
|  |         mapboxMap.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Enables location components | ||||||
|  |      * @param loadedMapStyle Style | ||||||
|  |      */ | ||||||
|  |     @SuppressWarnings( {"MissingPermission"}) | ||||||
|  |     private void enableLocationComponent(@NonNull final Style loadedMapStyle) { | ||||||
|  |         final UiSettings uiSettings = mapboxMap.getUiSettings(); | ||||||
|  |         uiSettings.setAttributionEnabled(false); | ||||||
|  | 
 | ||||||
|  |         // Check if permissions are enabled and if not request | ||||||
|  |         if (PermissionsManager.areLocationPermissionsGranted(this)) { | ||||||
|  | 
 | ||||||
|  |             // Get an instance of the component | ||||||
|  |             final LocationComponent locationComponent = mapboxMap.getLocationComponent(); | ||||||
|  | 
 | ||||||
|  |             // Activate with options | ||||||
|  |             locationComponent.activateLocationComponent( | ||||||
|  |                 LocationComponentActivationOptions.builder(this, loadedMapStyle).build()); | ||||||
|  | 
 | ||||||
|  |             // Enable to make component visible | ||||||
|  |             locationComponent.setLocationComponentEnabled(true); | ||||||
|  | 
 | ||||||
|  |             // Set the component's camera mode | ||||||
|  |             locationComponent.setCameraMode(CameraMode.NONE); | ||||||
|  | 
 | ||||||
|  |             // Set the component's render mode | ||||||
|  |             locationComponent.setRenderMode(RenderMode.NORMAL); | ||||||
|  | 
 | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Acts on camera moving | ||||||
|  |      * @param reason int | ||||||
|  |      */ | ||||||
|  |     @Override | ||||||
|  |     public void onCameraMoveStarted(final int reason) { | ||||||
|  |         Timber.v("Map camera has begun moving."); | ||||||
|  |         if (markerImage.getTranslationY() == 0) { | ||||||
|  |             markerImage.animate().translationY(-75) | ||||||
|  |                 .setInterpolator(new OvershootInterpolator()).setDuration(250).start(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Acts on camera idle | ||||||
|  |      */ | ||||||
|  |     @Override | ||||||
|  |     public void onCameraIdle() { | ||||||
|  |         Timber.v("Map camera is now idling."); | ||||||
|  |         markerImage.animate().translationY(0) | ||||||
|  |             .setInterpolator(new OvershootInterpolator()).setDuration(250).start(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Takes action on camera position | ||||||
|  |      * @param position position of picker | ||||||
|  |      */ | ||||||
|  |     @Override | ||||||
|  |     public void onChanged(@Nullable CameraPosition position) { | ||||||
|  |         if (position == null) { | ||||||
|  |             position = new Builder() | ||||||
|  |                 .target(new LatLng(mapboxMap.getCameraPosition().target.getLatitude(), | ||||||
|  |                     mapboxMap.getCameraPosition().target.getLongitude())) | ||||||
|  |                 .zoom(16).build(); | ||||||
|  |         } | ||||||
|  |         cameraPosition = position; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Select the preferable location | ||||||
|  |      */ | ||||||
|  |     private void addPlaceSelectedButton() { | ||||||
|  |         placeSelectedButton = findViewById(R.id.location_chosen_button); | ||||||
|  |         placeSelectedButton.setOnClickListener(view -> placeSelected()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Return the intent with required data | ||||||
|  |      */ | ||||||
|  |     void placeSelected() { | ||||||
|  |         if (activity.equals("NoLocationUploadActivity")) { | ||||||
|  |             applicationKvStore.putString(LAST_LOCATION, | ||||||
|  |                 mapboxMap.getCameraPosition().target.getLatitude() | ||||||
|  |                     + "," | ||||||
|  |                     + mapboxMap.getCameraPosition().target.getLongitude()); | ||||||
|  |             applicationKvStore.putString(LAST_ZOOM, mapboxMap.getCameraPosition().zoom + ""); | ||||||
|  |         } | ||||||
|  |         final Intent returningIntent = new Intent(); | ||||||
|  |         returningIntent.putExtra(LocationPickerConstants.MAP_CAMERA_POSITION, | ||||||
|  |             mapboxMap.getCameraPosition()); | ||||||
|  |         setResult(AppCompatActivity.RESULT_OK, returningIntent); | ||||||
|  |         finish(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     protected void onStart() { | ||||||
|  |         super.onStart(); | ||||||
|  |         mapView.onStart(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     protected void onResume() { | ||||||
|  |         super.onResume(); | ||||||
|  |         mapView.onResume(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     protected void onPause() { | ||||||
|  |         super.onPause(); | ||||||
|  |         mapView.onPause(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     protected void onStop() { | ||||||
|  |         super.onStop(); | ||||||
|  |         mapView.onStop(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     protected void onSaveInstanceState(final @NotNull Bundle outState) { | ||||||
|  |         super.onSaveInstanceState(outState); | ||||||
|  |         mapView.onSaveInstanceState(outState); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     protected void onDestroy() { | ||||||
|  |         super.onDestroy(); | ||||||
|  |         mapView.onDestroy(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onLowMemory() { | ||||||
|  |         super.onLowMemory(); | ||||||
|  |         mapView.onLowMemory(); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,17 @@ | ||||||
|  | package fr.free.nrw.commons.LocationPicker; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Constants need for location picking | ||||||
|  |  */ | ||||||
|  | public final class LocationPickerConstants { | ||||||
|  | 
 | ||||||
|  |     public static final String ACTIVITY_KEY | ||||||
|  |         = "location.picker.activity"; | ||||||
|  | 
 | ||||||
|  |     public static final String MAP_CAMERA_POSITION | ||||||
|  |         = "location.picker.cameraPosition"; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     private LocationPickerConstants() { | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,63 @@ | ||||||
|  | package fr.free.nrw.commons.LocationPicker; | ||||||
|  | 
 | ||||||
|  | import android.app.Application; | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.lifecycle.AndroidViewModel; | ||||||
|  | import androidx.lifecycle.MutableLiveData; | ||||||
|  | import com.mapbox.mapboxsdk.camera.CameraPosition; | ||||||
|  | import org.jetbrains.annotations.NotNull; | ||||||
|  | import retrofit2.Call; | ||||||
|  | import retrofit2.Callback; | ||||||
|  | import retrofit2.Response; | ||||||
|  | import timber.log.Timber; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Observes live camera position data | ||||||
|  |  */ | ||||||
|  | public class LocationPickerViewModel extends AndroidViewModel implements Callback<CameraPosition> { | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Wrapping CameraPosition with MutableLiveData | ||||||
|  |      */ | ||||||
|  |     private final MutableLiveData<CameraPosition> result = new MutableLiveData<>(); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Constructor for this class | ||||||
|  |      * | ||||||
|  |      * @param application Application | ||||||
|  |      */ | ||||||
|  |     public LocationPickerViewModel(@NonNull final Application application) { | ||||||
|  |         super(application); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Responses on camera position changing | ||||||
|  |      * | ||||||
|  |      * @param call     Call<CameraPosition> | ||||||
|  |      * @param response Response<CameraPosition> | ||||||
|  |      */ | ||||||
|  |     @Override | ||||||
|  |     public void onResponse(final @NotNull Call<CameraPosition> call, | ||||||
|  |         final Response<CameraPosition> response) { | ||||||
|  |         if (response.body() == null) { | ||||||
|  |             result.setValue(null); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         result.setValue(response.body()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onFailure(final @NotNull Call<CameraPosition> call, final @NotNull Throwable t) { | ||||||
|  |         Timber.e(t); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Gets live CameraPosition | ||||||
|  |      * | ||||||
|  |      * @return MutableLiveData<CameraPosition> | ||||||
|  |      */ | ||||||
|  |     public MutableLiveData<CameraPosition> getResult() { | ||||||
|  |         return result; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
							
								
								
									
										30
									
								
								app/src/main/java/fr/free/nrw/commons/MapController.java
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								app/src/main/java/fr/free/nrw/commons/MapController.java
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,30 @@ | ||||||
|  | package fr.free.nrw.commons; | ||||||
|  | 
 | ||||||
|  | import fr.free.nrw.commons.location.LatLng; | ||||||
|  | import fr.free.nrw.commons.nearby.Place; | ||||||
|  | import java.util.List; | ||||||
|  | 
 | ||||||
|  | public abstract class MapController { | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * We pass this variable as a group of placeList and boundaryCoordinates | ||||||
|  |      */ | ||||||
|  |     public class NearbyPlacesInfo { | ||||||
|  |         public List<Place> placeList; // List of nearby places | ||||||
|  |         public LatLng[] boundaryCoordinates; // Corners of nearby area | ||||||
|  |         public LatLng curLatLng; // Current location when this places are populated | ||||||
|  |         public LatLng searchLatLng; // Search location for finding this places | ||||||
|  |         public List<Media> mediaList; // Search location for finding this places | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * We pass this variable as a group of placeList and boundaryCoordinates | ||||||
|  |      */ | ||||||
|  |     public class ExplorePlacesInfo { | ||||||
|  |         public List<Place> explorePlaceList; // List of nearby places | ||||||
|  |         public LatLng[] boundaryCoordinates; // Corners of nearby area | ||||||
|  |         public LatLng curLatLng; // Current location when this places are populated | ||||||
|  |         public LatLng searchLatLng; // Search location for finding this places | ||||||
|  |         public List<Media> mediaList; // Search location for finding this places | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,46 +0,0 @@ | ||||||
| package fr.free.nrw.commons |  | ||||||
| 
 |  | ||||||
| import fr.free.nrw.commons.location.LatLng |  | ||||||
| import fr.free.nrw.commons.nearby.Place |  | ||||||
| 
 |  | ||||||
| abstract class MapController { |  | ||||||
|     /** |  | ||||||
|      * We pass this variable as a group of placeList and boundaryCoordinates |  | ||||||
|      */ |  | ||||||
|     inner class NearbyPlacesInfo { |  | ||||||
|         @JvmField |  | ||||||
|         var placeList: List<Place> = emptyList() // List of nearby places |  | ||||||
| 
 |  | ||||||
|         @JvmField |  | ||||||
|         var boundaryCoordinates: Array<LatLng> = emptyArray() // Corners of nearby area |  | ||||||
| 
 |  | ||||||
|         @JvmField |  | ||||||
|         var currentLatLng: LatLng? = null // Current location when this places are populated |  | ||||||
| 
 |  | ||||||
|         @JvmField |  | ||||||
|         var searchLatLng: LatLng? = null // Search location for finding this places |  | ||||||
| 
 |  | ||||||
|         @JvmField |  | ||||||
|         var mediaList: List<Media>? = null // Search location for finding this places |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * We pass this variable as a group of placeList and boundaryCoordinates |  | ||||||
|      */ |  | ||||||
|     inner class ExplorePlacesInfo { |  | ||||||
|         @JvmField |  | ||||||
|         var explorePlaceList: List<Place> = emptyList() // List of nearby places |  | ||||||
| 
 |  | ||||||
|         @JvmField |  | ||||||
|         var boundaryCoordinates: Array<LatLng> = emptyArray() // Corners of nearby area |  | ||||||
| 
 |  | ||||||
|         @JvmField |  | ||||||
|         var currentLatLng: LatLng? = null // Current location when this places are populated |  | ||||||
| 
 |  | ||||||
|         @JvmField |  | ||||||
|         var searchLatLng: LatLng? = null // Search location for finding this places |  | ||||||
| 
 |  | ||||||
|         @JvmField |  | ||||||
|         var mediaList: List<Media> = emptyList() // Search location for finding this places |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -1,15 +1,11 @@ | ||||||
| package fr.free.nrw.commons | package fr.free.nrw.commons | ||||||
| 
 | 
 | ||||||
| import android.os.Parcelable | import android.os.Parcelable | ||||||
| import fr.free.nrw.commons.BuildConfig.COMMONS_URL |  | ||||||
| import fr.free.nrw.commons.location.LatLng | import fr.free.nrw.commons.location.LatLng | ||||||
| import fr.free.nrw.commons.wikidata.model.WikiSite | import kotlinx.android.parcel.Parcelize | ||||||
| import fr.free.nrw.commons.wikidata.model.page.PageTitle | import org.wikipedia.dataclient.mwapi.MwQueryPage | ||||||
| import kotlinx.parcelize.IgnoredOnParcel | import org.wikipedia.page.PageTitle | ||||||
| import kotlinx.parcelize.Parcelize | import java.util.* | ||||||
| import java.util.Date |  | ||||||
| import java.util.Locale |  | ||||||
| import java.util.UUID |  | ||||||
| 
 | 
 | ||||||
| @Parcelize | @Parcelize | ||||||
| class Media constructor( | class Media constructor( | ||||||
|  | @ -19,6 +15,7 @@ class Media constructor( | ||||||
|      */ |      */ | ||||||
|     var pageId: String = UUID.randomUUID().toString(), |     var pageId: String = UUID.randomUUID().toString(), | ||||||
|     var thumbUrl: String? = null, |     var thumbUrl: String? = null, | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Gets image URL |      * Gets image URL | ||||||
|      * @return Image URL |      * @return Image URL | ||||||
|  | @ -30,9 +27,16 @@ class Media constructor( | ||||||
|      */ |      */ | ||||||
|     var filename: String? = null, |     var filename: String? = null, | ||||||
|     /** |     /** | ||||||
|      * The fallback description of the file, used if no other description is provided. |      * Gets the file description. | ||||||
|  |      * @return file description as a string | ||||||
|  |      */ | ||||||
|  |     // monolingual description on input... | ||||||
|  |     /** | ||||||
|  |      * Sets the file description. | ||||||
|  |      * @param fallbackDescription the new description of the file | ||||||
|      */ |      */ | ||||||
|     var fallbackDescription: String? = null, |     var fallbackDescription: String? = null, | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Gets the upload date of the file. |      * Gets the upload date of the file. | ||||||
|      * Can be null. |      * Can be null. | ||||||
|  | @ -40,25 +44,28 @@ class Media constructor( | ||||||
|      */ |      */ | ||||||
|     var dateUploaded: Date? = null, |     var dateUploaded: Date? = null, | ||||||
|     /** |     /** | ||||||
|      * The license name of the file. |      * Gets the license name of the file. | ||||||
|  |      * @return license as a String | ||||||
|  |      */ | ||||||
|  |     /** | ||||||
|  |      * Sets the license name of the file. | ||||||
|  |      * | ||||||
|  |      * @param license license name as a String | ||||||
|      */ |      */ | ||||||
|     var license: String? = null, |     var license: String? = null, | ||||||
|     /** |  | ||||||
|      * The URL corresponding to the license. |  | ||||||
|      */ |  | ||||||
|     var licenseUrl: String? = null, |     var licenseUrl: String? = null, | ||||||
|     /** |     /** | ||||||
|      * The name of the creator of the file. |      * Gets the name of the creator of the file. | ||||||
|  |      * @return author name as a String | ||||||
|  |      */ | ||||||
|  |     /** | ||||||
|  |      * Sets the author name of the file. | ||||||
|  |      * @param author creator name as a string | ||||||
|      */ |      */ | ||||||
|     var author: String? = null, |     var author: String? = null, | ||||||
|     /** | 
 | ||||||
|      * The username of the uploader. |     var user:String?=null, | ||||||
|      */ | 
 | ||||||
|     var user: String? = null, |  | ||||||
|     /** |  | ||||||
|      * The full name of the file's creator, if different from username. |  | ||||||
|      */ |  | ||||||
|     var creatorName: String? = null, |  | ||||||
|     /** |     /** | ||||||
|      * Gets the categories the file falls under. |      * Gets the categories the file falls under. | ||||||
|      * @return file categories as an ArrayList of Strings |      * @return file categories as an ArrayList of Strings | ||||||
|  | @ -72,111 +79,46 @@ class Media constructor( | ||||||
|     var captions: Map<String, String> = emptyMap(), |     var captions: Map<String, String> = emptyMap(), | ||||||
|     var descriptions: Map<String, String> = emptyMap(), |     var descriptions: Map<String, String> = emptyMap(), | ||||||
|     var depictionIds: List<String> = emptyList(), |     var depictionIds: List<String> = emptyList(), | ||||||
|     var creatorIds: List<String> = emptyList(), |  | ||||||
|     /** |     /** | ||||||
|      * This field was added to find non-hidden categories |      * This field was added to find non-hidden categories | ||||||
|      * Stores the mapping of category title to hidden attribute |      * Stores the mapping of category title to hidden attribute | ||||||
|      * Example: "Mountains" => false, "CC-BY-SA-2.0" => true |      * Example: "Mountains" => false, "CC-BY-SA-2.0" => true | ||||||
|      */ |      */ | ||||||
|     var categoriesHiddenStatus: Map<String, Boolean> = emptyMap(), |     var categoriesHiddenStatus: Map<String, Boolean> = emptyMap() | ||||||
| ) : Parcelable { | ) : Parcelable { | ||||||
|  | 
 | ||||||
|     constructor( |     constructor( | ||||||
|         captions: Map<String, String>, |         captions: Map<String, String>, | ||||||
|         categories: List<String>?, |         categories: List<String>?, | ||||||
|         filename: String?, |         filename: String?, | ||||||
|         fallbackDescription: String?, |         fallbackDescription: String?, | ||||||
|         author: String?, |         author: String?, user:String? | ||||||
|         user: String?, |  | ||||||
|     ) : this( |     ) : this( | ||||||
|         filename = filename, |         filename = filename, | ||||||
|         fallbackDescription = fallbackDescription, |         fallbackDescription = fallbackDescription, | ||||||
|         dateUploaded = Date(), |         dateUploaded = Date(), | ||||||
|         author = author, |         author = author, | ||||||
|         user = user, |         user=user, | ||||||
|         categories = categories, |         categories = categories, | ||||||
|         captions = captions, |         captions = captions | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
|     constructor( |  | ||||||
|         captions: Map<String, String>, |  | ||||||
|         categories: List<String>?, |  | ||||||
|         filename: String?, |  | ||||||
|         fallbackDescription: String?, |  | ||||||
|         author: String?, |  | ||||||
|         user: String?, |  | ||||||
|         dateUploaded: Date? = Date(), |  | ||||||
|         license: String? = null, |  | ||||||
|         licenseUrl: String? = null, |  | ||||||
|         imageUrl: String? = null, |  | ||||||
|         thumbUrl: String? = null, |  | ||||||
|         coordinates: LatLng? = null, |  | ||||||
|         descriptions: Map<String, String> = emptyMap(), |  | ||||||
|         depictionIds: List<String> = emptyList(), |  | ||||||
|         categoriesHiddenStatus: Map<String, Boolean> = emptyMap() |  | ||||||
|     ) : this( |  | ||||||
|         pageId = UUID.randomUUID().toString(), |  | ||||||
|         filename = filename, |  | ||||||
|         fallbackDescription = fallbackDescription, |  | ||||||
|         dateUploaded = dateUploaded, |  | ||||||
|         author = author, |  | ||||||
|         user = user, |  | ||||||
|         categories = categories, |  | ||||||
|         captions = captions, |  | ||||||
|         license = license, |  | ||||||
|         licenseUrl = licenseUrl, |  | ||||||
|         imageUrl = imageUrl, |  | ||||||
|         thumbUrl = thumbUrl, |  | ||||||
|         coordinates = coordinates, |  | ||||||
|         descriptions = descriptions, |  | ||||||
|         depictionIds = depictionIds, |  | ||||||
|         categoriesHiddenStatus = categoriesHiddenStatus |  | ||||||
|     ) |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Returns Author if it's not null or empty, otherwise |  | ||||||
|      * returns user |  | ||||||
|      * @return Author or User |  | ||||||
|      */ |  | ||||||
|     @Deprecated("Use user for uploader username. Use attributedAuthor() for attribution. Note that the uploader may not be the creator/author.") |  | ||||||
|     fun getAuthorOrUser(): String? { |  | ||||||
|         return if (!author.isNullOrEmpty()) { |  | ||||||
|             author |  | ||||||
|         } else{ |  | ||||||
|             user |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Returns author if it's not null or empty, otherwise |  | ||||||
|      * returns creator name |  | ||||||
|      * @return name of author or creator |  | ||||||
|      */ |  | ||||||
|     fun getAttributedAuthor(): String? { |  | ||||||
|         return if (!author.isNullOrEmpty()) { |  | ||||||
|             author |  | ||||||
|         } else{ |  | ||||||
|             creatorName |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |     /** | ||||||
|      * Gets media display title |      * Gets media display title | ||||||
|      * @return Media title |      * @return Media title | ||||||
|      */ |      */ | ||||||
|     val displayTitle: String |     val displayTitle: String | ||||||
|         get() = |         get() = | ||||||
|             if (filename != null) { |             if (filename != null) | ||||||
|                 pageTitle.displayTextWithoutNamespace.replaceFirst("[.][^.]+$".toRegex(), "") |                 pageTitle.displayTextWithoutNamespace.replaceFirst("[.][^.]+$".toRegex(), "") | ||||||
|             } else { |             else | ||||||
|                 "" |                 "" | ||||||
|             } |  | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Gets file page title |      * Gets file page title | ||||||
|      * @return New media page title |      * @return New media page title | ||||||
|      */ |      */ | ||||||
|     val pageTitle: PageTitle |     val pageTitle: PageTitle get() = Utils.getPageTitle(filename!!) | ||||||
|         get() = PageTitle(filename!!, WikiSite(COMMONS_URL)) |  | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Returns wikicode to use the media file on a MediaWiki site |      * Returns wikicode to use the media file on a MediaWiki site | ||||||
|  | @ -186,21 +128,17 @@ class Media constructor( | ||||||
|         get() = String.format("[[%s|thumb|%s]]", filename, mostRelevantCaption) |         get() = String.format("[[%s|thumb|%s]]", filename, mostRelevantCaption) | ||||||
| 
 | 
 | ||||||
|     val mostRelevantCaption: String |     val mostRelevantCaption: String | ||||||
|         get() = |         get() = captions[Locale.getDefault().language] | ||||||
|             captions[Locale.getDefault().language] |             ?: captions.values.firstOrNull() | ||||||
|                 ?: captions.values.firstOrNull() |             ?: displayTitle | ||||||
|                 ?: displayTitle |  | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Gets the categories the file falls under. |      * Gets the categories the file falls under. | ||||||
|      * @return file categories as an ArrayList of Strings |      * @return file categories as an ArrayList of Strings | ||||||
|      */ |      */ | ||||||
|     @IgnoredOnParcel |  | ||||||
|     var addedCategories: List<String>? = null |     var addedCategories: List<String>? = null | ||||||
|         // TODO added categories should be removed. It is added for a short fix. On category update, |         // TODO added categories should be removed. It is added for a short fix. On category update, | ||||||
|         //  categories should be re-fetched instead |         //  categories should be re-fetched instead | ||||||
|         get() = field // getter |         get() = field                     // getter | ||||||
|         set(value) { |         set(value) { field = value }      // setter | ||||||
|             field = value |  | ||||||
|         } // setter |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,9 +1,9 @@ | ||||||
| package fr.free.nrw.commons | package fr.free.nrw.commons | ||||||
| 
 | 
 | ||||||
| import androidx.core.text.HtmlCompat | import androidx.core.text.HtmlCompat | ||||||
| import fr.free.nrw.commons.media.IdAndLabels |  | ||||||
| import fr.free.nrw.commons.media.MediaClient |  | ||||||
| import fr.free.nrw.commons.media.PAGE_ID_PREFIX | import fr.free.nrw.commons.media.PAGE_ID_PREFIX | ||||||
|  | import fr.free.nrw.commons.media.IdAndCaptions | ||||||
|  | import fr.free.nrw.commons.media.MediaClient | ||||||
| import io.reactivex.Single | import io.reactivex.Single | ||||||
| import timber.log.Timber | import timber.log.Timber | ||||||
| import javax.inject.Inject | import javax.inject.Inject | ||||||
|  | @ -17,56 +17,42 @@ import javax.inject.Singleton | ||||||
|  * to the media and may change due to editing. |  * to the media and may change due to editing. | ||||||
|  */ |  */ | ||||||
| @Singleton | @Singleton | ||||||
| class MediaDataExtractor | class MediaDataExtractor @Inject constructor(private val mediaClient: MediaClient) { | ||||||
|     @Inject |  | ||||||
|     constructor( |  | ||||||
|         private val mediaClient: MediaClient, |  | ||||||
|     ) { |  | ||||||
|         fun fetchDepictionIdsAndLabels(media: Media) = |  | ||||||
|                 mediaClient |  | ||||||
|                 .getEntities(media.depictionIds) |  | ||||||
|                 .map { |  | ||||||
|                     it |  | ||||||
|                         .entities() |  | ||||||
|                         .mapValues { entry -> entry.value.labels().mapValues { it.value.value() } } |  | ||||||
|                 }.map { it.map { (key, value) -> IdAndLabels(key, value) } } |  | ||||||
|                 .onErrorReturn { emptyList() } |  | ||||||
| 
 | 
 | ||||||
|         fun fetchCreatorIdsAndLabels(media: Media) = |     fun fetchDepictionIdsAndLabels(media: Media) = | ||||||
|             mediaClient |         mediaClient.getEntities(media.depictionIds) | ||||||
|                 .getEntities(media.creatorIds) |             .map { | ||||||
|                 .map { |                 it.entities() | ||||||
|                     it |                     .mapValues { entry -> entry.value.labels().mapValues { it.value.value() } } | ||||||
|                         .entities() |             } | ||||||
|                         .mapValues { entry -> entry.value.labels().mapValues { it.value.value() } } |             .map { it.map { (key, value) -> IdAndCaptions(key, value) } } | ||||||
|                 }.map { it.map { (key, value) -> IdAndLabels(key, value) } } |             .onErrorReturn { emptyList() } | ||||||
|                 .onErrorReturn { emptyList() } |  | ||||||
| 
 | 
 | ||||||
|         fun checkDeletionRequestExists(media: Media) = mediaClient.checkPageExistsUsingTitle("Commons:Deletion_requests/" + media.filename) |     fun checkDeletionRequestExists(media: Media) = | ||||||
|  |         mediaClient.checkPageExistsUsingTitle("Commons:Deletion_requests/" + media.filename) | ||||||
| 
 | 
 | ||||||
|         fun fetchDiscussion(media: Media) = |     fun fetchDiscussion(media: Media) = | ||||||
|             mediaClient |         mediaClient.getPageHtml(media.filename!!.replace("File", "File talk")) | ||||||
|                 .getPageHtml(media.filename!!.replace("File", "File talk")) |             .map { HtmlCompat.fromHtml(it, HtmlCompat.FROM_HTML_MODE_LEGACY).toString() } | ||||||
|                 .map { HtmlCompat.fromHtml(it, HtmlCompat.FROM_HTML_MODE_LEGACY).toString() } |             .onErrorReturn { | ||||||
|                 .onErrorReturn { |                 Timber.d("Error occurred while fetching discussion") | ||||||
|                     Timber.d("Error occurred while fetching discussion") |                 "" | ||||||
|                     "" |             } | ||||||
|                 } |  | ||||||
| 
 | 
 | ||||||
|         fun refresh(media: Media): Single<Media> = |     fun refresh(media: Media): Single<Media> { | ||||||
|             Single.ambArray( |         return Single.ambArray( | ||||||
|                 mediaClient |             mediaClient.getMediaById(PAGE_ID_PREFIX + media.pageId) | ||||||
|                     .getMediaById(PAGE_ID_PREFIX + media.pageId) |                 .onErrorResumeNext { Single.never() }, | ||||||
|                     .onErrorResumeNext { Single.never() }, |             mediaClient.getMedia(media.filename) | ||||||
|                 mediaClient |                 .onErrorResumeNext { Single.never() } | ||||||
|                     .getMediaSuppressingErrors(media.filename) |         ) | ||||||
|                     .onErrorResumeNext { Single.never() }, |  | ||||||
|             ) |  | ||||||
| 
 | 
 | ||||||
|         fun getHtmlOfPage(title: String) = mediaClient.getPageHtml(title) |  | ||||||
| 
 |  | ||||||
|         /** |  | ||||||
|          * Fetches wikitext from mediaClient |  | ||||||
|          */ |  | ||||||
|         fun getCurrentWikiText(title: String) = mediaClient.getCurrentWikiText(title) |  | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     fun getHtmlOfPage(title: String) = mediaClient.getPageHtml(title); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Fetches wikitext from mediaClient | ||||||
|  |      */ | ||||||
|  |     fun getCurrentWikiText(title: String) = mediaClient.getCurrentWikiText(title); | ||||||
|  | } | ||||||
|  |  | ||||||
							
								
								
									
										8
									
								
								app/src/main/java/fr/free/nrw/commons/MvpView.java
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								app/src/main/java/fr/free/nrw/commons/MvpView.java
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,8 @@ | ||||||
|  | package fr.free.nrw.commons; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Base interface for all the views | ||||||
|  |  */ | ||||||
|  | public interface MvpView { | ||||||
|  |     void showMessage(String message); | ||||||
|  | } | ||||||
|  | @ -0,0 +1,114 @@ | ||||||
|  | package fr.free.nrw.commons; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import java.io.File; | ||||||
|  | import java.io.IOException; | ||||||
|  | import java.util.Arrays; | ||||||
|  | import java.util.Collections; | ||||||
|  | import java.util.List; | ||||||
|  | import java.util.concurrent.TimeUnit; | ||||||
|  | import okhttp3.Cache; | ||||||
|  | import okhttp3.Interceptor; | ||||||
|  | import okhttp3.OkHttpClient; | ||||||
|  | import okhttp3.Request; | ||||||
|  | import okhttp3.Response; | ||||||
|  | import okhttp3.ResponseBody; | ||||||
|  | import okhttp3.logging.HttpLoggingInterceptor; | ||||||
|  | import okhttp3.logging.HttpLoggingInterceptor.Level; | ||||||
|  | import org.wikipedia.dataclient.SharedPreferenceCookieManager; | ||||||
|  | import org.wikipedia.dataclient.okhttp.HttpStatusException; | ||||||
|  | import timber.log.Timber; | ||||||
|  | 
 | ||||||
|  | public final class OkHttpConnectionFactory { | ||||||
|  |     private static final String CACHE_DIR_NAME = "okhttp-cache"; | ||||||
|  |     private static final long NET_CACHE_SIZE = 64 * 1024 * 1024; | ||||||
|  |     @NonNull private static final Cache NET_CACHE = new Cache(new File(CommonsApplication.getInstance().getCacheDir(), | ||||||
|  |             CACHE_DIR_NAME), NET_CACHE_SIZE); | ||||||
|  | 
 | ||||||
|  |     @NonNull | ||||||
|  |     private static final OkHttpClient CLIENT = createClient(); | ||||||
|  | 
 | ||||||
|  |     @NonNull public static OkHttpClient getClient() { | ||||||
|  |         return CLIENT; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @NonNull | ||||||
|  |     private static OkHttpClient createClient() { | ||||||
|  |         return new OkHttpClient.Builder() | ||||||
|  |                 .cookieJar(SharedPreferenceCookieManager.getInstance()) | ||||||
|  |                 .cache(NET_CACHE) | ||||||
|  |                 .connectTimeout(120, TimeUnit.SECONDS) | ||||||
|  |                 .writeTimeout(120, TimeUnit.SECONDS) | ||||||
|  |                 .readTimeout(120, TimeUnit.SECONDS) | ||||||
|  |                 .addInterceptor(getLoggingInterceptor()) | ||||||
|  |                 .addInterceptor(new UnsuccessfulResponseInterceptor()) | ||||||
|  |                 .addInterceptor(new CommonHeaderRequestInterceptor()) | ||||||
|  |                 .build(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static HttpLoggingInterceptor getLoggingInterceptor() { | ||||||
|  |         final HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor() | ||||||
|  |             .setLevel(Level.BASIC); | ||||||
|  | 
 | ||||||
|  |         httpLoggingInterceptor.redactHeader("Authorization"); | ||||||
|  |         httpLoggingInterceptor.redactHeader("Cookie"); | ||||||
|  | 
 | ||||||
|  |         return httpLoggingInterceptor; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static class CommonHeaderRequestInterceptor implements Interceptor { | ||||||
|  | 
 | ||||||
|  |         @Override | ||||||
|  |         @NonNull | ||||||
|  |         public Response intercept(@NonNull final Chain chain) throws IOException { | ||||||
|  |             final Request request = chain.request().newBuilder() | ||||||
|  |                     .header("User-Agent", CommonsApplication.getInstance().getUserAgent()) | ||||||
|  |                     .build(); | ||||||
|  |             return chain.proceed(request); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static class UnsuccessfulResponseInterceptor implements Interceptor { | ||||||
|  |         private static final List<String> DO_NOT_INTERCEPT = Collections.singletonList( | ||||||
|  |             "api.php?format=json&formatversion=2&errorformat=plaintext&action=upload&ignorewarnings=1"); | ||||||
|  | 
 | ||||||
|  |         private static final String ERRORS_PREFIX = "{\"error"; | ||||||
|  | 
 | ||||||
|  |         @Override | ||||||
|  |         @NonNull | ||||||
|  |         public Response intercept(@NonNull final Chain chain) throws IOException { | ||||||
|  |             final Response rsp = chain.proceed(chain.request()); | ||||||
|  | 
 | ||||||
|  |             // Do not intercept certain requests and let the caller handle the errors | ||||||
|  |             if(isExcludedUrl(chain.request())) { | ||||||
|  |                 return rsp; | ||||||
|  |             } | ||||||
|  |             if (rsp.isSuccessful()) { | ||||||
|  |                 try (final ResponseBody responseBody = rsp.peekBody(ERRORS_PREFIX.length())) { | ||||||
|  |                     if (ERRORS_PREFIX.equals(responseBody.string())) { | ||||||
|  |                         try (final ResponseBody body = rsp.body()) { | ||||||
|  |                             throw new IOException(body.string()); | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } catch (final IOException e) { | ||||||
|  |                     Timber.e(e); | ||||||
|  |                 } | ||||||
|  |                 return rsp; | ||||||
|  |             } | ||||||
|  |             throw new HttpStatusException(rsp); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         private boolean isExcludedUrl(final Request request) { | ||||||
|  |             final String requestUrl = request.url().toString(); | ||||||
|  |             for(final String url: DO_NOT_INTERCEPT) { | ||||||
|  |                 if(requestUrl.contains(url)) { | ||||||
|  |                     return true; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private OkHttpConnectionFactory() { | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,135 +0,0 @@ | ||||||
| package fr.free.nrw.commons |  | ||||||
| 
 |  | ||||||
| import androidx.annotation.VisibleForTesting |  | ||||||
| import fr.free.nrw.commons.wikidata.GsonUtil |  | ||||||
| import fr.free.nrw.commons.wikidata.cookies.CommonsCookieJar |  | ||||||
| import fr.free.nrw.commons.wikidata.mwapi.MwErrorResponse |  | ||||||
| import fr.free.nrw.commons.wikidata.mwapi.MwIOException |  | ||||||
| import fr.free.nrw.commons.wikidata.mwapi.MwLegacyServiceError |  | ||||||
| import okhttp3.Cache |  | ||||||
| import okhttp3.Interceptor |  | ||||||
| import okhttp3.OkHttpClient |  | ||||||
| import okhttp3.Request |  | ||||||
| import okhttp3.Response |  | ||||||
| import okhttp3.logging.HttpLoggingInterceptor |  | ||||||
| import timber.log.Timber |  | ||||||
| import java.io.File |  | ||||||
| import java.io.IOException |  | ||||||
| import java.util.concurrent.TimeUnit |  | ||||||
| 
 |  | ||||||
| object OkHttpConnectionFactory { |  | ||||||
|     private const val CACHE_DIR_NAME = "okhttp-cache" |  | ||||||
|     private const val NET_CACHE_SIZE = (64 * 1024 * 1024).toLong() |  | ||||||
| 
 |  | ||||||
|     @VisibleForTesting |  | ||||||
|     var CLIENT: OkHttpClient? = null |  | ||||||
| 
 |  | ||||||
|     fun getClient(cookieJar: CommonsCookieJar): OkHttpClient { |  | ||||||
|         if (CLIENT == null) { |  | ||||||
|             CLIENT = createClient(cookieJar) |  | ||||||
|         } |  | ||||||
|         return CLIENT!! |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private fun createClient(cookieJar: CommonsCookieJar): OkHttpClient { |  | ||||||
|         return OkHttpClient.Builder() |  | ||||||
|             .cookieJar(cookieJar) |  | ||||||
|             .cache( |  | ||||||
|                 if (CommonsApplication.instance != null) Cache( |  | ||||||
|                     File(CommonsApplication.instance.cacheDir, CACHE_DIR_NAME), |  | ||||||
|                     NET_CACHE_SIZE |  | ||||||
|                 ) else null |  | ||||||
|             ) |  | ||||||
|             .connectTimeout(120, TimeUnit.SECONDS) |  | ||||||
|             .writeTimeout(120, TimeUnit.SECONDS) |  | ||||||
|             .readTimeout(120, TimeUnit.SECONDS) |  | ||||||
|             .addInterceptor(HttpLoggingInterceptor().apply { |  | ||||||
|                 setLevel(HttpLoggingInterceptor.Level.BASIC) |  | ||||||
|                 redactHeader("Authorization") |  | ||||||
|                 redactHeader("Cookie") |  | ||||||
|             }) |  | ||||||
|             .addInterceptor(UnsuccessfulResponseInterceptor()) |  | ||||||
|             .addInterceptor(CommonHeaderRequestInterceptor()) |  | ||||||
|             .build() |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| class CommonHeaderRequestInterceptor : Interceptor { |  | ||||||
|     @Throws(IOException::class) |  | ||||||
|     override fun intercept(chain: Interceptor.Chain): Response { |  | ||||||
|         val request = chain.request().newBuilder() |  | ||||||
|             .header("User-Agent", CommonsApplication.instance.userAgent) |  | ||||||
|             .build() |  | ||||||
|         return chain.proceed(request) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| private const val SUPPRESS_ERROR_LOG = "x-commons-suppress-error-log" |  | ||||||
| const val SUPPRESS_ERROR_LOG_HEADER: String = "$SUPPRESS_ERROR_LOG: true" |  | ||||||
| 
 |  | ||||||
| private class UnsuccessfulResponseInterceptor : Interceptor { |  | ||||||
|     @Throws(IOException::class) |  | ||||||
|     override fun intercept(chain: Interceptor.Chain): Response { |  | ||||||
|         val rq = chain.request() |  | ||||||
| 
 |  | ||||||
|         // If the request contains our special "suppress errors" header, make note of it |  | ||||||
|         // but don't pass that on to the server. |  | ||||||
|         val suppressErrors = rq.headers.names().contains(SUPPRESS_ERROR_LOG) |  | ||||||
|         val request = rq.newBuilder() |  | ||||||
|             .removeHeader(SUPPRESS_ERROR_LOG) |  | ||||||
|             .build() |  | ||||||
| 
 |  | ||||||
|         val rsp = chain.proceed(request) |  | ||||||
| 
 |  | ||||||
|         // Do not intercept certain requests and let the caller handle the errors |  | ||||||
|         if (isExcludedUrl(chain.request())) { |  | ||||||
|             return rsp |  | ||||||
|         } |  | ||||||
|         if (rsp.isSuccessful) { |  | ||||||
|             try { |  | ||||||
|                 rsp.peekBody(ERRORS_PREFIX.length.toLong()).use { responseBody -> |  | ||||||
|                     if (ERRORS_PREFIX == responseBody.string()) { |  | ||||||
|                         rsp.body.use { body -> |  | ||||||
|                             val bodyString = body!!.string() |  | ||||||
| 
 |  | ||||||
|                             throw MwIOException( |  | ||||||
|                                 "MediaWiki API returned error: $bodyString", |  | ||||||
|                                 GsonUtil.defaultGson.fromJson( |  | ||||||
|                                     bodyString, |  | ||||||
|                                     MwErrorResponse::class.java |  | ||||||
|                                 ).error!!, |  | ||||||
|                             ) |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } catch (e: MwIOException) { |  | ||||||
|                 // Log the error as debug (and therefore, "expected") or at error level |  | ||||||
|                 if (suppressErrors) { |  | ||||||
|                     Timber.d(e, "Suppressed (known / expected) error") |  | ||||||
|                 } else { |  | ||||||
|                     Timber.e(e) |  | ||||||
|                     throw e |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|             return rsp |  | ||||||
|         } |  | ||||||
|         throw IOException("Unsuccessful response") |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private fun isExcludedUrl(request: Request): Boolean { |  | ||||||
|         val requestUrl = request.url.toString() |  | ||||||
|         for (url in DO_NOT_INTERCEPT) { |  | ||||||
|             if (requestUrl.contains(url)) { |  | ||||||
|                 return true |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         return false |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     companion object { |  | ||||||
|         val DO_NOT_INTERCEPT = listOf( |  | ||||||
|             "api.php?format=json&formatversion=2&errorformat=plaintext&action=upload&ignorewarnings=1" |  | ||||||
|         ) |  | ||||||
|         const val ERRORS_PREFIX = "{\"error" |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -3,16 +3,13 @@ package fr.free.nrw.commons | ||||||
| internal object Urls { | internal object Urls { | ||||||
|     const val NEW_ISSUE_URL = "https://github.com/commons-app/apps-android-commons/issues" |     const val NEW_ISSUE_URL = "https://github.com/commons-app/apps-android-commons/issues" | ||||||
|     const val GITHUB_REPO_URL = "https://github.com/commons-app/apps-android-commons" |     const val GITHUB_REPO_URL = "https://github.com/commons-app/apps-android-commons" | ||||||
|     const val GITHUB_PACKAGE_NAME = "com.github.android" |  | ||||||
|     const val WEBSITE_URL = "https://commons-app.github.io" |     const val WEBSITE_URL = "https://commons-app.github.io" | ||||||
|     const val CREDITS_URL = "https://github.com/commons-app/apps-android-commons/blob/master/CREDITS" |     const val CREDITS_URL = "https://github.com/commons-app/apps-android-commons/blob/master/CREDITS" | ||||||
|     const val USER_GUIDE_URL = "https://commons-app.github.io/docs.html" |     const val USER_GUIDE_URL = "https://commons-app.github.io/docs.html" | ||||||
|     const val FAQ_URL = "https://github.com/commons-app/commons-app-documentation/blob/master/android/Frequently-Asked-Questions.md" |     const val FAQ_URL = "https://github.com/commons-app/commons-app-documentation/blob/master/android/Frequently-Asked-Questions.md" | ||||||
|     const val PLAY_STORE_PREFIX = "market://details?id=" |     const val PLAY_STORE_PREFIX = "market://details?id=" | ||||||
|     const val PLAY_STORE_URL_PREFIX = "https://play.google.com/store/apps/details?id=" |     const val PLAY_STORE_URL_PREFIX = "https://play.google.com/store/apps/details?id=" | ||||||
|     const val TRANSLATE_WIKI_URL = |     const val TRANSLATE_WIKI_URL = "https://translatewiki.net/w/i.php?title=Special:Translate&group=commons-android-strings&filter=%21translated&action=translate&language=" | ||||||
|         "https://translatewiki.net/w/i.php?title=Special:Translate" + |  | ||||||
|             "&group=commons-android-strings&filter=%21translated&action=translate&language=" |  | ||||||
|     const val FACEBOOK_WEB_URL = "https://www.facebook.com/1921335171459985" |     const val FACEBOOK_WEB_URL = "https://www.facebook.com/1921335171459985" | ||||||
|     const val FACEBOOK_APP_URL = "fb://page/1921335171459985" |     const val FACEBOOK_APP_URL = "fb://page/1921335171459985" | ||||||
|     const val FACEBOOK_PACKAGE_NAME = "com.facebook.katana" |     const val FACEBOOK_PACKAGE_NAME = "com.facebook.katana" | ||||||
|  |  | ||||||
							
								
								
									
										246
									
								
								app/src/main/java/fr/free/nrw/commons/Utils.java
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										246
									
								
								app/src/main/java/fr/free/nrw/commons/Utils.java
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,246 @@ | ||||||
|  | package fr.free.nrw.commons; | ||||||
|  | 
 | ||||||
|  | import android.content.ClipData; | ||||||
|  | import android.content.ClipboardManager; | ||||||
|  | import android.content.Context; | ||||||
|  | import android.content.Intent; | ||||||
|  | import android.graphics.Bitmap; | ||||||
|  | import android.net.Uri; | ||||||
|  | import android.text.SpannableString; | ||||||
|  | import android.text.style.UnderlineSpan; | ||||||
|  | import android.view.View; | ||||||
|  | import android.widget.TextView; | ||||||
|  | import android.widget.Toast; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.browser.customtabs.CustomTabColorSchemeParams; | ||||||
|  | import androidx.browser.customtabs.CustomTabsIntent; | ||||||
|  | import androidx.core.content.ContextCompat; | ||||||
|  | 
 | ||||||
|  | import fr.free.nrw.commons.kvstore.JsonKvStore; | ||||||
|  | import java.util.Date; | ||||||
|  | import org.wikipedia.dataclient.WikiSite; | ||||||
|  | import org.wikipedia.page.PageTitle; | ||||||
|  | 
 | ||||||
|  | import java.util.Locale; | ||||||
|  | import java.util.regex.Pattern; | ||||||
|  | 
 | ||||||
|  | import fr.free.nrw.commons.location.LatLng; | ||||||
|  | import fr.free.nrw.commons.settings.Prefs; | ||||||
|  | import fr.free.nrw.commons.utils.ViewUtil; | ||||||
|  | import timber.log.Timber; | ||||||
|  | 
 | ||||||
|  | import static android.widget.Toast.LENGTH_SHORT; | ||||||
|  | import static fr.free.nrw.commons.campaigns.CampaignView.CAMPAIGNS_DEFAULT_PREFERENCE; | ||||||
|  | 
 | ||||||
|  | public class Utils { | ||||||
|  | 
 | ||||||
|  |     public static PageTitle getPageTitle(@NonNull String title) { | ||||||
|  |         return new PageTitle(title, new WikiSite(BuildConfig.COMMONS_URL)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Generates licence name with given ID | ||||||
|  |      * @param license License ID | ||||||
|  |      * @return Name of license | ||||||
|  |      */ | ||||||
|  |     public static int licenseNameFor(String license) { | ||||||
|  |         switch (license) { | ||||||
|  |             case Prefs.Licenses.CC_BY_3: | ||||||
|  |                 return R.string.license_name_cc_by; | ||||||
|  |             case Prefs.Licenses.CC_BY_4: | ||||||
|  |                 return R.string.license_name_cc_by_four; | ||||||
|  |             case Prefs.Licenses.CC_BY_SA_3: | ||||||
|  |                 return R.string.license_name_cc_by_sa; | ||||||
|  |             case Prefs.Licenses.CC_BY_SA_4: | ||||||
|  |                 return R.string.license_name_cc_by_sa_four; | ||||||
|  |             case Prefs.Licenses.CC0: | ||||||
|  |                 return R.string.license_name_cc0; | ||||||
|  |         } | ||||||
|  |         throw new IllegalStateException("Unrecognized license value: " + license); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Generates license url with given ID | ||||||
|  |      * @param license License ID | ||||||
|  |      * @return Url of license | ||||||
|  |      */ | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     @NonNull | ||||||
|  |     public static String licenseUrlFor(String license) { | ||||||
|  |         switch (license) { | ||||||
|  |             case Prefs.Licenses.CC_BY_3: | ||||||
|  |                 return "https://creativecommons.org/licenses/by/3.0/"; | ||||||
|  |             case Prefs.Licenses.CC_BY_4: | ||||||
|  |                 return "https://creativecommons.org/licenses/by/4.0/"; | ||||||
|  |             case Prefs.Licenses.CC_BY_SA_3: | ||||||
|  |                 return "https://creativecommons.org/licenses/by-sa/3.0/"; | ||||||
|  |             case Prefs.Licenses.CC_BY_SA_4: | ||||||
|  |                 return "https://creativecommons.org/licenses/by-sa/4.0/"; | ||||||
|  |             case Prefs.Licenses.CC0: | ||||||
|  |                 return "https://creativecommons.org/publicdomain/zero/1.0/"; | ||||||
|  |             default: | ||||||
|  |                 throw new IllegalStateException("Unrecognized license value: " + license); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Adds extension to filename. Converts to .jpg if system provides .jpeg, adds .jpg if no extension detected | ||||||
|  |      * @param title File name | ||||||
|  |      * @param extension Correct extension | ||||||
|  |      * @return File with correct extension | ||||||
|  |      */ | ||||||
|  |     public static String fixExtension(String title, String extension) { | ||||||
|  |         Pattern jpegPattern = Pattern.compile("\\.jpeg$", Pattern.CASE_INSENSITIVE); | ||||||
|  | 
 | ||||||
|  |         // People are used to ".jpg" more than ".jpeg" which the system gives us. | ||||||
|  |         if (extension != null && extension.toLowerCase(Locale.ENGLISH).equals("jpeg")) { | ||||||
|  |             extension = "jpg"; | ||||||
|  |         } | ||||||
|  |         title = jpegPattern.matcher(title).replaceFirst(".jpg"); | ||||||
|  |         if (extension != null && !title.toLowerCase(Locale.getDefault()) | ||||||
|  |                 .endsWith("." + extension.toLowerCase(Locale.ENGLISH))) { | ||||||
|  |             title += "." + extension; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // If extension is still null, make it jpg. (Hotfix for https://github.com/commons-app/apps-android-commons/issues/228) | ||||||
|  |         // If title has an extension in it, if won't be true | ||||||
|  |         if (extension == null && title.lastIndexOf(".")<=0) { | ||||||
|  |            extension = "jpg"; | ||||||
|  |            title += "." + extension; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return title; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Launches intent to rate app | ||||||
|  |      * @param context | ||||||
|  |      */ | ||||||
|  |     public static void rateApp(Context context) { | ||||||
|  |         final String appPackageName = context.getPackageName(); | ||||||
|  |         try { | ||||||
|  |             context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(Urls.PLAY_STORE_PREFIX + appPackageName))); | ||||||
|  |         } | ||||||
|  |         catch (android.content.ActivityNotFoundException anfe) { | ||||||
|  |             handleWebUrl(context, Uri.parse(Urls.PLAY_STORE_URL_PREFIX + appPackageName)); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Opens Custom Tab Activity with in-app browser for the specified URL. | ||||||
|  |      * Launches intent for web URL | ||||||
|  |      * @param context | ||||||
|  |      * @param url | ||||||
|  |      */ | ||||||
|  |     public static void handleWebUrl(Context context, Uri url) { | ||||||
|  |         Timber.d("Launching web url %s", url.toString()); | ||||||
|  |         Intent browserIntent = new Intent(Intent.ACTION_VIEW, url); | ||||||
|  |         if (browserIntent.resolveActivity(context.getPackageManager()) == null) { | ||||||
|  |             Toast toast = Toast.makeText(context, context.getString(R.string.no_web_browser), LENGTH_SHORT); | ||||||
|  |             toast.show(); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         final CustomTabColorSchemeParams color = new CustomTabColorSchemeParams.Builder() | ||||||
|  |             .setToolbarColor(ContextCompat.getColor(context, R.color.primaryColor)) | ||||||
|  |             .setSecondaryToolbarColor(ContextCompat.getColor(context, R.color.primaryDarkColor)) | ||||||
|  |             .build(); | ||||||
|  | 
 | ||||||
|  |         CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder(); | ||||||
|  |         builder.setDefaultColorSchemeParams(color); | ||||||
|  |         builder.setExitAnimations(context, android.R.anim.slide_in_left, android.R.anim.slide_out_right); | ||||||
|  |         CustomTabsIntent customTabsIntent = builder.build(); | ||||||
|  |         // Clear previous browser tasks, so that back/exit buttons work as intended. | ||||||
|  |         customTabsIntent.intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); | ||||||
|  |         customTabsIntent.launchUrl(context, url); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Util function to handle geo coordinates | ||||||
|  |      * It no longer depends on google maps and any app capable of handling the map intent can handle it | ||||||
|  |      * @param context | ||||||
|  |      * @param latLng | ||||||
|  |      */ | ||||||
|  |     public static void handleGeoCoordinates(Context context, LatLng latLng) { | ||||||
|  |         Intent mapIntent = new Intent(Intent.ACTION_VIEW, latLng.getGmmIntentUri()); | ||||||
|  |         if (mapIntent.resolveActivity(context.getPackageManager()) != null) { | ||||||
|  |             context.startActivity(mapIntent); | ||||||
|  |         } else { | ||||||
|  |             ViewUtil.showShortToast(context, context.getString(R.string.map_application_missing)); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * To take screenshot of the screen and return it in Bitmap format | ||||||
|  |      * | ||||||
|  |      * @param view | ||||||
|  |      * @return | ||||||
|  |      */ | ||||||
|  |     public static Bitmap getScreenShot(View view) { | ||||||
|  |         View screenView = view.getRootView(); | ||||||
|  |         screenView.setDrawingCacheEnabled(true); | ||||||
|  |         Bitmap drawingCache = screenView.getDrawingCache(); | ||||||
|  |         if (drawingCache != null) { | ||||||
|  |             Bitmap bitmap = Bitmap.createBitmap(drawingCache); | ||||||
|  |             screenView.setDrawingCacheEnabled(false); | ||||||
|  |             return bitmap; | ||||||
|  |         } | ||||||
|  |         return null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /* | ||||||
|  |     *Copies the content to the clipboard | ||||||
|  |     * | ||||||
|  |     */ | ||||||
|  |     public static void copy(String label,String text, Context context){ | ||||||
|  |         ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); | ||||||
|  |         ClipData clip = ClipData.newPlainText(label, text); | ||||||
|  |         clipboard.setPrimaryClip(clip); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * This method sets underlined string text to a TextView | ||||||
|  |      * | ||||||
|  |      * @param textView TextView associated with string resource | ||||||
|  |      * @param stringResourceName string resource name | ||||||
|  |      * @param context | ||||||
|  |      */ | ||||||
|  |     public static void setUnderlinedText(TextView textView, int stringResourceName, Context context) { | ||||||
|  |         SpannableString content = new SpannableString(context.getString(stringResourceName)); | ||||||
|  |         content.setSpan(new UnderlineSpan(), 0, content.length(), 0); | ||||||
|  |         textView.setText(content); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * For now we are enabling the monuments only when the date lies between 1 Sept & 31 OCt | ||||||
|  |      * @param date | ||||||
|  |      * @return | ||||||
|  |      */ | ||||||
|  |     public static boolean isMonumentsEnabled(final Date date) { | ||||||
|  |         if (date.getMonth() == 8) { | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Util function to get the start date of wlm monument | ||||||
|  |      * For this release we are hardcoding it to be 1st September | ||||||
|  |      * @return | ||||||
|  |      */ | ||||||
|  |     public static String getWLMStartDate() { | ||||||
|  |         return "1 Sep"; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /*** | ||||||
|  |      * Util function to get the end date of wlm monument | ||||||
|  |      * For this release we are hardcoding it to be 31st October | ||||||
|  |      * @return | ||||||
|  |      */ | ||||||
|  |     public static String getWLMEndDate() { | ||||||
|  |         return "30 Sep"; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
							
								
								
									
										7
									
								
								app/src/main/java/fr/free/nrw/commons/ViewHolder.java
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								app/src/main/java/fr/free/nrw/commons/ViewHolder.java
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,7 @@ | ||||||
|  | package fr.free.nrw.commons; | ||||||
|  | 
 | ||||||
|  | import android.content.Context; | ||||||
|  | 
 | ||||||
|  | public interface ViewHolder<T> { | ||||||
|  |     void bindModel(Context context, T model); | ||||||
|  | } | ||||||
							
								
								
									
										57
									
								
								app/src/main/java/fr/free/nrw/commons/ViewPagerAdapter.java
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								app/src/main/java/fr/free/nrw/commons/ViewPagerAdapter.java
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,57 @@ | ||||||
|  | package fr.free.nrw.commons; | ||||||
|  | 
 | ||||||
|  | import androidx.fragment.app.Fragment; | ||||||
|  | import androidx.fragment.app.FragmentManager; | ||||||
|  | import androidx.fragment.app.FragmentPagerAdapter; | ||||||
|  | 
 | ||||||
|  | import java.util.ArrayList; | ||||||
|  | import java.util.List; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * This adapter will be used to display fragments in a ViewPager | ||||||
|  |  */ | ||||||
|  | public class ViewPagerAdapter extends FragmentPagerAdapter { | ||||||
|  |     private List<Fragment> fragmentList = new ArrayList<>(); | ||||||
|  |     private List<String> fragmentTitleList = new ArrayList<>(); | ||||||
|  | 
 | ||||||
|  |     public ViewPagerAdapter(FragmentManager manager) { | ||||||
|  |         super(manager); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * This method returns the fragment of the viewpager at a particular position | ||||||
|  |      * @param position | ||||||
|  |      */ | ||||||
|  |     @Override | ||||||
|  |     public Fragment getItem(int position) { | ||||||
|  |         return fragmentList.get(position); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * This method returns the total number of fragments in the viewpager. | ||||||
|  |      * @return size | ||||||
|  |      */ | ||||||
|  |     @Override | ||||||
|  |     public int getCount() { | ||||||
|  |         return fragmentList.size(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * This method sets the fragment and title list in the viewpager | ||||||
|  |      * @param fragmentList List of all fragments to be displayed in the viewpager | ||||||
|  |      * @param fragmentTitleList List of all titles of the fragments | ||||||
|  |      */ | ||||||
|  |     public void setTabData(List<Fragment> fragmentList, List<String> fragmentTitleList) { | ||||||
|  |         this.fragmentList = fragmentList; | ||||||
|  |         this.fragmentTitleList = fragmentTitleList; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * This method returns the title of the page at a particular position | ||||||
|  |      * @param position | ||||||
|  |      */ | ||||||
|  |     @Override | ||||||
|  |     public CharSequence getPageTitle(int position) { | ||||||
|  |         return fragmentTitleList.get(position); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,44 +0,0 @@ | ||||||
| package fr.free.nrw.commons |  | ||||||
| 
 |  | ||||||
| import android.content.Context |  | ||||||
| import androidx.fragment.app.Fragment |  | ||||||
| import androidx.fragment.app.FragmentManager |  | ||||||
| import androidx.fragment.app.FragmentPagerAdapter |  | ||||||
| import java.util.Locale |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * This adapter will be used to display fragments in a ViewPager |  | ||||||
|  */ |  | ||||||
| class ViewPagerAdapter : FragmentPagerAdapter { |  | ||||||
|     private val context: Context |  | ||||||
|     private var fragmentList: List<Fragment> = emptyList() |  | ||||||
|     private var fragmentTitleList: List<String> = emptyList() |  | ||||||
| 
 |  | ||||||
|     constructor(context: Context, manager: FragmentManager) : super(manager) { |  | ||||||
|         this.context = context |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     constructor(context: Context, manager: FragmentManager, behavior: Int) : super(manager, behavior) { |  | ||||||
|         this.context = context |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override fun getItem(position: Int): Fragment = fragmentList[position] |  | ||||||
| 
 |  | ||||||
|     override fun getPageTitle(position: Int): CharSequence = fragmentTitleList[position] |  | ||||||
| 
 |  | ||||||
|     override fun getCount(): Int = fragmentList.size |  | ||||||
| 
 |  | ||||||
|     fun setTabs(vararg titlesToFragments: Pair<Int, Fragment>) { |  | ||||||
|         // Enforce that every title must come from strings.xml and all will consistently be uppercase |  | ||||||
|         fragmentTitleList = titlesToFragments.map { |  | ||||||
|             context.getString(it.first).uppercase(Locale.ROOT) |  | ||||||
|         } |  | ||||||
|         fragmentList = titlesToFragments.map { it.second } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     companion object { |  | ||||||
|         // Convenience method for Java callers, can be removed when everything is migrated |  | ||||||
|         @JvmStatic |  | ||||||
|         fun pairOf(first: Int, second: Fragment) = first to second |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										116
									
								
								app/src/main/java/fr/free/nrw/commons/WelcomeActivity.java
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								app/src/main/java/fr/free/nrw/commons/WelcomeActivity.java
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,116 @@ | ||||||
|  | package fr.free.nrw.commons; | ||||||
|  | 
 | ||||||
|  | import android.content.Context; | ||||||
|  | import android.content.Intent; | ||||||
|  | import android.os.Bundle; | ||||||
|  | import android.view.View; | ||||||
|  | 
 | ||||||
|  | import androidx.viewpager.widget.ViewPager; | ||||||
|  | 
 | ||||||
|  | import com.viewpagerindicator.CirclePageIndicator; | ||||||
|  | 
 | ||||||
|  | import butterknife.BindView; | ||||||
|  | import butterknife.ButterKnife; | ||||||
|  | import butterknife.OnClick; | ||||||
|  | import fr.free.nrw.commons.quiz.QuizActivity; | ||||||
|  | import fr.free.nrw.commons.theme.BaseActivity; | ||||||
|  | import fr.free.nrw.commons.utils.ConfigUtils; | ||||||
|  | import android.app.AlertDialog; | ||||||
|  | import android.widget.Button; | ||||||
|  | 
 | ||||||
|  | public class WelcomeActivity extends BaseActivity { | ||||||
|  | 
 | ||||||
|  |     @BindView(R.id.welcomePager) | ||||||
|  |     ViewPager pager; | ||||||
|  |     @BindView(R.id.welcomePagerIndicator) | ||||||
|  |     CirclePageIndicator indicator; | ||||||
|  | 
 | ||||||
|  |     private WelcomePagerAdapter adapter = new WelcomePagerAdapter(); | ||||||
|  |     private boolean isQuiz; | ||||||
|  |     private AlertDialog.Builder dialogBuilder; | ||||||
|  |     private AlertDialog dialog; | ||||||
|  |     Button okButton; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Initialises exiting fields and dependencies | ||||||
|  |      * | ||||||
|  |      * @param savedInstanceState WelcomeActivity bundled data | ||||||
|  |      */ | ||||||
|  |     @Override | ||||||
|  |     public void onCreate(Bundle savedInstanceState) { | ||||||
|  |         super.onCreate(savedInstanceState); | ||||||
|  |         setContentView(R.layout.activity_welcome); | ||||||
|  | 
 | ||||||
|  |         if (getIntent() != null) { | ||||||
|  |             Bundle bundle = getIntent().getExtras(); | ||||||
|  |             if (bundle != null) { | ||||||
|  |                 isQuiz = bundle.getBoolean("isQuiz"); | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             isQuiz = false; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Enable skip button if beta flavor | ||||||
|  |         if (ConfigUtils.isBetaFlavour()) { | ||||||
|  |             findViewById(R.id.finishTutorialButton).setVisibility(View.VISIBLE); | ||||||
|  | 
 | ||||||
|  |             dialogBuilder = new AlertDialog.Builder(this); | ||||||
|  |             final View contactPopupView = getLayoutInflater().inflate(R.layout.popup_for_copyright,null); | ||||||
|  |             dialogBuilder.setView(contactPopupView); | ||||||
|  |             dialog = dialogBuilder.create(); | ||||||
|  |             dialog.show(); | ||||||
|  | 
 | ||||||
|  |             okButton = dialog.findViewById(R.id.button_ok); | ||||||
|  |             okButton.setOnClickListener(view -> dialog.dismiss()); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         ButterKnife.bind(this); | ||||||
|  | 
 | ||||||
|  |         pager.setAdapter(adapter); | ||||||
|  |         indicator.setViewPager(pager); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * References WelcomePageAdapter to null before the activity is destroyed | ||||||
|  |      */ | ||||||
|  |     @Override | ||||||
|  |     public void onDestroy() { | ||||||
|  |         if (isQuiz) { | ||||||
|  |             Intent i = new Intent(WelcomeActivity.this, QuizActivity.class); | ||||||
|  |             startActivity(i); | ||||||
|  |         } | ||||||
|  |         super.onDestroy(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Creates a way to change current activity to WelcomeActivity | ||||||
|  |      * | ||||||
|  |      * @param context Activity context | ||||||
|  |      */ | ||||||
|  |     public static void startYourself(Context context) { | ||||||
|  |         Intent welcomeIntent = new Intent(context, WelcomeActivity.class); | ||||||
|  |         context.startActivity(welcomeIntent); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Override onBackPressed() to go to previous tutorial 'pages' if not on first page | ||||||
|  |      */ | ||||||
|  |     @Override | ||||||
|  |     public void onBackPressed() { | ||||||
|  |         if (pager.getCurrentItem() != 0) { | ||||||
|  |             pager.setCurrentItem(pager.getCurrentItem() - 1, true); | ||||||
|  |         } else { | ||||||
|  |             if (defaultKvStore.getBoolean("firstrun", true)) { | ||||||
|  |                 finishAffinity(); | ||||||
|  |             } else { | ||||||
|  |                 super.onBackPressed(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @OnClick(R.id.finishTutorialButton) | ||||||
|  |     public void finishTutorial() { | ||||||
|  |         defaultKvStore.putBoolean("firstrun", false); | ||||||
|  |         finish(); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,80 +0,0 @@ | ||||||
| package fr.free.nrw.commons |  | ||||||
| 
 |  | ||||||
| import android.app.AlertDialog |  | ||||||
| import android.content.Context |  | ||||||
| import android.content.Intent |  | ||||||
| import android.os.Bundle |  | ||||||
| import android.view.View |  | ||||||
| import fr.free.nrw.commons.databinding.ActivityWelcomeBinding |  | ||||||
| import fr.free.nrw.commons.databinding.PopupForCopyrightBinding |  | ||||||
| import fr.free.nrw.commons.quiz.QuizActivity |  | ||||||
| import fr.free.nrw.commons.theme.BaseActivity |  | ||||||
| import fr.free.nrw.commons.utils.applyEdgeToEdgeAllInsets |  | ||||||
| import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour |  | ||||||
| 
 |  | ||||||
| class WelcomeActivity : BaseActivity() { |  | ||||||
|     private var binding: ActivityWelcomeBinding? = null |  | ||||||
|     private var isQuiz = false |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Initialises exiting fields and dependencies |  | ||||||
|      * |  | ||||||
|      * @param savedInstanceState WelcomeActivity bundled data |  | ||||||
|      */ |  | ||||||
|     public override fun onCreate(savedInstanceState: Bundle?) { |  | ||||||
|         super.onCreate(savedInstanceState) |  | ||||||
|         binding = ActivityWelcomeBinding.inflate(layoutInflater) |  | ||||||
|         applyEdgeToEdgeAllInsets(binding!!.welcomePager.rootView) |  | ||||||
|         setContentView(binding!!.root) |  | ||||||
| 
 |  | ||||||
|         isQuiz = intent?.extras?.getBoolean("isQuiz", false) ?: false |  | ||||||
| 
 |  | ||||||
|         // Enable skip button if beta flavor |  | ||||||
|         if (isBetaFlavour) { |  | ||||||
|             binding!!.finishTutorialButton.visibility = View.VISIBLE |  | ||||||
| 
 |  | ||||||
|             val copyrightBinding = PopupForCopyrightBinding.inflate(layoutInflater) |  | ||||||
| 
 |  | ||||||
|             val dialog = AlertDialog.Builder(this) |  | ||||||
|                 .setView(copyrightBinding.root) |  | ||||||
|                 .setCancelable(false) |  | ||||||
|                 .create() |  | ||||||
|             dialog.show() |  | ||||||
| 
 |  | ||||||
|             copyrightBinding.buttonOk.setOnClickListener { v: View? -> dialog.dismiss() } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         val adapter = WelcomePagerAdapter() |  | ||||||
|         binding!!.welcomePager.adapter = adapter |  | ||||||
|         binding!!.welcomePagerIndicator.setViewPager(binding!!.welcomePager) |  | ||||||
|         binding!!.finishTutorialButton.setOnClickListener { v: View? -> finishTutorial() } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public override fun onDestroy() { |  | ||||||
|         if (isQuiz) { |  | ||||||
|             startActivity(Intent(this, QuizActivity::class.java)) |  | ||||||
|         } |  | ||||||
|         super.onDestroy() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override fun onBackPressed() { |  | ||||||
|         if (binding!!.welcomePager.currentItem != 0) { |  | ||||||
|             binding!!.welcomePager.setCurrentItem(binding!!.welcomePager.currentItem - 1, true) |  | ||||||
|         } else { |  | ||||||
|             if (defaultKvStore.getBoolean("firstrun", true)) { |  | ||||||
|                 finishAffinity() |  | ||||||
|             } else { |  | ||||||
|                 super.onBackPressed() |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fun finishTutorial() { |  | ||||||
|         defaultKvStore.putBoolean("firstrun", false) |  | ||||||
|         finish() |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| fun Context.startWelcome() { |  | ||||||
|     startActivity(Intent(this, WelcomeActivity::class.java)) |  | ||||||
| } |  | ||||||
|  | @ -0,0 +1,75 @@ | ||||||
|  | package fr.free.nrw.commons; | ||||||
|  | 
 | ||||||
|  | import android.net.Uri; | ||||||
|  | import android.text.Html; | ||||||
|  | import android.view.LayoutInflater; | ||||||
|  | import android.view.View; | ||||||
|  | import android.view.ViewGroup; | ||||||
|  | import android.widget.TextView; | ||||||
|  | 
 | ||||||
|  | import androidx.viewpager.widget.PagerAdapter; | ||||||
|  | 
 | ||||||
|  | public class WelcomePagerAdapter extends PagerAdapter { | ||||||
|  |     private static final int[] PAGE_LAYOUTS = new int[]{ | ||||||
|  |             R.layout.welcome_wikipedia, | ||||||
|  |             R.layout.welcome_do_upload, | ||||||
|  |             R.layout.welcome_dont_upload, | ||||||
|  |             R.layout.welcome_image_example, | ||||||
|  |             R.layout.welcome_final | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Gets total number of layouts | ||||||
|  |      * @return Number of layouts | ||||||
|  |      */ | ||||||
|  |     @Override | ||||||
|  |     public int getCount() { | ||||||
|  |         return PAGE_LAYOUTS.length; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Compares given view with provided object | ||||||
|  |      * @param view Adapter view | ||||||
|  |      * @param object Adapter object | ||||||
|  |      * @return Equality between view and object | ||||||
|  |      */ | ||||||
|  |     @Override | ||||||
|  |     public boolean isViewFromObject(View view, Object object) { | ||||||
|  |         return (view == object); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public Object instantiateItem(ViewGroup container, int position) { | ||||||
|  |         LayoutInflater inflater = LayoutInflater.from(container.getContext()); | ||||||
|  |         ViewGroup layout = (ViewGroup) inflater.inflate(PAGE_LAYOUTS[position], container, false); | ||||||
|  | 
 | ||||||
|  |         // If final page | ||||||
|  |         if (position == PAGE_LAYOUTS.length - 1) { | ||||||
|  |             // Add link to more information | ||||||
|  |             TextView moreInfo = layout.findViewById(R.id.welcomeInfo); | ||||||
|  |             Utils.setUnderlinedText(moreInfo, R.string.welcome_help_button_text, container.getContext()); | ||||||
|  |             moreInfo.setOnClickListener(view -> Utils.handleWebUrl( | ||||||
|  |                     container.getContext(), | ||||||
|  |                     Uri.parse("https://commons.wikimedia.org/wiki/Help:Contents") | ||||||
|  |             )); | ||||||
|  | 
 | ||||||
|  |             // Handle click of finishTutorialButton ("YES!" button) inside layout | ||||||
|  |             layout.findViewById(R.id.finishTutorialButton) | ||||||
|  |                     .setOnClickListener(view -> ((WelcomeActivity) container.getContext()).finishTutorial()); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         container.addView(layout); | ||||||
|  |         return layout; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Provides a way to remove an item from container | ||||||
|  |      * @param container Adapter view group container | ||||||
|  |      * @param position Index of item | ||||||
|  |      * @param obj Adapter object | ||||||
|  |      */ | ||||||
|  |     @Override | ||||||
|  |     public void destroyItem(ViewGroup container, int position, Object obj) { | ||||||
|  |         container.removeView((View) obj); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,70 +0,0 @@ | ||||||
| package fr.free.nrw.commons |  | ||||||
| 
 |  | ||||||
| import android.view.LayoutInflater |  | ||||||
| import android.view.View |  | ||||||
| import android.view.ViewGroup |  | ||||||
| import android.widget.TextView |  | ||||||
| import androidx.core.net.toUri |  | ||||||
| import androidx.viewpager.widget.PagerAdapter |  | ||||||
| import fr.free.nrw.commons.utils.UnderlineUtils.setUnderlinedText |  | ||||||
| import fr.free.nrw.commons.utils.handleWebUrl |  | ||||||
| 
 |  | ||||||
| class WelcomePagerAdapter : PagerAdapter() { |  | ||||||
|     /** |  | ||||||
|      * Gets total number of layouts |  | ||||||
|      * @return Number of layouts |  | ||||||
|      */ |  | ||||||
|     override fun getCount(): Int = PAGE_LAYOUTS.size |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Compares given view with provided object |  | ||||||
|      * @param view Adapter view |  | ||||||
|      * @param obj Adapter object |  | ||||||
|      * @return Equality between view and object |  | ||||||
|      */ |  | ||||||
|     override fun isViewFromObject(view: View, obj: Any): Boolean = (view === obj) |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Provides a way to remove an item from container |  | ||||||
|      * @param container Adapter view group container |  | ||||||
|      * @param position Index of item |  | ||||||
|      * @param obj Adapter object |  | ||||||
|      */ |  | ||||||
|     override fun destroyItem(container: ViewGroup, position: Int, obj: Any) = |  | ||||||
|         container.removeView(obj as View) |  | ||||||
| 
 |  | ||||||
|     override fun instantiateItem(container: ViewGroup, position: Int): Any { |  | ||||||
|         val inflater = LayoutInflater.from(container.context) |  | ||||||
|         val layout = inflater.inflate(PAGE_LAYOUTS[position], container, false) as ViewGroup |  | ||||||
| 
 |  | ||||||
|         // If final page |  | ||||||
|         if (position == PAGE_LAYOUTS.size - 1) { |  | ||||||
|             // Add link to more information |  | ||||||
|             val moreInfo = layout.findViewById<TextView>(R.id.welcomeInfo) |  | ||||||
|             setUnderlinedText(moreInfo, R.string.welcome_help_button_text) |  | ||||||
|             moreInfo.setOnClickListener { |  | ||||||
|                 handleWebUrl( |  | ||||||
|                     container.context, |  | ||||||
|                     "https://commons.wikimedia.org/wiki/Help:Contents".toUri() |  | ||||||
|                 ) |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             // Handle click of finishTutorialButton ("YES!" button) inside layout |  | ||||||
|             layout.findViewById<View>(R.id.finishTutorialButton) |  | ||||||
|                 .setOnClickListener { view: View? -> (container.context as WelcomeActivity).finishTutorial() } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         container.addView(layout) |  | ||||||
|         return layout |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     companion object { |  | ||||||
|         private val PAGE_LAYOUTS = intArrayOf( |  | ||||||
|             R.layout.welcome_wikipedia, |  | ||||||
|             R.layout.welcome_do_upload, |  | ||||||
|             R.layout.welcome_dont_upload, |  | ||||||
|             R.layout.welcome_image_example, |  | ||||||
|             R.layout.welcome_final |  | ||||||
|         ) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -1,18 +0,0 @@ | ||||||
| package fr.free.nrw.commons.actions |  | ||||||
| 
 |  | ||||||
| import fr.free.nrw.commons.wikidata.mwapi.MwResponse |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Response of the Thanks API. |  | ||||||
|  * Context: |  | ||||||
|  * The Commons Android app lets you thank other contributors who have uploaded a great picture. |  | ||||||
|  * See https://www.mediawiki.org/wiki/Extension:Thanks |  | ||||||
|  */ |  | ||||||
| class MwThankPostResponse : MwResponse() { |  | ||||||
|     var result: Result? = null |  | ||||||
| 
 |  | ||||||
|     inner class Result { |  | ||||||
|         var success: Int? = null |  | ||||||
|         var recipient: String? = null |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -1,9 +1,8 @@ | ||||||
| package fr.free.nrw.commons.actions | package fr.free.nrw.commons.actions | ||||||
| 
 | 
 | ||||||
| import fr.free.nrw.commons.auth.csrf.CsrfTokenClient |  | ||||||
| import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException |  | ||||||
| import io.reactivex.Observable | import io.reactivex.Observable | ||||||
| import io.reactivex.Single | import io.reactivex.Single | ||||||
|  | import org.wikipedia.csrf.CsrfTokenClient | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * This class acts as a Client to facilitate wiki page editing |  * This class acts as a Client to facilitate wiki page editing | ||||||
|  | @ -14,8 +13,9 @@ import io.reactivex.Single | ||||||
|  */ |  */ | ||||||
| class PageEditClient( | class PageEditClient( | ||||||
|     private val csrfTokenClient: CsrfTokenClient, |     private val csrfTokenClient: CsrfTokenClient, | ||||||
|     private val pageEditInterface: PageEditInterface, |     private val pageEditInterface: PageEditInterface | ||||||
| ) { | ) { | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Replace the content of a wiki page |      * Replace the content of a wiki page | ||||||
|      * @param pageTitle   Title of the page to edit |      * @param pageTitle   Title of the page to edit | ||||||
|  | @ -23,60 +23,14 @@ class PageEditClient( | ||||||
|      * @param summary     Edit summary |      * @param summary     Edit summary | ||||||
|      * @return whether the edit was successful |      * @return whether the edit was successful | ||||||
|      */ |      */ | ||||||
|     fun edit( |     fun edit(pageTitle: String, text: String, summary: String): Observable<Boolean> { | ||||||
|         pageTitle: String, |         return try { | ||||||
|         text: String, |             pageEditInterface.postEdit(pageTitle, summary, text, csrfTokenClient.tokenBlocking) | ||||||
|         summary: String, |                 .map { editResponse -> editResponse.edit()!!.editSucceeded() } | ||||||
|     ): Observable<Boolean> = |  | ||||||
|         try { |  | ||||||
|             pageEditInterface |  | ||||||
|                 .postEdit(pageTitle, summary, text, csrfTokenClient.getTokenBlocking()) |  | ||||||
|                 .map { editResponse -> |  | ||||||
|                     editResponse.edit()!!.editSucceeded() |  | ||||||
|                 } |  | ||||||
|         } catch (throwable: Throwable) { |         } catch (throwable: Throwable) { | ||||||
|             if (throwable is InvalidLoginTokenException) { |             Observable.just(false) | ||||||
|                 throw throwable |  | ||||||
|             } else { |  | ||||||
|                 Observable.just(false) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Creates a new page with the given title, text, and summary. |  | ||||||
|      * |  | ||||||
|      * @param pageTitle The title of the page to be created. |  | ||||||
|      * @param text      The content of the page in wikitext format. |  | ||||||
|      * @param summary   The edit summary for the page creation. |  | ||||||
|      * @return An observable that emits true if the page creation succeeded, false otherwise. |  | ||||||
|      * @throws InvalidLoginTokenException If an invalid login token is encountered during the process. |  | ||||||
|      */ |  | ||||||
|     fun postCreate( |  | ||||||
|         pageTitle: String, |  | ||||||
|         text: String, |  | ||||||
|         summary: String, |  | ||||||
|     ): Observable<Boolean> = |  | ||||||
|         try { |  | ||||||
|             pageEditInterface |  | ||||||
|                 .postCreate( |  | ||||||
|                     pageTitle, |  | ||||||
|                     summary, |  | ||||||
|                     text, |  | ||||||
|                     "text/x-wiki", |  | ||||||
|                     "wikitext", |  | ||||||
|                     true, |  | ||||||
|                     true, |  | ||||||
|                     csrfTokenClient.getTokenBlocking(), |  | ||||||
|                 ).map { editResponse -> |  | ||||||
|                     editResponse.edit()!!.editSucceeded() |  | ||||||
|                 } |  | ||||||
|         } catch (throwable: Throwable) { |  | ||||||
|             if (throwable is InvalidLoginTokenException) { |  | ||||||
|                 throw throwable |  | ||||||
|             } else { |  | ||||||
|                 Observable.just(false) |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Append text to the end of a wiki page |      * Append text to the end of a wiki page | ||||||
|  | @ -85,22 +39,14 @@ class PageEditClient( | ||||||
|      * @param summary     Edit summary |      * @param summary     Edit summary | ||||||
|      * @return whether the edit was successful |      * @return whether the edit was successful | ||||||
|      */ |      */ | ||||||
|     fun appendEdit( |     fun appendEdit(pageTitle: String, appendText: String, summary: String): Observable<Boolean> { | ||||||
|         pageTitle: String, |         return try { | ||||||
|         appendText: String, |             pageEditInterface.postAppendEdit(pageTitle, summary, appendText, csrfTokenClient.tokenBlocking) | ||||||
|         summary: String, |  | ||||||
|     ): Observable<Boolean> = |  | ||||||
|         try { |  | ||||||
|             pageEditInterface |  | ||||||
|                 .postAppendEdit(pageTitle, summary, appendText, csrfTokenClient.getTokenBlocking()) |  | ||||||
|                 .map { editResponse -> editResponse.edit()!!.editSucceeded() } |                 .map { editResponse -> editResponse.edit()!!.editSucceeded() } | ||||||
|         } catch (throwable: Throwable) { |         } catch (throwable: Throwable) { | ||||||
|             if (throwable is InvalidLoginTokenException) { |             Observable.just(false) | ||||||
|                 throw throwable |  | ||||||
|             } else { |  | ||||||
|                 Observable.just(false) |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Prepend text to the beginning of a wiki page |      * Prepend text to the beginning of a wiki page | ||||||
|  | @ -109,48 +55,14 @@ class PageEditClient( | ||||||
|      * @param summary     Edit summary |      * @param summary     Edit summary | ||||||
|      * @return whether the edit was successful |      * @return whether the edit was successful | ||||||
|      */ |      */ | ||||||
|     fun prependEdit( |     fun prependEdit(pageTitle: String, prependText: String, summary: String): Observable<Boolean> { | ||||||
|         pageTitle: String, |         return try { | ||||||
|         prependText: String, |             pageEditInterface.postPrependEdit(pageTitle, summary, prependText, csrfTokenClient.tokenBlocking) | ||||||
|         summary: String, |  | ||||||
|     ): Observable<Boolean> = |  | ||||||
|         try { |  | ||||||
|             pageEditInterface |  | ||||||
|                 .postPrependEdit(pageTitle, summary, prependText, csrfTokenClient.getTokenBlocking()) |  | ||||||
|                 .map { editResponse -> editResponse.edit()?.editSucceeded() ?: false } |  | ||||||
|         } catch (throwable: Throwable) { |  | ||||||
|             if (throwable is InvalidLoginTokenException) { |  | ||||||
|                 throw throwable |  | ||||||
|             } else { |  | ||||||
|                 Observable.just(false) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Appends a new section to the wiki page |  | ||||||
|      * @param pageTitle   Title of the page to edit |  | ||||||
|      * @param sectionTitle Title of the new section that needs to be created |  | ||||||
|      * @param sectionText  The page content that is to be added to the section |  | ||||||
|      * @param summary     Edit summary |  | ||||||
|      * @return whether the edit was successful |  | ||||||
|      */ |  | ||||||
|     fun createNewSection( |  | ||||||
|         pageTitle: String, |  | ||||||
|         sectionTitle: String, |  | ||||||
|         sectionText: String, |  | ||||||
|         summary: String, |  | ||||||
|     ): Observable<Boolean> = |  | ||||||
|         try { |  | ||||||
|             pageEditInterface |  | ||||||
|                 .postNewSection(pageTitle, summary, sectionTitle, sectionText, csrfTokenClient.getTokenBlocking()) |  | ||||||
|                 .map { editResponse -> editResponse.edit()!!.editSucceeded() } |                 .map { editResponse -> editResponse.edit()!!.editSucceeded() } | ||||||
|         } catch (throwable: Throwable) { |         } catch (throwable: Throwable) { | ||||||
|             if (throwable is InvalidLoginTokenException) { |             Observable.just(false) | ||||||
|                 throw throwable |  | ||||||
|             } else { |  | ||||||
|                 Observable.just(false) |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Set new labels to Wikibase server of commons |      * Set new labels to Wikibase server of commons | ||||||
|  | @ -160,42 +72,24 @@ class PageEditClient( | ||||||
|      * @param value label |      * @param value label | ||||||
|      * @return 1 when the edit was successful |      * @return 1 when the edit was successful | ||||||
|      */ |      */ | ||||||
|     fun setCaptions( |     fun setCaptions(summary: String, title: String, | ||||||
|         summary: String, |                     language: String, value: String) : Observable<Int>{ | ||||||
|         title: String, |         return try { | ||||||
|         language: String, |             pageEditInterface.postCaptions(summary, title, language, | ||||||
|         value: String, |                 value, csrfTokenClient.tokenBlocking).map { it.success } | ||||||
|     ): Observable<Int> = |  | ||||||
|         try { |  | ||||||
|             pageEditInterface |  | ||||||
|                 .postCaptions( |  | ||||||
|                     summary, |  | ||||||
|                     title, |  | ||||||
|                     language, |  | ||||||
|                     value, |  | ||||||
|                     csrfTokenClient.getTokenBlocking(), |  | ||||||
|                 ).map { it.success } |  | ||||||
|         } catch (throwable: Throwable) { |         } catch (throwable: Throwable) { | ||||||
|             if (throwable is InvalidLoginTokenException) { |             Observable.just(0) | ||||||
|                 throw throwable |  | ||||||
|             } else { |  | ||||||
|                 Observable.just(0) |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Get whole WikiText of required file |      * Get whole WikiText of required file | ||||||
|      * @param title : Name of the file |      * @param title : Name of the file | ||||||
|      * @return Observable<MwQueryResult> |      * @return Observable<MwQueryResult> | ||||||
|      */ |      */ | ||||||
|     fun getCurrentWikiText(title: String): Single<String?> = |     fun getCurrentWikiText(title: String): Single<String?> { | ||||||
|         pageEditInterface.getWikiText(title).map { |         return pageEditInterface.getWikiText(title).map { | ||||||
|             it |             it.query()?.pages()?.get(0)?.revisions()?.get(0)?.content() | ||||||
|                 .query() |  | ||||||
|                 ?.pages() |  | ||||||
|                 ?.get(0) |  | ||||||
|                 ?.revisions() |  | ||||||
|                 ?.get(0) |  | ||||||
|                 ?.content() |  | ||||||
|         } |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | @ -1,17 +1,12 @@ | ||||||
| package fr.free.nrw.commons.actions | package fr.free.nrw.commons.actions | ||||||
| 
 | 
 | ||||||
| import fr.free.nrw.commons.wikidata.WikidataConstants.MW_API_PREFIX |  | ||||||
| import fr.free.nrw.commons.wikidata.model.Entities |  | ||||||
| import fr.free.nrw.commons.wikidata.model.edit.Edit |  | ||||||
| import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse |  | ||||||
| import io.reactivex.Observable | import io.reactivex.Observable | ||||||
| import io.reactivex.Single | import io.reactivex.Single | ||||||
| import retrofit2.http.Field | import org.wikipedia.dataclient.Service | ||||||
| import retrofit2.http.FormUrlEncoded | import org.wikipedia.dataclient.mwapi.MwQueryResponse | ||||||
| import retrofit2.http.GET | import org.wikipedia.edit.Edit | ||||||
| import retrofit2.http.Headers | import org.wikipedia.wikidata.Entities | ||||||
| import retrofit2.http.POST | import retrofit2.http.* | ||||||
| import retrofit2.http.Query |  | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * This interface facilitates wiki commons page editing services to the Networking module |  * This interface facilitates wiki commons page editing services to the Networking module | ||||||
|  | @ -32,40 +27,13 @@ interface PageEditInterface { | ||||||
|      */ |      */ | ||||||
|     @FormUrlEncoded |     @FormUrlEncoded | ||||||
|     @Headers("Cache-Control: no-cache") |     @Headers("Cache-Control: no-cache") | ||||||
|     @POST(MW_API_PREFIX + "action=edit") |     @POST(Service.MW_API_PREFIX + "action=edit") | ||||||
|     fun postEdit( |     fun postEdit( | ||||||
|         @Field("title") title: String, |         @Field("title") title: String, | ||||||
|         @Field("summary") summary: String, |         @Field("summary") summary: String, | ||||||
|         @Field("text") text: String, |         @Field("text") text: String, | ||||||
|         // NOTE: This csrf shold always be sent as the last field of form data |         // NOTE: This csrf shold always be sent as the last field of form data | ||||||
|         @Field("token") token: String, |         @Field("token") token: String | ||||||
|     ): Observable<Edit> |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * This method creates or edits a page for nearby items. |  | ||||||
|      * |  | ||||||
|      * @param title           Title of the page to edit. Cannot be used together with pageid. |  | ||||||
|      * @param summary         Edit summary. Also used as the section title when section=new and sectiontitle is not set. |  | ||||||
|      * @param text            Text of the page. |  | ||||||
|      * @param contentformat   Format of the content (e.g., "text/x-wiki"). |  | ||||||
|      * @param contentmodel    Model of the content (e.g., "wikitext"). |  | ||||||
|      * @param minor           Whether the edit is a minor edit. |  | ||||||
|      * @param recreate        Whether to recreate the page if it does not exist. |  | ||||||
|      * @param token           A "csrf" token. This should always be sent as the last field of form data. |  | ||||||
|      */ |  | ||||||
|     @FormUrlEncoded |  | ||||||
|     @Headers("Cache-Control: no-cache") |  | ||||||
|     @POST(MW_API_PREFIX + "action=edit") |  | ||||||
|     fun postCreate( |  | ||||||
|         @Field("title") title: String, |  | ||||||
|         @Field("summary") summary: String, |  | ||||||
|         @Field("text") text: String, |  | ||||||
|         @Field("contentformat") contentformat: String, |  | ||||||
|         @Field("contentmodel") contentmodel: String, |  | ||||||
|         @Field("minor") minor: Boolean, |  | ||||||
|         @Field("recreate") recreate: Boolean, |  | ||||||
|         // NOTE: This csrf shold always be sent as the last field of form data |  | ||||||
|         @Field("token") token: String, |  | ||||||
|     ): Observable<Edit> |     ): Observable<Edit> | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -79,12 +47,12 @@ interface PageEditInterface { | ||||||
|      */ |      */ | ||||||
|     @FormUrlEncoded |     @FormUrlEncoded | ||||||
|     @Headers("Cache-Control: no-cache") |     @Headers("Cache-Control: no-cache") | ||||||
|     @POST(MW_API_PREFIX + "action=edit") |     @POST(Service.MW_API_PREFIX + "action=edit") | ||||||
|     fun postAppendEdit( |     fun postAppendEdit( | ||||||
|         @Field("title") title: String, |         @Field("title") title: String, | ||||||
|         @Field("summary") summary: String, |         @Field("summary") summary: String, | ||||||
|         @Field("appendtext") appendText: String, |         @Field("appendtext") appendText: String, | ||||||
|         @Field("token") token: String, |         @Field("token") token: String | ||||||
|     ): Observable<Edit> |     ): Observable<Edit> | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -98,44 +66,36 @@ interface PageEditInterface { | ||||||
|      */ |      */ | ||||||
|     @FormUrlEncoded |     @FormUrlEncoded | ||||||
|     @Headers("Cache-Control: no-cache") |     @Headers("Cache-Control: no-cache") | ||||||
|     @POST(MW_API_PREFIX + "action=edit") |     @POST(Service.MW_API_PREFIX + "action=edit") | ||||||
|     fun postPrependEdit( |     fun postPrependEdit( | ||||||
|         @Field("title") title: String, |         @Field("title") title: String, | ||||||
|         @Field("summary") summary: String, |         @Field("summary") summary: String, | ||||||
|         @Field("prependtext") prependText: String, |         @Field("prependtext") prependText: String, | ||||||
|         @Field("token") token: String, |         @Field("token") token: String | ||||||
|     ): Observable<Edit> |     ): Observable<Edit> | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|     @FormUrlEncoded |     @FormUrlEncoded | ||||||
|     @Headers("Cache-Control: no-cache") |     @Headers("Cache-Control: no-cache") | ||||||
|     @POST(MW_API_PREFIX + "action=edit§ion=new") |     @POST(Service.MW_API_PREFIX + "action=wbsetlabel&format=json&site=commonswiki&formatversion=2") | ||||||
|     fun postNewSection( |  | ||||||
|         @Field("title") title: String, |  | ||||||
|         @Field("summary") summary: String, |  | ||||||
|         @Field("sectiontitle") sectionTitle: String, |  | ||||||
|         @Field("text") sectionText: String, |  | ||||||
|         @Field("token") token: String, |  | ||||||
|     ): Observable<Edit> |  | ||||||
| 
 |  | ||||||
|     @FormUrlEncoded |  | ||||||
|     @Headers("Cache-Control: no-cache") |  | ||||||
|     @POST(MW_API_PREFIX + "action=wbsetlabel&format=json&site=commonswiki&formatversion=2") |  | ||||||
|     fun postCaptions( |     fun postCaptions( | ||||||
|         @Field("summary") summary: String, |         @Field("summary") summary: String, | ||||||
|         @Field("title") title: String, |         @Field("title") title: String, | ||||||
|         @Field("language") language: String, |         @Field("language") language: String, | ||||||
|         @Field("value") value: String, |         @Field("value") value: String, | ||||||
|         @Field("token") token: String, |         @Field("token") token: String | ||||||
|     ): Observable<Entities> |     ): Observable<Entities> | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Gets the wiki text for the provided file name. |      * Get wiki text for provided file names | ||||||
|      * |      * @param titles : Name of the file | ||||||
|      * @param title The title (name) of the file to fetch wiki text for. |      * @return Single<MwQueryResult> | ||||||
|      * @return A Single emitting the wiki query response. |  | ||||||
|      */ |      */ | ||||||
|     @GET(MW_API_PREFIX + "action=query&prop=revisions&rvprop=content|timestamp&rvlimit=1&converttitles=") |     @GET( | ||||||
|  |         Service.MW_API_PREFIX + | ||||||
|  |                 "action=query&prop=revisions&rvprop=content|timestamp&rvlimit=1&converttitles=" | ||||||
|  |     ) | ||||||
|     fun getWikiText( |     fun getWikiText( | ||||||
|         @Query("titles") title: String, |         @Query("titles") title: String | ||||||
|     ): Single<MwQueryResponse?> |     ): Single<MwQueryResponse?> | ||||||
| } | } | ||||||
|  | @ -1,10 +1,11 @@ | ||||||
| package fr.free.nrw.commons.actions | package fr.free.nrw.commons.actions | ||||||
| 
 | 
 | ||||||
| import fr.free.nrw.commons.CommonsApplication | import fr.free.nrw.commons.CommonsApplication | ||||||
| import fr.free.nrw.commons.auth.csrf.CsrfTokenClient | import fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_CSRF | ||||||
| import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException |  | ||||||
| import fr.free.nrw.commons.di.NetworkingModule.Companion.NAMED_COMMONS_CSRF |  | ||||||
| import io.reactivex.Observable | import io.reactivex.Observable | ||||||
|  | import org.wikipedia.csrf.CsrfTokenClient | ||||||
|  | import org.wikipedia.dataclient.Service | ||||||
|  | import org.wikipedia.dataclient.mwapi.MwPostResponse | ||||||
| import javax.inject.Inject | import javax.inject.Inject | ||||||
| import javax.inject.Named | import javax.inject.Named | ||||||
| import javax.inject.Singleton | import javax.inject.Singleton | ||||||
|  | @ -14,33 +15,22 @@ import javax.inject.Singleton | ||||||
|  * Thanks are used by a user to show gratitude to another user for their contributions |  * Thanks are used by a user to show gratitude to another user for their contributions | ||||||
|  */ |  */ | ||||||
| @Singleton | @Singleton | ||||||
| class ThanksClient | class ThanksClient @Inject constructor( | ||||||
|     @Inject |     @param:Named(NAMED_COMMONS_CSRF) private val csrfTokenClient: CsrfTokenClient, | ||||||
|     constructor( |     @param:Named("commons-service") private val service: Service | ||||||
|         @param:Named(NAMED_COMMONS_CSRF) private val csrfTokenClient: CsrfTokenClient, | ) { | ||||||
|         private val service: ThanksInterface, |     /** | ||||||
|     ) { |      * Thanks a user for a particular revision | ||||||
|         /** |      * @param revisionId The revision ID the user would like to thank someone for | ||||||
|          * Thanks a user for a particular revision |      * @return if thanks was successfully sent to intended recipient | ||||||
|          * @param revisionId The revision ID the user would like to thank someone for |      */ | ||||||
|          * @return if thanks was successfully sent to intended recipient |     fun thank(revisionId: Long): Observable<Boolean> { | ||||||
|          */ |         return try { | ||||||
|         fun thank(revisionId: Long): Observable<Boolean> = |             service.thank(revisionId.toString(), null, csrfTokenClient.tokenBlocking, CommonsApplication.getInstance().userAgent) | ||||||
|             try { |                 .map { mwThankPostResponse -> mwThankPostResponse.result.success== 1 } | ||||||
|                 service |         } catch (throwable: Throwable) { | ||||||
|                     .thank( |             Observable.just(false) | ||||||
|                         revisionId.toString(), // Rev |         } | ||||||
|                         null, // Log |  | ||||||
|                         csrfTokenClient.getTokenBlocking(), // Token |  | ||||||
|                         CommonsApplication.instance.userAgent, // Source |  | ||||||
|                     ).map { mwThankPostResponse -> |  | ||||||
|                         mwThankPostResponse.result?.success == 1 |  | ||||||
|                     } |  | ||||||
|             } catch (throwable: Throwable) { |  | ||||||
|                 if (throwable is InvalidLoginTokenException) { |  | ||||||
|                     Observable.error(throwable) |  | ||||||
|                 } else { |  | ||||||
|                     Observable.just(false) |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -1,24 +0,0 @@ | ||||||
| package fr.free.nrw.commons.actions |  | ||||||
| 
 |  | ||||||
| import fr.free.nrw.commons.wikidata.WikidataConstants.MW_API_PREFIX |  | ||||||
| import io.reactivex.Observable |  | ||||||
| import retrofit2.http.Field |  | ||||||
| import retrofit2.http.FormUrlEncoded |  | ||||||
| import retrofit2.http.POST |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Thanks API. |  | ||||||
|  * Context: |  | ||||||
|  * The Commons Android app lets you thank another contributor who has uploaded a great picture. |  | ||||||
|  * See https://www.mediawiki.org/wiki/Extension:Thanks |  | ||||||
|  */ |  | ||||||
| interface ThanksInterface { |  | ||||||
|     @FormUrlEncoded |  | ||||||
|     @POST(MW_API_PREFIX + "action=thank") |  | ||||||
|     fun thank( |  | ||||||
|         @Field("rev") rev: String?, |  | ||||||
|         @Field("log") log: String?, |  | ||||||
|         @Field("token") token: String, |  | ||||||
|         @Field("source") source: String?, |  | ||||||
|     ): Observable<MwThankPostResponse?> |  | ||||||
| } |  | ||||||
|  | @ -1,218 +0,0 @@ | ||||||
| package fr.free.nrw.commons.activity |  | ||||||
| 
 |  | ||||||
| import android.annotation.SuppressLint |  | ||||||
| import android.content.Context |  | ||||||
| import android.content.Intent |  | ||||||
| import android.os.Bundle |  | ||||||
| import android.webkit.ConsoleMessage |  | ||||||
| import android.webkit.CookieManager |  | ||||||
| import android.webkit.WebChromeClient |  | ||||||
| import android.webkit.WebResourceRequest |  | ||||||
| import android.webkit.WebView |  | ||||||
| import android.webkit.WebViewClient |  | ||||||
| import androidx.activity.ComponentActivity |  | ||||||
| import androidx.activity.compose.setContent |  | ||||||
| import androidx.activity.enableEdgeToEdge |  | ||||||
| import androidx.compose.foundation.layout.fillMaxSize |  | ||||||
| import androidx.compose.foundation.layout.padding |  | ||||||
| import androidx.compose.material.icons.Icons |  | ||||||
| import androidx.compose.material.icons.automirrored.filled.ArrowBack |  | ||||||
| import androidx.compose.material3.ExperimentalMaterial3Api |  | ||||||
| import androidx.compose.material3.Icon |  | ||||||
| import androidx.compose.material3.IconButton |  | ||||||
| import androidx.compose.material3.Scaffold |  | ||||||
| import androidx.compose.material3.Text |  | ||||||
| import androidx.compose.material3.TopAppBar |  | ||||||
| import androidx.compose.runtime.Composable |  | ||||||
| import androidx.compose.runtime.mutableStateOf |  | ||||||
| import androidx.compose.runtime.remember |  | ||||||
| import androidx.compose.ui.Modifier |  | ||||||
| import androidx.compose.ui.viewinterop.AndroidView |  | ||||||
| import fr.free.nrw.commons.CommonsApplication |  | ||||||
| import fr.free.nrw.commons.CommonsApplication.ActivityLogoutListener |  | ||||||
| import fr.free.nrw.commons.R |  | ||||||
| import fr.free.nrw.commons.di.ApplicationlessInjection |  | ||||||
| import fr.free.nrw.commons.wikidata.cookies.CommonsCookieJar |  | ||||||
| import okhttp3.HttpUrl.Companion.toHttpUrl |  | ||||||
| import timber.log.Timber |  | ||||||
| import javax.inject.Inject |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * SingleWebViewActivity is a reusable activity webView based on a given url(initial url) and |  | ||||||
|  * closes itself when a specified success URL is reached to success url. |  | ||||||
|  */ |  | ||||||
| class SingleWebViewActivity : ComponentActivity() { |  | ||||||
|     @Inject |  | ||||||
|     lateinit var cookieJar: CommonsCookieJar |  | ||||||
| 
 |  | ||||||
|     @OptIn(ExperimentalMaterial3Api::class) |  | ||||||
|     override fun onCreate(savedInstanceState: Bundle?) { |  | ||||||
|         super.onCreate(savedInstanceState) |  | ||||||
|         val url = intent.getStringExtra(VANISH_ACCOUNT_URL) |  | ||||||
|         val successUrl = intent.getStringExtra(VANISH_ACCOUNT_SUCCESS_URL) |  | ||||||
|         if (url == null || successUrl == null) { |  | ||||||
|             finish() |  | ||||||
|             return |  | ||||||
|         } |  | ||||||
|         ApplicationlessInjection |  | ||||||
|             .getInstance(applicationContext) |  | ||||||
|             .commonsApplicationComponent |  | ||||||
|             .inject(this) |  | ||||||
|         setCookies(url) |  | ||||||
|         enableEdgeToEdge() |  | ||||||
|         setContent { |  | ||||||
|             Scaffold( |  | ||||||
|                 topBar = { |  | ||||||
|                     TopAppBar( |  | ||||||
|                         modifier = Modifier, |  | ||||||
|                         title = { Text(getString(R.string.vanish_account)) }, |  | ||||||
|                         navigationIcon = { |  | ||||||
|                             IconButton( |  | ||||||
|                                 onClick = { |  | ||||||
|                                     // Close the WebView Activity if the user taps the back button |  | ||||||
|                                     finish() |  | ||||||
|                                 }, |  | ||||||
|                             ) { |  | ||||||
|                                 Icon( |  | ||||||
|                                     imageVector = Icons.AutoMirrored.Filled.ArrowBack, |  | ||||||
|                                     // TODO("Add contentDescription) |  | ||||||
|                                     contentDescription = "" |  | ||||||
|                                 ) |  | ||||||
|                             } |  | ||||||
|                         } |  | ||||||
|                     ) |  | ||||||
|                 }, |  | ||||||
|                 content = { |  | ||||||
|                     WebViewComponent( |  | ||||||
|                         url = url, |  | ||||||
|                         successUrl = successUrl, |  | ||||||
|                         onSuccess = { |  | ||||||
|                             //Redirect the user to login screen like we do when the user logout's |  | ||||||
|                             val app = applicationContext as CommonsApplication |  | ||||||
|                             app.clearApplicationData( |  | ||||||
|                                 applicationContext, |  | ||||||
|                                 ActivityLogoutListener(activity = this, ctx = applicationContext) |  | ||||||
|                             ) |  | ||||||
|                             finish() |  | ||||||
|                         }, |  | ||||||
|                         modifier = Modifier |  | ||||||
|                             .fillMaxSize() |  | ||||||
|                             .padding(it) |  | ||||||
|                     ) |  | ||||||
|                 } |  | ||||||
|             ) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * @param url The initial URL which we are loading in the WebView. |  | ||||||
|      * @param successUrl The URL that, when reached, triggers the `onSuccess` callback. |  | ||||||
|      * @param onSuccess A callback that is invoked when the current url of webView is successUrl. |  | ||||||
|      * This is used when we want to close when the webView once a success url is hit. |  | ||||||
|      * @param modifier An optional [Modifier] to customize the layout or appearance of the WebView. |  | ||||||
|      */ |  | ||||||
|     @SuppressLint("SetJavaScriptEnabled") |  | ||||||
|     @Composable |  | ||||||
|     private fun WebViewComponent( |  | ||||||
|         url: String, |  | ||||||
|         successUrl: String, |  | ||||||
|         onSuccess: () -> Unit, |  | ||||||
|         modifier: Modifier = Modifier |  | ||||||
|     ) { |  | ||||||
|         val webView = remember { mutableStateOf<WebView?>(null) } |  | ||||||
|         AndroidView( |  | ||||||
|             modifier = modifier, |  | ||||||
|             factory = { |  | ||||||
|                 WebView(it).apply { |  | ||||||
|                     settings.apply { |  | ||||||
|                         javaScriptEnabled = true |  | ||||||
|                         domStorageEnabled = true |  | ||||||
|                         javaScriptCanOpenWindowsAutomatically = true |  | ||||||
| 
 |  | ||||||
|                     } |  | ||||||
|                     webViewClient = object : WebViewClient() { |  | ||||||
|                         override fun shouldOverrideUrlLoading( |  | ||||||
|                             view: WebView?, |  | ||||||
|                             request: WebResourceRequest? |  | ||||||
|                         ): Boolean { |  | ||||||
| 
 |  | ||||||
|                             request?.url?.let { url -> |  | ||||||
|                                 Timber.d("URL Loading: $url") |  | ||||||
|                                 if (url.toString() == successUrl) { |  | ||||||
|                                     Timber.d("Success URL detected. Closing WebView.") |  | ||||||
|                                     onSuccess() // Close the activity |  | ||||||
|                                     return true |  | ||||||
|                                 } |  | ||||||
|                                 return false |  | ||||||
|                             } |  | ||||||
|                             return false |  | ||||||
|                         } |  | ||||||
| 
 |  | ||||||
|                         override fun onPageFinished(view: WebView?, url: String?) { |  | ||||||
|                             super.onPageFinished(view, url) |  | ||||||
|                             setCookies(url.orEmpty()) |  | ||||||
|                         } |  | ||||||
| 
 |  | ||||||
|                     } |  | ||||||
| 
 |  | ||||||
|                     webChromeClient = object : WebChromeClient() { |  | ||||||
|                         override fun onConsoleMessage(message: ConsoleMessage): Boolean { |  | ||||||
|                             Timber.d("%s%s", |  | ||||||
|                                 "Console: ${message.message()} -- From line ", |  | ||||||
|                                 "${message.lineNumber()} of ${message.sourceId()}") |  | ||||||
|                             return true |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
| 
 |  | ||||||
|                     loadUrl(url) |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             update = { |  | ||||||
|                 webView.value = it |  | ||||||
|             } |  | ||||||
|         ) |  | ||||||
| 
 |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Sets cookies for the given URL using the cookies stored in the `CommonsCookieJar`. |  | ||||||
|      * |  | ||||||
|      * @param url The URL for which cookies need to be set. |  | ||||||
|      */ |  | ||||||
|     private fun setCookies(url: String) { |  | ||||||
|         CookieManager.getInstance().let { |  | ||||||
|             val cookies = cookieJar.loadForRequest(url.toHttpUrl()) |  | ||||||
|             for (cookie in cookies) { |  | ||||||
|                 it.setCookie(url, cookie.toString()) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     companion object { |  | ||||||
|         private const val VANISH_ACCOUNT_URL = "VanishAccountUrl" |  | ||||||
|         private const val VANISH_ACCOUNT_SUCCESS_URL = "vanishAccountSuccessUrl" |  | ||||||
| 
 |  | ||||||
|         /** |  | ||||||
|          * Launch the WebViewActivity with the specified URL and success URL. |  | ||||||
|          * @param context The context from which the activity is launched. |  | ||||||
|          * @param url The initial URL to load in the WebView. |  | ||||||
|          * @param successUrl The URL that triggers the WebView to close when matched. |  | ||||||
|          */ |  | ||||||
|         fun showWebView( |  | ||||||
|             context: Context, |  | ||||||
|             url: String, |  | ||||||
|             successUrl: String |  | ||||||
|         ) { |  | ||||||
|             val intent = Intent( |  | ||||||
|                 context, |  | ||||||
|                 SingleWebViewActivity::class.java |  | ||||||
|             ).apply { |  | ||||||
|                 putExtra(VANISH_ACCOUNT_URL, url) |  | ||||||
|                 putExtra(VANISH_ACCOUNT_SUCCESS_URL, successUrl) |  | ||||||
|             } |  | ||||||
|             context.startActivity(intent) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
							
								
								
									
										44
									
								
								app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.java
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.java
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,44 @@ | ||||||
|  | package fr.free.nrw.commons.auth; | ||||||
|  | 
 | ||||||
|  | import android.accounts.Account; | ||||||
|  | import android.accounts.AccountManager; | ||||||
|  | import android.content.Context; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.Nullable; | ||||||
|  | 
 | ||||||
|  | import fr.free.nrw.commons.BuildConfig; | ||||||
|  | import timber.log.Timber; | ||||||
|  | 
 | ||||||
|  | public class AccountUtil { | ||||||
|  | 
 | ||||||
|  |     public static final String AUTH_TOKEN_TYPE = "CommonsAndroid"; | ||||||
|  | 
 | ||||||
|  |     public AccountUtil() { | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * @return Account|null | ||||||
|  |      */ | ||||||
|  |     @Nullable | ||||||
|  |     public static Account account(Context context) { | ||||||
|  |         try { | ||||||
|  |             Account[] accounts = accountManager(context).getAccountsByType(BuildConfig.ACCOUNT_TYPE); | ||||||
|  |             if (accounts.length > 0) { | ||||||
|  |                 return accounts[0]; | ||||||
|  |             } | ||||||
|  |         } catch (SecurityException e) { | ||||||
|  |             Timber.e(e); | ||||||
|  |         } | ||||||
|  |         return null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Nullable | ||||||
|  |     public static String getUserName(Context context) { | ||||||
|  |         Account account = account(context); | ||||||
|  |         return account == null ? null : account.name; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static AccountManager accountManager(Context context) { | ||||||
|  |         return AccountManager.get(context); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,24 +0,0 @@ | ||||||
| package fr.free.nrw.commons.auth |  | ||||||
| 
 |  | ||||||
| import android.accounts.Account |  | ||||||
| import android.accounts.AccountManager |  | ||||||
| import android.content.Context |  | ||||||
| import androidx.annotation.VisibleForTesting |  | ||||||
| import fr.free.nrw.commons.BuildConfig.ACCOUNT_TYPE |  | ||||||
| import timber.log.Timber |  | ||||||
| 
 |  | ||||||
| const val AUTH_TOKEN_TYPE: String = "CommonsAndroid" |  | ||||||
| 
 |  | ||||||
| fun getUserName(context: Context): String? { |  | ||||||
|     return account(context)?.name |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| @VisibleForTesting |  | ||||||
| fun account(context: Context): Account? = try { |  | ||||||
|     val accountManager = AccountManager.get(context) |  | ||||||
|     val accounts = accountManager.getAccountsByType(ACCOUNT_TYPE) |  | ||||||
|     if (accounts.isNotEmpty()) accounts[0] else null |  | ||||||
| } catch (e: SecurityException) { |  | ||||||
|     Timber.e(e) |  | ||||||
|     null |  | ||||||
| } |  | ||||||
							
								
								
									
										496
									
								
								app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										496
									
								
								app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,496 @@ | ||||||
|  | package fr.free.nrw.commons.auth; | ||||||
|  | 
 | ||||||
|  | import android.accounts.AccountAuthenticatorActivity; | ||||||
|  | import android.app.ProgressDialog; | ||||||
|  | import android.content.Context; | ||||||
|  | import android.content.Intent; | ||||||
|  | import android.net.Uri; | ||||||
|  | import android.os.Bundle; | ||||||
|  | import android.text.Editable; | ||||||
|  | import android.text.TextWatcher; | ||||||
|  | import android.view.KeyEvent; | ||||||
|  | import android.view.MenuInflater; | ||||||
|  | import android.view.MenuItem; | ||||||
|  | import android.view.View; | ||||||
|  | import android.view.ViewGroup; | ||||||
|  | import android.view.inputmethod.InputMethodManager; | ||||||
|  | import android.widget.Button; | ||||||
|  | import android.widget.EditText; | ||||||
|  | import android.widget.TextView; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.ColorRes; | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.annotation.Nullable; | ||||||
|  | import androidx.annotation.StringRes; | ||||||
|  | import androidx.appcompat.app.AlertDialog; | ||||||
|  | import androidx.appcompat.app.AppCompatDelegate; | ||||||
|  | import androidx.core.app.NavUtils; | ||||||
|  | import androidx.core.content.ContextCompat; | ||||||
|  | 
 | ||||||
|  | import com.google.android.material.textfield.TextInputLayout; | ||||||
|  | 
 | ||||||
|  | import fr.free.nrw.commons.utils.ActivityUtils; | ||||||
|  | import java.util.Locale; | ||||||
|  | import org.wikipedia.AppAdapter; | ||||||
|  | import org.wikipedia.dataclient.ServiceFactory; | ||||||
|  | import org.wikipedia.dataclient.WikiSite; | ||||||
|  | import org.wikipedia.dataclient.mwapi.MwQueryResponse; | ||||||
|  | import org.wikipedia.login.LoginClient; | ||||||
|  | import org.wikipedia.login.LoginClient.LoginCallback; | ||||||
|  | import org.wikipedia.login.LoginResult; | ||||||
|  | 
 | ||||||
|  | import javax.inject.Inject; | ||||||
|  | import javax.inject.Named; | ||||||
|  | 
 | ||||||
|  | import butterknife.BindView; | ||||||
|  | import butterknife.ButterKnife; | ||||||
|  | import butterknife.OnClick; | ||||||
|  | import butterknife.OnEditorAction; | ||||||
|  | import butterknife.OnFocusChange; | ||||||
|  | import fr.free.nrw.commons.BuildConfig; | ||||||
|  | import fr.free.nrw.commons.R; | ||||||
|  | import fr.free.nrw.commons.Utils; | ||||||
|  | import fr.free.nrw.commons.WelcomeActivity; | ||||||
|  | import fr.free.nrw.commons.contributions.MainActivity; | ||||||
|  | import fr.free.nrw.commons.di.ApplicationlessInjection; | ||||||
|  | import fr.free.nrw.commons.kvstore.JsonKvStore; | ||||||
|  | import fr.free.nrw.commons.utils.ConfigUtils; | ||||||
|  | import fr.free.nrw.commons.utils.SystemThemeUtils; | ||||||
|  | import fr.free.nrw.commons.utils.ViewUtil; | ||||||
|  | import io.reactivex.disposables.CompositeDisposable; | ||||||
|  | import retrofit2.Call; | ||||||
|  | import retrofit2.Callback; | ||||||
|  | import retrofit2.Response; | ||||||
|  | import timber.log.Timber; | ||||||
|  | 
 | ||||||
|  | import static android.view.KeyEvent.KEYCODE_ENTER; | ||||||
|  | import static android.view.View.VISIBLE; | ||||||
|  | import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE; | ||||||
|  | import static fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_WIKI_SITE; | ||||||
|  | 
 | ||||||
|  | public class LoginActivity extends AccountAuthenticatorActivity { | ||||||
|  | 
 | ||||||
|  |     @Inject | ||||||
|  |     SessionManager sessionManager; | ||||||
|  | 
 | ||||||
|  |     @Inject | ||||||
|  |     @Named(NAMED_COMMONS_WIKI_SITE) | ||||||
|  |     WikiSite commonsWikiSite; | ||||||
|  | 
 | ||||||
|  |     @Inject | ||||||
|  |     @Named("default_preferences") | ||||||
|  |     JsonKvStore applicationKvStore; | ||||||
|  | 
 | ||||||
|  |     @Inject | ||||||
|  |     LoginClient loginClient; | ||||||
|  | 
 | ||||||
|  |     @Inject | ||||||
|  |     SystemThemeUtils systemThemeUtils; | ||||||
|  | 
 | ||||||
|  |     @BindView(R.id.login_button) | ||||||
|  |     Button loginButton; | ||||||
|  | 
 | ||||||
|  |     @BindView(R.id.login_username) | ||||||
|  |     EditText usernameEdit; | ||||||
|  | 
 | ||||||
|  |     @BindView(R.id.login_password) | ||||||
|  |     EditText passwordEdit; | ||||||
|  | 
 | ||||||
|  |     @BindView(R.id.login_two_factor) | ||||||
|  |     EditText twoFactorEdit; | ||||||
|  | 
 | ||||||
|  |     @BindView(R.id.error_message_container) | ||||||
|  |     ViewGroup errorMessageContainer; | ||||||
|  | 
 | ||||||
|  |     @BindView(R.id.error_message) | ||||||
|  |     TextView errorMessage; | ||||||
|  | 
 | ||||||
|  |     @BindView(R.id.login_credentials) | ||||||
|  |     TextView loginCredentials; | ||||||
|  | 
 | ||||||
|  |     @BindView(R.id.two_factor_container) | ||||||
|  |     TextInputLayout twoFactorContainer; | ||||||
|  | 
 | ||||||
|  |     ProgressDialog progressDialog; | ||||||
|  |     private AppCompatDelegate delegate; | ||||||
|  |     private LoginTextWatcher textWatcher = new LoginTextWatcher(); | ||||||
|  |     private CompositeDisposable compositeDisposable = new CompositeDisposable(); | ||||||
|  |     private Call<MwQueryResponse> loginToken; | ||||||
|  |     final  String saveProgressDailog="ProgressDailog_state"; | ||||||
|  |     final String saveErrorMessage ="errorMessage"; | ||||||
|  |     final String saveUsername="username"; | ||||||
|  |     final  String savePassword="password"; | ||||||
|  |     @Override | ||||||
|  |     public void onCreate(Bundle savedInstanceState) { | ||||||
|  |         super.onCreate(savedInstanceState); | ||||||
|  |         ApplicationlessInjection | ||||||
|  |                 .getInstance(this.getApplicationContext()) | ||||||
|  |                 .getCommonsApplicationComponent() | ||||||
|  |                 .inject(this); | ||||||
|  | 
 | ||||||
|  |         boolean isDarkTheme = systemThemeUtils.isDeviceInNightMode(); | ||||||
|  |         setTheme(isDarkTheme ? R.style.DarkAppTheme : R.style.LightAppTheme); | ||||||
|  |         getDelegate().installViewFactory(); | ||||||
|  |         getDelegate().onCreate(savedInstanceState); | ||||||
|  | 
 | ||||||
|  |         setContentView(R.layout.activity_login); | ||||||
|  | 
 | ||||||
|  |         ButterKnife.bind(this); | ||||||
|  | 
 | ||||||
|  |         usernameEdit.addTextChangedListener(textWatcher); | ||||||
|  |         passwordEdit.addTextChangedListener(textWatcher); | ||||||
|  |         twoFactorEdit.addTextChangedListener(textWatcher); | ||||||
|  | 
 | ||||||
|  |         if (ConfigUtils.isBetaFlavour()) { | ||||||
|  |             loginCredentials.setText(getString(R.string.login_credential)); | ||||||
|  |         } else { | ||||||
|  |             loginCredentials.setVisibility(View.GONE); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @OnFocusChange(R.id.login_password) | ||||||
|  |     void onPasswordFocusChanged(View view, boolean hasFocus) { | ||||||
|  |         if (!hasFocus) { | ||||||
|  |             ViewUtil.hideKeyboard(view); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @OnEditorAction(R.id.login_password) | ||||||
|  |     boolean onEditorAction(int actionId, KeyEvent keyEvent) { | ||||||
|  |         if (loginButton.isEnabled()) { | ||||||
|  |             if (actionId == IME_ACTION_DONE) { | ||||||
|  |                 performLogin(); | ||||||
|  |                 return true; | ||||||
|  |             } else if ((keyEvent != null) && keyEvent.getKeyCode() == KEYCODE_ENTER) { | ||||||
|  |                 performLogin(); | ||||||
|  |                 return true; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     @OnClick(R.id.skip_login) | ||||||
|  |     void skipLogin() { | ||||||
|  |         new AlertDialog.Builder(this).setTitle(R.string.skip_login_title) | ||||||
|  |                 .setMessage(R.string.skip_login_message) | ||||||
|  |                 .setCancelable(false) | ||||||
|  |                 .setPositiveButton(R.string.yes, (dialog, which) -> { | ||||||
|  |                     dialog.cancel(); | ||||||
|  |                     performSkipLogin(); | ||||||
|  |                 }) | ||||||
|  |                 .setNegativeButton(R.string.no, (dialog, which) -> dialog.cancel()) | ||||||
|  |                 .show(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @OnClick(R.id.forgot_password) | ||||||
|  |     void forgotPassword() { | ||||||
|  |         Utils.handleWebUrl(this, Uri.parse(BuildConfig.FORGOT_PASSWORD_URL)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @OnClick(R.id.about_privacy_policy) | ||||||
|  |     void onPrivacyPolicyClicked() { | ||||||
|  |         Utils.handleWebUrl(this, Uri.parse(BuildConfig.PRIVACY_POLICY_URL)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @OnClick(R.id.sign_up_button) | ||||||
|  |     void signUp() { | ||||||
|  |         Intent intent = new Intent(this, SignupActivity.class); | ||||||
|  |         startActivity(intent); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     protected void onPostCreate(Bundle savedInstanceState) { | ||||||
|  |         super.onPostCreate(savedInstanceState); | ||||||
|  |         getDelegate().onPostCreate(savedInstanceState); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     protected void onResume() { | ||||||
|  |         super.onResume(); | ||||||
|  | 
 | ||||||
|  |         if (sessionManager.getCurrentAccount() != null | ||||||
|  |                 && sessionManager.isUserLoggedIn()) { | ||||||
|  |             applicationKvStore.putBoolean("login_skipped", false); | ||||||
|  |             startMainActivity(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (applicationKvStore.getBoolean("login_skipped", false)) { | ||||||
|  |             performSkipLogin(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     protected void onDestroy() { | ||||||
|  |         compositeDisposable.clear(); | ||||||
|  |         try { | ||||||
|  |             // To prevent leaked window when finish() is called, see http://stackoverflow.com/questions/32065854/activity-has-leaked-window-at-alertdialog-show-method | ||||||
|  |             if (progressDialog != null && progressDialog.isShowing()) { | ||||||
|  |                 progressDialog.dismiss(); | ||||||
|  |             } | ||||||
|  |         } catch (Exception e) { | ||||||
|  |             e.printStackTrace(); | ||||||
|  |         } | ||||||
|  |         usernameEdit.removeTextChangedListener(textWatcher); | ||||||
|  |         passwordEdit.removeTextChangedListener(textWatcher); | ||||||
|  |         twoFactorEdit.removeTextChangedListener(textWatcher); | ||||||
|  |         delegate.onDestroy(); | ||||||
|  |         if(null!=loginClient) { | ||||||
|  |             loginClient.cancel(); | ||||||
|  |         } | ||||||
|  |         super.onDestroy(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @OnClick(R.id.login_button) | ||||||
|  |     public void performLogin() { | ||||||
|  |         Timber.d("Login to start!"); | ||||||
|  |         final String username = usernameEdit.getText().toString(); | ||||||
|  |         final String rawUsername = usernameEdit.getText().toString().trim(); | ||||||
|  |         final String password = passwordEdit.getText().toString(); | ||||||
|  |         String twoFactorCode = twoFactorEdit.getText().toString(); | ||||||
|  | 
 | ||||||
|  |         showLoggingProgressBar(); | ||||||
|  |         doLogin(username, password, twoFactorCode); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void doLogin(String username, String password, String twoFactorCode) { | ||||||
|  |         progressDialog.show(); | ||||||
|  |         loginToken = ServiceFactory.get(commonsWikiSite).getLoginToken(); | ||||||
|  |         loginToken.enqueue( | ||||||
|  |                 new Callback<MwQueryResponse>() { | ||||||
|  |                     @Override | ||||||
|  |                     public void onResponse(Call<MwQueryResponse> call, | ||||||
|  |                                            Response<MwQueryResponse> response) { | ||||||
|  |                         loginClient.login(commonsWikiSite, username, password, null, twoFactorCode, | ||||||
|  |                                 response.body().query().loginToken(), Locale.getDefault().getLanguage(), new LoginCallback() { | ||||||
|  |                                     @Override | ||||||
|  |                                     public void success(@NonNull LoginResult result) { | ||||||
|  |                                         Timber.d("Login Success"); | ||||||
|  |                                         onLoginSuccess(result); | ||||||
|  |                                     } | ||||||
|  | 
 | ||||||
|  |                                     @Override | ||||||
|  |                                     public void twoFactorPrompt(@NonNull Throwable caught, | ||||||
|  |                                                                 @Nullable String token) { | ||||||
|  |                                         Timber.d("Requesting 2FA prompt"); | ||||||
|  |                                         hideProgress(); | ||||||
|  |                                         askUserForTwoFactorAuth(); | ||||||
|  |                                     } | ||||||
|  | 
 | ||||||
|  |                                     @Override | ||||||
|  |                                     public void passwordResetPrompt(@Nullable String token) { | ||||||
|  |                                         Timber.d("Showing password reset prompt"); | ||||||
|  |                                         hideProgress(); | ||||||
|  |                                         showPasswordResetPrompt(); | ||||||
|  |                                     } | ||||||
|  | 
 | ||||||
|  |                                     @Override | ||||||
|  |                                     public void error(@NonNull Throwable caught) { | ||||||
|  |                                         Timber.e(caught); | ||||||
|  |                                         hideProgress(); | ||||||
|  |                                         showMessageAndCancelDialog(caught.getLocalizedMessage()); | ||||||
|  |                                     } | ||||||
|  |                                 }); | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                     @Override | ||||||
|  |                     public void onFailure(Call<MwQueryResponse> call, Throwable t) { | ||||||
|  |                         Timber.e(t); | ||||||
|  |                         showMessageAndCancelDialog(t.getLocalizedMessage()); | ||||||
|  |                     } | ||||||
|  |                 }); | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void hideProgress() { | ||||||
|  |         progressDialog.dismiss(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void showPasswordResetPrompt() { | ||||||
|  |         showMessageAndCancelDialog(getString(R.string.you_must_reset_your_passsword)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * This function is called when user skips the login. | ||||||
|  |      * It redirects the user to Explore Activity. | ||||||
|  |      */ | ||||||
|  |     private void performSkipLogin() { | ||||||
|  |         applicationKvStore.putBoolean("login_skipped", true); | ||||||
|  |         MainActivity.startYourself(this); | ||||||
|  |         finish(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void showLoggingProgressBar() { | ||||||
|  |         progressDialog = new ProgressDialog(this); | ||||||
|  |         progressDialog.setIndeterminate(true); | ||||||
|  |         progressDialog.setTitle(getString(R.string.logging_in_title)); | ||||||
|  |         progressDialog.setMessage(getString(R.string.logging_in_message)); | ||||||
|  |         progressDialog.setCanceledOnTouchOutside(false); | ||||||
|  |         progressDialog.show(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void onLoginSuccess(LoginResult loginResult) { | ||||||
|  |         if (!progressDialog.isShowing()) { | ||||||
|  |             // no longer attached to activity! | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         compositeDisposable.clear(); | ||||||
|  |         sessionManager.setUserLoggedIn(true); | ||||||
|  |         AppAdapter.get().updateAccount(loginResult); | ||||||
|  |         progressDialog.dismiss(); | ||||||
|  |         showSuccessAndDismissDialog(); | ||||||
|  |         startMainActivity(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     protected void onStart() { | ||||||
|  |         super.onStart(); | ||||||
|  |         delegate.onStart(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     protected void onStop() { | ||||||
|  |         super.onStop(); | ||||||
|  |         delegate.onStop(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     protected void onPostResume() { | ||||||
|  |         super.onPostResume(); | ||||||
|  |         getDelegate().onPostResume(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void setContentView(View view, ViewGroup.LayoutParams params) { | ||||||
|  |         getDelegate().setContentView(view, params); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public boolean onOptionsItemSelected(MenuItem item) { | ||||||
|  |         switch (item.getItemId()) { | ||||||
|  |             case android.R.id.home: | ||||||
|  |                 NavUtils.navigateUpFromSameTask(this); | ||||||
|  |                 return true; | ||||||
|  |         } | ||||||
|  |         return super.onOptionsItemSelected(item); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     @NonNull | ||||||
|  |     public MenuInflater getMenuInflater() { | ||||||
|  |         return getDelegate().getMenuInflater(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void askUserForTwoFactorAuth() { | ||||||
|  |         progressDialog.dismiss(); | ||||||
|  |         twoFactorContainer.setVisibility(VISIBLE); | ||||||
|  |         twoFactorEdit.setVisibility(VISIBLE); | ||||||
|  |         twoFactorEdit.requestFocus(); | ||||||
|  |         InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); | ||||||
|  |         imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY); | ||||||
|  |         showMessageAndCancelDialog(R.string.login_failed_2fa_needed); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void showMessageAndCancelDialog(@StringRes int resId) { | ||||||
|  |         showMessage(resId, R.color.secondaryDarkColor); | ||||||
|  |         if (progressDialog != null) { | ||||||
|  |             progressDialog.cancel(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void showMessageAndCancelDialog(String error) { | ||||||
|  |         showMessage(error, R.color.secondaryDarkColor); | ||||||
|  |         if (progressDialog != null) { | ||||||
|  |             progressDialog.cancel(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void showSuccessAndDismissDialog() { | ||||||
|  |         showMessage(R.string.login_success, R.color.primaryDarkColor); | ||||||
|  |         progressDialog.dismiss(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void startMainActivity() { | ||||||
|  |         ActivityUtils.startActivityWithFlags(this, MainActivity.class, Intent.FLAG_ACTIVITY_SINGLE_TOP); | ||||||
|  |         finish(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void showMessage(@StringRes int resId, @ColorRes int colorResId) { | ||||||
|  |         errorMessage.setText(getString(resId)); | ||||||
|  |         errorMessage.setTextColor(ContextCompat.getColor(this, colorResId)); | ||||||
|  |         errorMessageContainer.setVisibility(VISIBLE); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void showMessage(String message, @ColorRes int colorResId) { | ||||||
|  |         errorMessage.setText(message); | ||||||
|  |         errorMessage.setTextColor(ContextCompat.getColor(this, colorResId)); | ||||||
|  |         errorMessageContainer.setVisibility(VISIBLE); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private AppCompatDelegate getDelegate() { | ||||||
|  |         if (delegate == null) { | ||||||
|  |             delegate = AppCompatDelegate.create(this, null); | ||||||
|  |         } | ||||||
|  |         return delegate; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private class LoginTextWatcher implements TextWatcher { | ||||||
|  |         @Override | ||||||
|  |         public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) { | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @Override | ||||||
|  |         public void onTextChanged(CharSequence charSequence, int start, int count, int after) { | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @Override | ||||||
|  |         public void afterTextChanged(Editable editable) { | ||||||
|  |             boolean enabled = usernameEdit.getText().length() != 0 && passwordEdit.getText().length() != 0 | ||||||
|  |                     && (BuildConfig.DEBUG || twoFactorEdit.getText().length() != 0 || twoFactorEdit.getVisibility() != VISIBLE); | ||||||
|  |             loginButton.setEnabled(enabled); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static void startYourself(Context context) { | ||||||
|  |         Intent intent = new Intent(context, LoginActivity.class); | ||||||
|  |         context.startActivity(intent); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     protected void onSaveInstanceState(Bundle outState) { | ||||||
|  |         // if progressDialog is visible during the configuration change  then store state as  true else false so that | ||||||
|  |         // we maintain visibility of progressDailog after configuration change | ||||||
|  |         if(progressDialog!=null&&progressDialog.isShowing()) { | ||||||
|  |             outState.putBoolean(saveProgressDailog,true); | ||||||
|  |         } else { | ||||||
|  |             outState.putBoolean(saveProgressDailog,false); | ||||||
|  |         } | ||||||
|  |         outState.putString(saveErrorMessage,errorMessage.getText().toString()); //Save the errorMessage | ||||||
|  |         outState.putString(saveUsername,getUsername()); // Save the username | ||||||
|  |         outState.putString(savePassword,getPassword()); // Save the password | ||||||
|  |     } | ||||||
|  |     private String getUsername() { | ||||||
|  |         return usernameEdit.getText().toString(); | ||||||
|  |     } | ||||||
|  |     private String getPassword(){ | ||||||
|  |         return  passwordEdit.getText().toString(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     protected void onRestoreInstanceState(final Bundle savedInstanceState) { | ||||||
|  |         super.onRestoreInstanceState(savedInstanceState); | ||||||
|  |         usernameEdit.setText(savedInstanceState.getString(saveUsername)); | ||||||
|  |         passwordEdit.setText(savedInstanceState.getString(savePassword)); | ||||||
|  |         if(savedInstanceState.getBoolean(saveProgressDailog)) { | ||||||
|  |             performLogin(); | ||||||
|  |         } | ||||||
|  |         String errorMessage=savedInstanceState.getString(saveErrorMessage); | ||||||
|  |         if(sessionManager.isUserLoggedIn()) { | ||||||
|  |             showMessage(R.string.login_success, R.color.primaryDarkColor); | ||||||
|  |         } else { | ||||||
|  |             showMessage(errorMessage, R.color.secondaryDarkColor); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,489 +0,0 @@ | ||||||
| package fr.free.nrw.commons.auth |  | ||||||
| 
 |  | ||||||
| import android.accounts.AccountAuthenticatorActivity |  | ||||||
| import android.app.ProgressDialog |  | ||||||
| import android.content.Context |  | ||||||
| import android.content.DialogInterface |  | ||||||
| import android.content.Intent |  | ||||||
| import android.net.Uri |  | ||||||
| import android.os.Bundle |  | ||||||
| import android.view.KeyEvent |  | ||||||
| import android.view.MenuInflater |  | ||||||
| import android.view.MenuItem |  | ||||||
| import android.view.View |  | ||||||
| import android.view.ViewGroup |  | ||||||
| import android.view.inputmethod.EditorInfo |  | ||||||
| import android.view.inputmethod.InputMethodManager |  | ||||||
| import android.widget.TextView |  | ||||||
| import androidx.annotation.ColorRes |  | ||||||
| import androidx.annotation.StringRes |  | ||||||
| import androidx.annotation.VisibleForTesting |  | ||||||
| import androidx.appcompat.app.AlertDialog |  | ||||||
| import androidx.appcompat.app.AppCompatDelegate |  | ||||||
| import androidx.core.app.NavUtils |  | ||||||
| import androidx.core.content.ContextCompat |  | ||||||
| import androidx.core.view.WindowCompat |  | ||||||
| import fr.free.nrw.commons.BuildConfig |  | ||||||
| import fr.free.nrw.commons.CommonsApplication |  | ||||||
| import fr.free.nrw.commons.R |  | ||||||
| import fr.free.nrw.commons.auth.login.LoginCallback |  | ||||||
| import fr.free.nrw.commons.auth.login.LoginClient |  | ||||||
| import fr.free.nrw.commons.auth.login.LoginResult |  | ||||||
| import fr.free.nrw.commons.contributions.MainActivity |  | ||||||
| import fr.free.nrw.commons.databinding.ActivityLoginBinding |  | ||||||
| import fr.free.nrw.commons.di.ApplicationlessInjection |  | ||||||
| import fr.free.nrw.commons.kvstore.JsonKvStore |  | ||||||
| import fr.free.nrw.commons.utils.applyEdgeToEdgeAllInsets |  | ||||||
| import fr.free.nrw.commons.utils.AbstractTextWatcher |  | ||||||
| import fr.free.nrw.commons.utils.ActivityUtils.startActivityWithFlags |  | ||||||
| import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour |  | ||||||
| import fr.free.nrw.commons.utils.SystemThemeUtils |  | ||||||
| import fr.free.nrw.commons.utils.ViewUtil.hideKeyboard |  | ||||||
| import fr.free.nrw.commons.utils.handleKeyboardInsets |  | ||||||
| import fr.free.nrw.commons.utils.handleWebUrl |  | ||||||
| import io.reactivex.disposables.CompositeDisposable |  | ||||||
| import timber.log.Timber |  | ||||||
| import java.util.Locale |  | ||||||
| import javax.inject.Inject |  | ||||||
| import javax.inject.Named |  | ||||||
| 
 |  | ||||||
| class LoginActivity : AccountAuthenticatorActivity() { |  | ||||||
|     @Inject |  | ||||||
|     lateinit var sessionManager: SessionManager |  | ||||||
| 
 |  | ||||||
|     @Inject |  | ||||||
|     @field:Named("default_preferences") |  | ||||||
|     lateinit var applicationKvStore: JsonKvStore |  | ||||||
| 
 |  | ||||||
|     @Inject |  | ||||||
|     lateinit var loginClient: LoginClient |  | ||||||
| 
 |  | ||||||
|     @Inject |  | ||||||
|     lateinit var systemThemeUtils: SystemThemeUtils |  | ||||||
| 
 |  | ||||||
|     private var binding: ActivityLoginBinding? = null |  | ||||||
|     private var progressDialog: ProgressDialog? = null |  | ||||||
|     private val textWatcher = AbstractTextWatcher(::onTextChanged) |  | ||||||
|     private val compositeDisposable = CompositeDisposable() |  | ||||||
|     private val delegate: AppCompatDelegate by lazy { |  | ||||||
|         AppCompatDelegate.create(this, null) |  | ||||||
|     } |  | ||||||
|     private var lastLoginResult: LoginResult? = null |  | ||||||
| 
 |  | ||||||
|     public override fun onCreate(savedInstanceState: Bundle?) { |  | ||||||
|         super.onCreate(savedInstanceState) |  | ||||||
|         ApplicationlessInjection |  | ||||||
|             .getInstance(this.applicationContext) |  | ||||||
|             .commonsApplicationComponent |  | ||||||
|             .inject(this) |  | ||||||
| 
 |  | ||||||
|         val isDarkTheme = systemThemeUtils.isDeviceInNightMode() |  | ||||||
|         setTheme(if (isDarkTheme) R.style.DarkAppTheme else R.style.LightAppTheme) |  | ||||||
|         delegate.installViewFactory() |  | ||||||
|         delegate.onCreate(savedInstanceState) |  | ||||||
| 
 |  | ||||||
|         WindowCompat.getInsetsController(window, window.decorView) |  | ||||||
|             .isAppearanceLightStatusBars = !isDarkTheme |  | ||||||
| 
 |  | ||||||
|         WindowCompat.setDecorFitsSystemWindows(window, false) |  | ||||||
| 
 |  | ||||||
|         binding = ActivityLoginBinding.inflate(layoutInflater) |  | ||||||
|         applyEdgeToEdgeAllInsets(binding!!.root) |  | ||||||
|         binding!!.root.handleKeyboardInsets() |  | ||||||
|         with(binding!!) { |  | ||||||
|             setContentView(root) |  | ||||||
| 
 |  | ||||||
|             loginUsername.addTextChangedListener(textWatcher) |  | ||||||
|             loginPassword.addTextChangedListener(textWatcher) |  | ||||||
|             loginTwoFactor.addTextChangedListener(textWatcher) |  | ||||||
| 
 |  | ||||||
|             skipLogin.setOnClickListener { skipLogin() } |  | ||||||
|             forgotPassword.setOnClickListener { forgotPassword() } |  | ||||||
|             aboutPrivacyPolicy.setOnClickListener { onPrivacyPolicyClicked() } |  | ||||||
|             signUpButton.setOnClickListener { signUp() } |  | ||||||
|             loginButton.setOnClickListener { performLogin() } |  | ||||||
|             loginPassword.setOnEditorActionListener { textView, actionId, keyEvent -> |  | ||||||
|                 if (binding!!.loginButton.isEnabled && isTriggerAction(actionId, keyEvent)) { |  | ||||||
|                     if (actionId == EditorInfo.IME_ACTION_NEXT && lastLoginResult != null) { |  | ||||||
|                         askUserForTwoFactorAuthWithKeyboard() |  | ||||||
|                         true |  | ||||||
|                     } else { |  | ||||||
|                         performLogin() |  | ||||||
|                         true |  | ||||||
|                     } |  | ||||||
|                 } else { |  | ||||||
|                     false |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             loginPassword.onFocusChangeListener = |  | ||||||
|                 View.OnFocusChangeListener(::onPasswordFocusChanged) |  | ||||||
| 
 |  | ||||||
|             if (isBetaFlavour) { |  | ||||||
|                 loginCredentials.text = getString(R.string.login_credential) |  | ||||||
|             } else { |  | ||||||
|                 loginCredentials.visibility = View.GONE |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             intent.getStringExtra(CommonsApplication.LOGIN_MESSAGE_INTENT_KEY)?.let { |  | ||||||
|                 showMessage(it, R.color.secondaryDarkColor) |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             intent.getStringExtra(CommonsApplication.LOGIN_USERNAME_INTENT_KEY)?.let { |  | ||||||
|                 loginUsername.setText(it) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @VisibleForTesting |  | ||||||
|     fun askUserForTwoFactorAuthWithKeyboard() { |  | ||||||
|         if (binding == null) { |  | ||||||
|             Timber.w("Binding is null, reinitializing in askUserForTwoFactorAuthWithKeyboard") |  | ||||||
|             binding = ActivityLoginBinding.inflate(layoutInflater) |  | ||||||
|             setContentView(binding!!.root) |  | ||||||
|         } |  | ||||||
|         progressDialog!!.dismiss() |  | ||||||
|         if (binding != null) { |  | ||||||
|             with(binding!!) { |  | ||||||
|                 twoFactorContainer.visibility = View.VISIBLE |  | ||||||
|                 twoFactorContainer.hint = getString(if (lastLoginResult is LoginResult.EmailAuthResult) R.string.email_auth_code else R.string._2fa_code) |  | ||||||
|                 loginTwoFactor.visibility = View.VISIBLE |  | ||||||
|                 loginTwoFactor.requestFocus() |  | ||||||
| 
 |  | ||||||
|                 val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager |  | ||||||
|                 imm.showSoftInput(loginTwoFactor, InputMethodManager.SHOW_IMPLICIT) |  | ||||||
| 
 |  | ||||||
|                 loginTwoFactor.setOnEditorActionListener { _, actionId, event -> |  | ||||||
|                     if (actionId == EditorInfo.IME_ACTION_DONE || |  | ||||||
|                         (event != null && event.keyCode == KeyEvent.KEYCODE_ENTER && event.action == KeyEvent.ACTION_DOWN)) { |  | ||||||
|                         performLogin() |  | ||||||
|                         true |  | ||||||
|                     } else { |  | ||||||
|                         false |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } else { |  | ||||||
|             Timber.e("Binding is null in askUserForTwoFactorAuthWithKeyboard after reinitialization attempt") |  | ||||||
|         } |  | ||||||
|         showMessageAndCancelDialog(getString(if (lastLoginResult is LoginResult.EmailAuthResult) R.string.login_failed_email_auth_needed else R.string.login_failed_2fa_needed)) |  | ||||||
|     } |  | ||||||
|     override fun onPostCreate(savedInstanceState: Bundle?) { |  | ||||||
|         super.onPostCreate(savedInstanceState) |  | ||||||
|         delegate.onPostCreate(savedInstanceState) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override fun onResume() { |  | ||||||
|         super.onResume() |  | ||||||
| 
 |  | ||||||
|         if (sessionManager.currentAccount != null && sessionManager.isUserLoggedIn) { |  | ||||||
|             applicationKvStore.putBoolean("login_skipped", false) |  | ||||||
|             startMainActivity() |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if (applicationKvStore.getBoolean("login_skipped", false)) { |  | ||||||
|             performSkipLogin() |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override fun onDestroy() { |  | ||||||
|         compositeDisposable.clear() |  | ||||||
|         try { |  | ||||||
|             // To prevent leaked window when finish() is called, see http://stackoverflow.com/questions/32065854/activity-has-leaked-window-at-alertdialog-show-method |  | ||||||
|             if (progressDialog?.isShowing == true) { |  | ||||||
|                 progressDialog!!.dismiss() |  | ||||||
|             } |  | ||||||
|         } catch (e: Exception) { |  | ||||||
|             e.printStackTrace() |  | ||||||
|         } |  | ||||||
|         with(binding!!) { |  | ||||||
|             loginUsername.removeTextChangedListener(textWatcher) |  | ||||||
|             loginPassword.removeTextChangedListener(textWatcher) |  | ||||||
|             loginTwoFactor.removeTextChangedListener(textWatcher) |  | ||||||
|         } |  | ||||||
|         delegate.onDestroy() |  | ||||||
|         loginClient.cancel() |  | ||||||
|         binding = null |  | ||||||
|         super.onDestroy() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override fun onStart() { |  | ||||||
|         super.onStart() |  | ||||||
|         delegate.onStart() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override fun onStop() { |  | ||||||
|         super.onStop() |  | ||||||
|         delegate.onStop() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override fun onPostResume() { |  | ||||||
|         super.onPostResume() |  | ||||||
|         delegate.onPostResume() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override fun setContentView(view: View, params: ViewGroup.LayoutParams) { |  | ||||||
|         delegate.setContentView(view, params) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override fun onOptionsItemSelected(item: MenuItem): Boolean { |  | ||||||
|         when (item.itemId) { |  | ||||||
|             android.R.id.home -> { |  | ||||||
|                 NavUtils.navigateUpFromSameTask(this) |  | ||||||
|                 return true |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         return super.onOptionsItemSelected(item) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override fun onSaveInstanceState(outState: Bundle) { |  | ||||||
|         // if progressDialog is visible during the configuration change  then store state as  true else false so that |  | ||||||
|         // we maintain visibility of progressDialog after configuration change |  | ||||||
|         if (progressDialog != null && progressDialog!!.isShowing) { |  | ||||||
|             outState.putBoolean(SAVE_PROGRESS_DIALOG, true) |  | ||||||
|         } else { |  | ||||||
|             outState.putBoolean(SAVE_PROGRESS_DIALOG, false) |  | ||||||
|         } |  | ||||||
|         outState.putString( |  | ||||||
|             SAVE_ERROR_MESSAGE, |  | ||||||
|             binding!!.errorMessage.text.toString() |  | ||||||
|         ) //Save the errorMessage |  | ||||||
|         outState.putString( |  | ||||||
|             SAVE_USERNAME, |  | ||||||
|             binding!!.loginUsername.text.toString() |  | ||||||
|         ) // Save the username |  | ||||||
|         outState.putString( |  | ||||||
|             SAVE_PASSWORD, |  | ||||||
|             binding!!.loginPassword.text.toString() |  | ||||||
|         ) // Save the password |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override fun onRestoreInstanceState(savedInstanceState: Bundle) { |  | ||||||
|         super.onRestoreInstanceState(savedInstanceState) |  | ||||||
|         binding!!.loginUsername.setText(savedInstanceState.getString(SAVE_USERNAME)) |  | ||||||
|         binding!!.loginPassword.setText(savedInstanceState.getString(SAVE_PASSWORD)) |  | ||||||
|         if (savedInstanceState.getBoolean(SAVE_PROGRESS_DIALOG)) { |  | ||||||
|             performLogin() |  | ||||||
|         } |  | ||||||
|         val errorMessage = savedInstanceState.getString(SAVE_ERROR_MESSAGE) |  | ||||||
|         if (sessionManager.isUserLoggedIn) { |  | ||||||
|             showMessage(R.string.login_success, R.color.primaryDarkColor) |  | ||||||
|         } else { |  | ||||||
|             showMessage(errorMessage, R.color.secondaryDarkColor) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Hides the keyboard if the user's focus is not on the password (hasFocus is false). |  | ||||||
|      * @param view The keyboard |  | ||||||
|      * @param hasFocus Set to true if the keyboard has focus |  | ||||||
|      */ |  | ||||||
|     private fun onPasswordFocusChanged(view: View, hasFocus: Boolean) { |  | ||||||
|         if (!hasFocus) { |  | ||||||
|             hideKeyboard(view) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private fun onEditorAction(textView: TextView, actionId: Int, keyEvent: KeyEvent?) = |  | ||||||
|         if (binding!!.loginButton.isEnabled && isTriggerAction(actionId, keyEvent)) { |  | ||||||
|             performLogin() |  | ||||||
|             true |  | ||||||
|         } else false |  | ||||||
| 
 |  | ||||||
|     private fun isTriggerAction(actionId: Int, keyEvent: KeyEvent?) = |  | ||||||
|         actionId == EditorInfo.IME_ACTION_NEXT || actionId == EditorInfo.IME_ACTION_DONE || keyEvent?.keyCode == KeyEvent.KEYCODE_ENTER |  | ||||||
| 
 |  | ||||||
|     private fun skipLogin() { |  | ||||||
|         AlertDialog.Builder(this) |  | ||||||
|             .setTitle(R.string.skip_login_title) |  | ||||||
|             .setMessage(R.string.skip_login_message) |  | ||||||
|             .setCancelable(false) |  | ||||||
|             .setPositiveButton(R.string.yes) { dialog: DialogInterface, which: Int -> |  | ||||||
|                 dialog.cancel() |  | ||||||
|                 performSkipLogin() |  | ||||||
|             } |  | ||||||
|             .setNegativeButton(R.string.no) { dialog: DialogInterface, which: Int -> |  | ||||||
|                 dialog.cancel() |  | ||||||
|             } |  | ||||||
|             .show() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private fun forgotPassword() = |  | ||||||
|         handleWebUrl(this, Uri.parse(BuildConfig.FORGOT_PASSWORD_URL)) |  | ||||||
| 
 |  | ||||||
|     private fun onPrivacyPolicyClicked() = |  | ||||||
|         handleWebUrl(this, Uri.parse(BuildConfig.PRIVACY_POLICY_URL)) |  | ||||||
| 
 |  | ||||||
|     private fun signUp() = |  | ||||||
|         startActivity(Intent(this, SignupActivity::class.java)) |  | ||||||
| 
 |  | ||||||
|     @VisibleForTesting |  | ||||||
|     fun performLogin() { |  | ||||||
|         Timber.d("Login to start!") |  | ||||||
|         val username = binding!!.loginUsername.text.toString() |  | ||||||
|         val password = binding!!.loginPassword.text.toString() |  | ||||||
|         val twoFactorCode = binding!!.loginTwoFactor.text.toString() |  | ||||||
| 
 |  | ||||||
|         showLoggingProgressBar() |  | ||||||
|         loginClient.doLogin(username, |  | ||||||
|             password, |  | ||||||
|             lastLoginResult, |  | ||||||
|             twoFactorCode, |  | ||||||
|             Locale.getDefault().language, |  | ||||||
|             object : LoginCallback { |  | ||||||
|                 override fun success(loginResult: LoginResult) = runOnUiThread { |  | ||||||
|                     Timber.d("Login Success") |  | ||||||
|                     progressDialog!!.dismiss() |  | ||||||
|                     onLoginSuccess(loginResult) |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 override fun twoFactorPrompt(loginResult: LoginResult, caught: Throwable, token: String?) = runOnUiThread { |  | ||||||
|                     Timber.d("Requesting 2FA prompt") |  | ||||||
|                     progressDialog!!.dismiss() |  | ||||||
|                     lastLoginResult = loginResult |  | ||||||
|                     askUserForTwoFactorAuthWithKeyboard() |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 override fun emailAuthPrompt(loginResult: LoginResult, caught: Throwable, token: String?) = runOnUiThread { |  | ||||||
|                     Timber.d("Requesting email auth prompt") |  | ||||||
|                     progressDialog!!.dismiss() |  | ||||||
|                     lastLoginResult = loginResult |  | ||||||
|                     askUserForTwoFactorAuthWithKeyboard() |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 override fun passwordResetPrompt(token: String?) = runOnUiThread { |  | ||||||
|                     Timber.d("Showing password reset prompt") |  | ||||||
|                     progressDialog!!.dismiss() |  | ||||||
|                     showPasswordResetPrompt() |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 override fun error(caught: Throwable) = runOnUiThread { |  | ||||||
|                     Timber.e(caught) |  | ||||||
|                     progressDialog!!.dismiss() |  | ||||||
|                     showMessageAndCancelDialog(caught.localizedMessage ?: "") |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         ) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private fun showPasswordResetPrompt() = |  | ||||||
|         showMessageAndCancelDialog(getString(R.string.you_must_reset_your_passsword)) |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * This function is called when user skips the login. |  | ||||||
|      * It redirects the user to Explore Activity. |  | ||||||
|      */ |  | ||||||
|     private fun performSkipLogin() { |  | ||||||
|         applicationKvStore.putBoolean("login_skipped", true) |  | ||||||
|         MainActivity.startYourself(this) |  | ||||||
|         finish() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private fun showLoggingProgressBar() { |  | ||||||
|         progressDialog = ProgressDialog(this).apply { |  | ||||||
|             isIndeterminate = true |  | ||||||
|             setTitle(getString(R.string.logging_in_title)) |  | ||||||
|             setMessage(getString(R.string.logging_in_message)) |  | ||||||
|             setCancelable(false) |  | ||||||
|         } |  | ||||||
|         progressDialog!!.show() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private fun onLoginSuccess(loginResult: LoginResult) { |  | ||||||
|         compositeDisposable.clear() |  | ||||||
|         sessionManager.setUserLoggedIn(true) |  | ||||||
|         sessionManager.updateAccount(loginResult) |  | ||||||
|         progressDialog!!.dismiss() |  | ||||||
|         showSuccessAndDismissDialog() |  | ||||||
|         startMainActivity() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override fun getMenuInflater(): MenuInflater = |  | ||||||
|         delegate.menuInflater |  | ||||||
| 
 |  | ||||||
|     @VisibleForTesting |  | ||||||
|     fun askUserForTwoFactorAuth() { |  | ||||||
|         if (binding == null) { |  | ||||||
|             Timber.w("Binding is null, reinitializing in askUserForTwoFactorAuth") |  | ||||||
|             binding = ActivityLoginBinding.inflate(layoutInflater) |  | ||||||
|             setContentView(binding!!.root) |  | ||||||
|         } |  | ||||||
|         progressDialog!!.dismiss() |  | ||||||
|         if (binding != null) { |  | ||||||
|             with(binding!!) { |  | ||||||
|                 twoFactorContainer.visibility = View.VISIBLE |  | ||||||
|                 twoFactorContainer.hint = getString(if (lastLoginResult is LoginResult.EmailAuthResult) R.string.email_auth_code else R.string._2fa_code) |  | ||||||
|                 loginTwoFactor.visibility = View.VISIBLE |  | ||||||
|                 loginTwoFactor.requestFocus() |  | ||||||
| 
 |  | ||||||
|                 loginTwoFactor.setOnEditorActionListener { _, actionId, event -> |  | ||||||
|                     if (actionId == EditorInfo.IME_ACTION_DONE || |  | ||||||
|                         (event != null && event.keyCode == KeyEvent.KEYCODE_ENTER && event.action == KeyEvent.ACTION_DOWN)) { |  | ||||||
|                         performLogin() |  | ||||||
|                         true |  | ||||||
|                     } else { |  | ||||||
|                         false |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } else { |  | ||||||
|             Timber.e("Binding is null in askUserForTwoFactorAuth after reinitialization attempt") |  | ||||||
|         } |  | ||||||
|         val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager |  | ||||||
|         imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY) |  | ||||||
|         showMessageAndCancelDialog(getString(if (lastLoginResult is LoginResult.EmailAuthResult) R.string.login_failed_email_auth_needed else R.string.login_failed_2fa_needed)) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @VisibleForTesting |  | ||||||
|     fun showMessageAndCancelDialog(@StringRes resId: Int) { |  | ||||||
|         showMessage(resId, R.color.secondaryDarkColor) |  | ||||||
|         progressDialog?.cancel() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @VisibleForTesting |  | ||||||
|     fun showMessageAndCancelDialog(error: String) { |  | ||||||
|         showMessage(error, R.color.secondaryDarkColor) |  | ||||||
|         progressDialog?.cancel() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @VisibleForTesting |  | ||||||
|     fun showSuccessAndDismissDialog() { |  | ||||||
|         showMessage(R.string.login_success, R.color.primaryDarkColor) |  | ||||||
|         progressDialog!!.dismiss() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @VisibleForTesting |  | ||||||
|     fun startMainActivity() { |  | ||||||
|         startActivityWithFlags(this, MainActivity::class.java, Intent.FLAG_ACTIVITY_SINGLE_TOP) |  | ||||||
|         finish() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private fun showMessage(@StringRes resId: Int, @ColorRes colorResId: Int) = with(binding!!) { |  | ||||||
|         errorMessage.text = getString(resId) |  | ||||||
|         errorMessage.setTextColor(ContextCompat.getColor(this@LoginActivity, colorResId)) |  | ||||||
|         errorMessageContainer.visibility = View.VISIBLE |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private fun showMessage(message: String?, @ColorRes colorResId: Int) = with(binding!!) { |  | ||||||
|         errorMessage.text = message |  | ||||||
|         errorMessage.setTextColor(ContextCompat.getColor(this@LoginActivity, colorResId)) |  | ||||||
|         errorMessageContainer.visibility = View.VISIBLE |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private fun onTextChanged(text: String) { |  | ||||||
|         val enabled = |  | ||||||
|             binding!!.loginUsername.text!!.length != 0 && binding!!.loginPassword.text!!.length != 0 && |  | ||||||
|                     (BuildConfig.DEBUG || binding!!.loginTwoFactor.text!!.length != 0 || binding!!.loginTwoFactor.visibility != View.VISIBLE) |  | ||||||
|         binding!!.loginButton.isEnabled = enabled |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     companion object { |  | ||||||
|         fun startYourself(context: Context) = |  | ||||||
|             context.startActivity(Intent(context, LoginActivity::class.java)) |  | ||||||
| 
 |  | ||||||
|         const val SAVE_PROGRESS_DIALOG: String = "ProgressDialog_state" |  | ||||||
|         const val SAVE_ERROR_MESSAGE: String = "errorMessage" |  | ||||||
|         const val SAVE_USERNAME: String = "username" |  | ||||||
|         const val SAVE_PASSWORD: String = "password" |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										36
									
								
								app/src/main/java/fr/free/nrw/commons/auth/LogoutClient.java
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								app/src/main/java/fr/free/nrw/commons/auth/LogoutClient.java
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,36 @@ | ||||||
|  | package fr.free.nrw.commons.auth; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | import org.wikipedia.dataclient.Service; | ||||||
|  | import org.wikipedia.dataclient.mwapi.MwPostResponse; | ||||||
|  | 
 | ||||||
|  | import java.util.Objects; | ||||||
|  | 
 | ||||||
|  | import javax.inject.Inject; | ||||||
|  | import javax.inject.Named; | ||||||
|  | import javax.inject.Singleton; | ||||||
|  | 
 | ||||||
|  | import io.reactivex.Observable; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Handler for logout | ||||||
|  |  */ | ||||||
|  | @Singleton | ||||||
|  | public class LogoutClient { | ||||||
|  | 
 | ||||||
|  |     private final Service service; | ||||||
|  | 
 | ||||||
|  |     @Inject | ||||||
|  |     public LogoutClient(@Named("commons-service") Service service) { | ||||||
|  |         this.service = service; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Fetches the  CSRF token and uses that to post the logout api call | ||||||
|  |      * @return | ||||||
|  |      */ | ||||||
|  |     public Observable<MwPostResponse> postLogout() { | ||||||
|  |         return service.getCsrfToken().concatMap(tokenResponse -> service.postLogout( | ||||||
|  |                 Objects.requireNonNull(Objects.requireNonNull(tokenResponse.query()).csrfToken()))); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										149
									
								
								app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										149
									
								
								app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,149 @@ | ||||||
|  | package fr.free.nrw.commons.auth; | ||||||
|  | 
 | ||||||
|  | import android.accounts.Account; | ||||||
|  | import android.accounts.AccountManager; | ||||||
|  | import android.content.Context; | ||||||
|  | import android.os.Build; | ||||||
|  | import android.text.TextUtils; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.annotation.Nullable; | ||||||
|  | 
 | ||||||
|  | import org.wikipedia.login.LoginResult; | ||||||
|  | 
 | ||||||
|  | import javax.inject.Inject; | ||||||
|  | import javax.inject.Named; | ||||||
|  | import javax.inject.Singleton; | ||||||
|  | 
 | ||||||
|  | import fr.free.nrw.commons.BuildConfig; | ||||||
|  | import fr.free.nrw.commons.kvstore.JsonKvStore; | ||||||
|  | import io.reactivex.Completable; | ||||||
|  | import io.reactivex.Observable; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Manage the current logged in user session. | ||||||
|  |  */ | ||||||
|  | @Singleton | ||||||
|  | public class SessionManager { | ||||||
|  |     private final Context context; | ||||||
|  |     private Account currentAccount; // Unlike a savings account...  ;-) | ||||||
|  |     private JsonKvStore defaultKvStore; | ||||||
|  | 
 | ||||||
|  |     @Inject | ||||||
|  |     public SessionManager(Context context, | ||||||
|  |                           @Named("default_preferences") JsonKvStore defaultKvStore) { | ||||||
|  |         this.context = context; | ||||||
|  |         this.currentAccount = null; | ||||||
|  |         this.defaultKvStore = defaultKvStore; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private boolean createAccount(@NonNull String userName, @NonNull String password) { | ||||||
|  |         Account account = getCurrentAccount(); | ||||||
|  |         if (account == null || TextUtils.isEmpty(account.name) || !account.name.equals(userName)) { | ||||||
|  |             removeAccount(); | ||||||
|  |             account = new Account(userName, BuildConfig.ACCOUNT_TYPE); | ||||||
|  |             return accountManager().addAccountExplicitly(account, password, null); | ||||||
|  |         } | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void removeAccount() { | ||||||
|  |         Account account = getCurrentAccount(); | ||||||
|  |         if (account != null) { | ||||||
|  |             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { | ||||||
|  |                 accountManager().removeAccountExplicitly(account); | ||||||
|  |             } else { | ||||||
|  |                 //noinspection deprecation | ||||||
|  |                 accountManager().removeAccount(account, null, null); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void updateAccount(LoginResult result) { | ||||||
|  |         boolean accountCreated = createAccount(result.getUserName(), result.getPassword()); | ||||||
|  |         if (accountCreated) { | ||||||
|  |             setPassword(result.getPassword()); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void setPassword(@NonNull String password) { | ||||||
|  |         Account account = getCurrentAccount(); | ||||||
|  |         if (account != null) { | ||||||
|  |             accountManager().setPassword(account, password); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * @return Account|null | ||||||
|  |      */ | ||||||
|  |     @Nullable | ||||||
|  |     public Account getCurrentAccount() { | ||||||
|  |         if (currentAccount == null) { | ||||||
|  |             AccountManager accountManager = AccountManager.get(context); | ||||||
|  |             Account[] allAccounts = accountManager.getAccountsByType(BuildConfig.ACCOUNT_TYPE); | ||||||
|  |             if (allAccounts.length != 0) { | ||||||
|  |                 currentAccount = allAccounts[0]; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return currentAccount; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public boolean doesAccountExist() { | ||||||
|  |         return getCurrentAccount() != null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Nullable | ||||||
|  |     public String getUserName() { | ||||||
|  |         Account account = getCurrentAccount(); | ||||||
|  |         return account == null ? null : account.name; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Nullable | ||||||
|  |     public String getPassword() { | ||||||
|  |         Account account = getCurrentAccount(); | ||||||
|  |         return account == null ? null : accountManager().getPassword(account); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private AccountManager accountManager() { | ||||||
|  |         return AccountManager.get(context); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public boolean isUserLoggedIn() { | ||||||
|  |         return defaultKvStore.getBoolean("isUserLoggedIn", false); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     void setUserLoggedIn(boolean isLoggedIn) { | ||||||
|  |         defaultKvStore.putBoolean("isUserLoggedIn", isLoggedIn); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void forceLogin(Context context) { | ||||||
|  |         if (context != null) { | ||||||
|  |             LoginActivity.startYourself(context); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * 1. Clears existing accounts from account manager | ||||||
|  |      * 2. Calls MediaWikiApi's logout function to clear cookies | ||||||
|  |      * @return | ||||||
|  |      */ | ||||||
|  |     public Completable logout() { | ||||||
|  |         AccountManager accountManager = AccountManager.get(context); | ||||||
|  |         Account[] allAccounts = accountManager.getAccountsByType(BuildConfig.ACCOUNT_TYPE); | ||||||
|  |         return Completable.fromObservable(Observable.fromArray(allAccounts) | ||||||
|  |                 .map(a -> accountManager.removeAccount(a, null, null).getResult())) | ||||||
|  |                 .doOnComplete(() -> { | ||||||
|  |                     currentAccount = null; | ||||||
|  |                 }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Return a corresponding boolean preference | ||||||
|  |      * | ||||||
|  |      * @param key | ||||||
|  |      * @return | ||||||
|  |      */ | ||||||
|  |     public boolean getPreference(String key) { | ||||||
|  |         return defaultKvStore.getBoolean(key); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,95 +0,0 @@ | ||||||
| package fr.free.nrw.commons.auth |  | ||||||
| 
 |  | ||||||
| import android.accounts.Account |  | ||||||
| import android.accounts.AccountManager |  | ||||||
| import android.content.Context |  | ||||||
| import android.os.Build |  | ||||||
| import android.text.TextUtils |  | ||||||
| import fr.free.nrw.commons.BuildConfig.ACCOUNT_TYPE |  | ||||||
| import fr.free.nrw.commons.auth.login.LoginResult |  | ||||||
| import fr.free.nrw.commons.kvstore.JsonKvStore |  | ||||||
| import io.reactivex.Completable |  | ||||||
| import io.reactivex.Observable |  | ||||||
| import javax.inject.Inject |  | ||||||
| import javax.inject.Named |  | ||||||
| import javax.inject.Singleton |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Manage the current logged in user session. |  | ||||||
|  */ |  | ||||||
| @Singleton |  | ||||||
| class SessionManager @Inject constructor( |  | ||||||
|     private val context: Context, |  | ||||||
|     @param:Named("default_preferences") private val defaultKvStore: JsonKvStore |  | ||||||
| ) { |  | ||||||
|     private val accountManager: AccountManager get() = AccountManager.get(context) |  | ||||||
| 
 |  | ||||||
|     private var _currentAccount: Account? = null // Unlike a savings account...  ;-) |  | ||||||
|     val currentAccount: Account? get() { |  | ||||||
|         if (_currentAccount == null) { |  | ||||||
|             val allAccounts = AccountManager.get(context).getAccountsByType(ACCOUNT_TYPE) |  | ||||||
|             if (allAccounts.isNotEmpty()) { |  | ||||||
|                 _currentAccount = allAccounts[0] |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         return _currentAccount |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     val userName: String? |  | ||||||
|         get() = currentAccount?.name |  | ||||||
| 
 |  | ||||||
|     var password: String? |  | ||||||
|         get() = currentAccount?.let { accountManager.getPassword(it) } |  | ||||||
|         private set(value) { |  | ||||||
|             currentAccount?.let { accountManager.setPassword(it, value) } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|     val isUserLoggedIn: Boolean |  | ||||||
|         get() = defaultKvStore.getBoolean("isUserLoggedIn", false) |  | ||||||
| 
 |  | ||||||
|     fun updateAccount(result: LoginResult) { |  | ||||||
|         if (createAccount(result.userName!!, result.password!!)) { |  | ||||||
|             password = result.password |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fun doesAccountExist(): Boolean = |  | ||||||
|         currentAccount != null |  | ||||||
| 
 |  | ||||||
|     fun setUserLoggedIn(isLoggedIn: Boolean) = |  | ||||||
|         defaultKvStore.putBoolean("isUserLoggedIn", isLoggedIn) |  | ||||||
| 
 |  | ||||||
|     fun forceLogin(context: Context?) = |  | ||||||
|         context?.let { LoginActivity.startYourself(it) } |  | ||||||
| 
 |  | ||||||
|     fun getPreference(key: String): Boolean = |  | ||||||
|         defaultKvStore.getBoolean(key) |  | ||||||
| 
 |  | ||||||
|     fun logout(): Completable = Completable.fromObservable( |  | ||||||
|         Observable.empty<Any>() |  | ||||||
|             .doOnComplete { |  | ||||||
|                 removeAccount() |  | ||||||
|                 _currentAccount = null |  | ||||||
|             } |  | ||||||
|     ) |  | ||||||
| 
 |  | ||||||
|     private fun createAccount(userName: String, password: String): Boolean { |  | ||||||
|         var account = currentAccount |  | ||||||
|         if (account == null || TextUtils.isEmpty(account.name) || account.name != userName) { |  | ||||||
|             removeAccount() |  | ||||||
|             account = Account(userName, ACCOUNT_TYPE) |  | ||||||
|             return accountManager.addAccountExplicitly(account, password, null) |  | ||||||
|         } |  | ||||||
|         return true |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private fun removeAccount() { |  | ||||||
|         currentAccount?.let { |  | ||||||
|             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { |  | ||||||
|                 accountManager.removeAccountExplicitly(it) |  | ||||||
|             } else { |  | ||||||
|                 accountManager.removeAccount(it, null, null) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -0,0 +1,64 @@ | ||||||
|  | package fr.free.nrw.commons.auth; | ||||||
|  | 
 | ||||||
|  | import android.os.Bundle; | ||||||
|  | import android.webkit.WebSettings; | ||||||
|  | import android.webkit.WebView; | ||||||
|  | import android.webkit.WebViewClient; | ||||||
|  | import android.widget.Toast; | ||||||
|  | 
 | ||||||
|  | import fr.free.nrw.commons.BuildConfig; | ||||||
|  | import fr.free.nrw.commons.R; | ||||||
|  | import fr.free.nrw.commons.theme.BaseActivity; | ||||||
|  | import timber.log.Timber; | ||||||
|  | 
 | ||||||
|  | public class SignupActivity extends BaseActivity { | ||||||
|  | 
 | ||||||
|  |     private WebView webView; | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     protected void onCreate(Bundle savedInstanceState) { | ||||||
|  |         super.onCreate(savedInstanceState); | ||||||
|  |         Timber.d("Signup Activity started"); | ||||||
|  | 
 | ||||||
|  |         webView = new WebView(this); | ||||||
|  |         setContentView(webView); | ||||||
|  | 
 | ||||||
|  |         webView.setWebViewClient(new MyWebViewClient()); | ||||||
|  |         WebSettings webSettings = webView.getSettings(); | ||||||
|  |         /*Needed to refresh Captcha. Might introduce XSS vulnerabilities, but we can | ||||||
|  |          trust Wikimedia's site... right?*/ | ||||||
|  |         webSettings.setJavaScriptEnabled(true); | ||||||
|  | 
 | ||||||
|  |         webView.loadUrl(BuildConfig.SIGNUP_LANDING_URL); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private class MyWebViewClient extends WebViewClient { | ||||||
|  |         @Override | ||||||
|  |         public boolean shouldOverrideUrlLoading(WebView view, String url) { | ||||||
|  |             if (url.equals(BuildConfig.SIGNUP_SUCCESS_REDIRECTION_URL)) { | ||||||
|  |                 //Signup success, so clear cookies, notify user, and load LoginActivity again | ||||||
|  |                 Timber.d("Overriding URL %s", url); | ||||||
|  | 
 | ||||||
|  |                 Toast toast = Toast.makeText(SignupActivity.this, | ||||||
|  |                         R.string.account_created, Toast.LENGTH_LONG); | ||||||
|  |                 toast.show(); | ||||||
|  |                 // terminate on task completion. | ||||||
|  |                 finish(); | ||||||
|  |                 return true; | ||||||
|  |             } else { | ||||||
|  |                 //If user clicks any other links in the webview | ||||||
|  |                 Timber.d("Not overriding URL, URL is: %s", url); | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onBackPressed() { | ||||||
|  |         if (webView.canGoBack()) { | ||||||
|  |             webView.goBack(); | ||||||
|  |         } else { | ||||||
|  |             super.onBackPressed(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,77 +0,0 @@ | ||||||
| package fr.free.nrw.commons.auth |  | ||||||
| 
 |  | ||||||
| import android.annotation.SuppressLint |  | ||||||
| import android.content.res.Configuration |  | ||||||
| import android.os.Build |  | ||||||
| import android.os.Bundle |  | ||||||
| import android.webkit.WebView |  | ||||||
| import android.webkit.WebViewClient |  | ||||||
| import android.widget.Toast |  | ||||||
| import fr.free.nrw.commons.BuildConfig |  | ||||||
| import fr.free.nrw.commons.R |  | ||||||
| import fr.free.nrw.commons.theme.BaseActivity |  | ||||||
| import fr.free.nrw.commons.utils.applyEdgeToEdgeAllInsets |  | ||||||
| import timber.log.Timber |  | ||||||
| 
 |  | ||||||
| class SignupActivity : BaseActivity() { |  | ||||||
|     private var webView: WebView? = null |  | ||||||
| 
 |  | ||||||
|     @SuppressLint("SetJavaScriptEnabled") |  | ||||||
|     override fun onCreate(savedInstanceState: Bundle?) { |  | ||||||
|         super.onCreate(savedInstanceState) |  | ||||||
|         Timber.d("Signup Activity started") |  | ||||||
| 
 |  | ||||||
|         webView = WebView(this) |  | ||||||
|         applyEdgeToEdgeAllInsets(webView!!) |  | ||||||
|         with(webView!!) { |  | ||||||
|             setContentView(this) |  | ||||||
|             webViewClient = MyWebViewClient() |  | ||||||
|             // Needed to refresh Captcha. Might introduce XSS vulnerabilities, but we can |  | ||||||
|             // trust Wikimedia's site... right? |  | ||||||
|             settings.javaScriptEnabled = true |  | ||||||
|             loadUrl(BuildConfig.SIGNUP_LANDING_URL) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override fun onBackPressed() { |  | ||||||
|         if (webView!!.canGoBack()) { |  | ||||||
|             webView!!.goBack() |  | ||||||
|         } else { |  | ||||||
|             super.onBackPressed() |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Known bug in androidx.appcompat library version 1.1.0 being tracked here |  | ||||||
|      * https://issuetracker.google.com/issues/141132133 |  | ||||||
|      * App tries to put light/dark theme to webview and crashes in the process |  | ||||||
|      * This code tries to prevent applying the theme when sdk is between api 21 to 25 |  | ||||||
|      */ |  | ||||||
|     override fun applyOverrideConfiguration(overrideConfiguration: Configuration) { |  | ||||||
|         if (Build.VERSION.SDK_INT <= 25 && |  | ||||||
|             (resources.configuration.uiMode == applicationContext.resources.configuration.uiMode) |  | ||||||
|         ) return |  | ||||||
|         super.applyOverrideConfiguration(overrideConfiguration) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private inner class MyWebViewClient : WebViewClient() { |  | ||||||
|         @Deprecated("Deprecated in Java") |  | ||||||
|         override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean = |  | ||||||
|             if (url == BuildConfig.SIGNUP_SUCCESS_REDIRECTION_URL) { |  | ||||||
|                 //Signup success, so clear cookies, notify user, and load LoginActivity again |  | ||||||
|                 Timber.d("Overriding URL %s", url) |  | ||||||
| 
 |  | ||||||
|                 Toast.makeText( |  | ||||||
|                     this@SignupActivity, R.string.account_created, Toast.LENGTH_LONG |  | ||||||
|                 ).show() |  | ||||||
| 
 |  | ||||||
|                 // terminate on task completion. |  | ||||||
|                 finish() |  | ||||||
|                 true |  | ||||||
|             } else { |  | ||||||
|                 //If user clicks any other links in the webview |  | ||||||
|                 Timber.d("Not overriding URL, URL is: %s", url) |  | ||||||
|                 false |  | ||||||
|             } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -0,0 +1,141 @@ | ||||||
|  | package fr.free.nrw.commons.auth; | ||||||
|  | 
 | ||||||
|  | import android.accounts.AbstractAccountAuthenticator; | ||||||
|  | import android.accounts.Account; | ||||||
|  | import android.accounts.AccountAuthenticatorResponse; | ||||||
|  | import android.accounts.AccountManager; | ||||||
|  | import android.accounts.NetworkErrorException; | ||||||
|  | import android.content.ContentResolver; | ||||||
|  | import android.content.Context; | ||||||
|  | import android.content.Intent; | ||||||
|  | import android.os.Bundle; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.annotation.Nullable; | ||||||
|  | 
 | ||||||
|  | import fr.free.nrw.commons.BuildConfig; | ||||||
|  | 
 | ||||||
|  | import static fr.free.nrw.commons.auth.AccountUtil.AUTH_TOKEN_TYPE; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Handles WikiMedia commons account Authentication | ||||||
|  |  */ | ||||||
|  | public class WikiAccountAuthenticator extends AbstractAccountAuthenticator { | ||||||
|  |     private static final String[] SYNC_AUTHORITIES = {BuildConfig.CONTRIBUTION_AUTHORITY, BuildConfig.MODIFICATION_AUTHORITY}; | ||||||
|  | 
 | ||||||
|  |     @NonNull | ||||||
|  |     private final Context context; | ||||||
|  | 
 | ||||||
|  |     public WikiAccountAuthenticator(@NonNull Context context) { | ||||||
|  |         super(context); | ||||||
|  |         this.context = context; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Provides Bundle with edited Account Properties  | ||||||
|  |      */ | ||||||
|  |     @Override | ||||||
|  |     public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) { | ||||||
|  |         Bundle bundle = new Bundle(); | ||||||
|  |         bundle.putString("test", "editProperties"); | ||||||
|  |         return bundle; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public Bundle addAccount(@NonNull AccountAuthenticatorResponse response, | ||||||
|  |                              @NonNull String accountType, @Nullable String authTokenType, | ||||||
|  |                              @Nullable String[] requiredFeatures, @Nullable Bundle options) | ||||||
|  |             throws NetworkErrorException { | ||||||
|  |         // account type not supported returns bundle without loginActivity Intent, it just contains "test" key  | ||||||
|  |         if (!supportedAccountType(accountType)) { | ||||||
|  |             Bundle bundle = new Bundle(); | ||||||
|  |             bundle.putString("test", "addAccount"); | ||||||
|  |             return bundle; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return addAccount(response); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public Bundle confirmCredentials(@NonNull AccountAuthenticatorResponse response, | ||||||
|  |                                      @NonNull Account account, @Nullable Bundle options) | ||||||
|  |             throws NetworkErrorException { | ||||||
|  |         Bundle bundle = new Bundle(); | ||||||
|  |         bundle.putString("test", "confirmCredentials"); | ||||||
|  |         return bundle; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public Bundle getAuthToken(@NonNull AccountAuthenticatorResponse response, | ||||||
|  |                                @NonNull Account account, @NonNull String authTokenType, | ||||||
|  |                                @Nullable Bundle options) | ||||||
|  |             throws NetworkErrorException { | ||||||
|  |         Bundle bundle = new Bundle(); | ||||||
|  |         bundle.putString("test", "getAuthToken"); | ||||||
|  |         return bundle; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Nullable | ||||||
|  |     @Override | ||||||
|  |     public String getAuthTokenLabel(@NonNull String authTokenType) { | ||||||
|  |         return supportedAccountType(authTokenType) ? AUTH_TOKEN_TYPE : null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Nullable | ||||||
|  |     @Override | ||||||
|  |     public Bundle updateCredentials(@NonNull AccountAuthenticatorResponse response, | ||||||
|  |                                     @NonNull Account account, @Nullable String authTokenType, | ||||||
|  |                                     @Nullable Bundle options) | ||||||
|  |             throws NetworkErrorException { | ||||||
|  |         Bundle bundle = new Bundle(); | ||||||
|  |         bundle.putString("test", "updateCredentials"); | ||||||
|  |         return bundle; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Nullable | ||||||
|  |     @Override | ||||||
|  |     public Bundle hasFeatures(@NonNull AccountAuthenticatorResponse response, | ||||||
|  |                               @NonNull Account account, @NonNull String[] features) | ||||||
|  |             throws NetworkErrorException { | ||||||
|  |         Bundle bundle = new Bundle(); | ||||||
|  |         bundle.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false); | ||||||
|  |         return bundle; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private boolean supportedAccountType(@Nullable String type) { | ||||||
|  |         return BuildConfig.ACCOUNT_TYPE.equals(type); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Provides a bundle containing a Parcel  | ||||||
|  |      * the Parcel packs an Intent with LoginActivity and Authenticator response (requires valid account type) | ||||||
|  |      */ | ||||||
|  |     private Bundle addAccount(AccountAuthenticatorResponse response) { | ||||||
|  |         Intent intent = new Intent(context, LoginActivity.class); | ||||||
|  |         intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response); | ||||||
|  | 
 | ||||||
|  |         Bundle bundle = new Bundle(); | ||||||
|  |         bundle.putParcelable(AccountManager.KEY_INTENT, intent); | ||||||
|  | 
 | ||||||
|  |         return bundle; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public Bundle getAccountRemovalAllowed(AccountAuthenticatorResponse response, | ||||||
|  |                                            Account account) throws NetworkErrorException { | ||||||
|  |         Bundle result = super.getAccountRemovalAllowed(response, account); | ||||||
|  | 
 | ||||||
|  |         if (result.containsKey(AccountManager.KEY_BOOLEAN_RESULT) | ||||||
|  |                 && !result.containsKey(AccountManager.KEY_INTENT)) { | ||||||
|  |             boolean allowed = result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT); | ||||||
|  | 
 | ||||||
|  |             if (allowed) { | ||||||
|  |                 for (String auth : SYNC_AUTHORITIES) { | ||||||
|  |                     ContentResolver.cancelSync(account, auth); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return result; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,108 +0,0 @@ | ||||||
| package fr.free.nrw.commons.auth |  | ||||||
| 
 |  | ||||||
| import android.accounts.AbstractAccountAuthenticator |  | ||||||
| import android.accounts.Account |  | ||||||
| import android.accounts.AccountAuthenticatorResponse |  | ||||||
| import android.accounts.AccountManager |  | ||||||
| import android.accounts.NetworkErrorException |  | ||||||
| import android.content.ContentResolver |  | ||||||
| import android.content.Context |  | ||||||
| import android.content.Intent |  | ||||||
| import android.os.Bundle |  | ||||||
| import androidx.core.os.bundleOf |  | ||||||
| import fr.free.nrw.commons.BuildConfig |  | ||||||
| 
 |  | ||||||
| private val SYNC_AUTHORITIES = arrayOf( |  | ||||||
|     BuildConfig.CONTRIBUTION_AUTHORITY, BuildConfig.MODIFICATION_AUTHORITY |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Handles WikiMedia commons account Authentication |  | ||||||
|  */ |  | ||||||
| class WikiAccountAuthenticator( |  | ||||||
|     private val context: Context |  | ||||||
| ) : AbstractAccountAuthenticator(context) { |  | ||||||
|     /** |  | ||||||
|      * Provides Bundle with edited Account Properties |  | ||||||
|      */ |  | ||||||
|     override fun editProperties( |  | ||||||
|         response: AccountAuthenticatorResponse, |  | ||||||
|         accountType: String |  | ||||||
|     ) = bundleOf("test" to "editProperties") |  | ||||||
| 
 |  | ||||||
|     // account type not supported returns bundle without loginActivity Intent, it just contains "test" key |  | ||||||
|     @Throws(NetworkErrorException::class) |  | ||||||
|     override fun addAccount( |  | ||||||
|         response: AccountAuthenticatorResponse, |  | ||||||
|         accountType: String, |  | ||||||
|         authTokenType: String?, |  | ||||||
|         requiredFeatures: Array<String>?, |  | ||||||
|         options: Bundle? |  | ||||||
|     ) = if (BuildConfig.ACCOUNT_TYPE == accountType) { |  | ||||||
|         addAccount(response) |  | ||||||
|     } else { |  | ||||||
|         bundleOf("test" to "addAccount") |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Throws(NetworkErrorException::class) |  | ||||||
|     override fun confirmCredentials( |  | ||||||
|         response: AccountAuthenticatorResponse, account: Account, options: Bundle? |  | ||||||
|     ) = bundleOf("test" to "confirmCredentials") |  | ||||||
| 
 |  | ||||||
|     @Throws(NetworkErrorException::class) |  | ||||||
|     override fun getAuthToken( |  | ||||||
|         response: AccountAuthenticatorResponse, |  | ||||||
|         account: Account, |  | ||||||
|         authTokenType: String, |  | ||||||
|         options: Bundle? |  | ||||||
|     ) = bundleOf("test" to "getAuthToken") |  | ||||||
| 
 |  | ||||||
|     override fun getAuthTokenLabel(authTokenType: String) = |  | ||||||
|         if (BuildConfig.ACCOUNT_TYPE == authTokenType) AUTH_TOKEN_TYPE else null |  | ||||||
| 
 |  | ||||||
|     @Throws(NetworkErrorException::class) |  | ||||||
|     override fun updateCredentials( |  | ||||||
|         response: AccountAuthenticatorResponse, |  | ||||||
|         account: Account, |  | ||||||
|         authTokenType: String?, |  | ||||||
|         options: Bundle? |  | ||||||
|     ) = bundleOf("test" to "updateCredentials") |  | ||||||
| 
 |  | ||||||
|     @Throws(NetworkErrorException::class) |  | ||||||
|     override fun hasFeatures( |  | ||||||
|         response: AccountAuthenticatorResponse, |  | ||||||
|         account: Account, features: Array<String> |  | ||||||
|     ) = bundleOf(AccountManager.KEY_BOOLEAN_RESULT to false) |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Provides a bundle containing a Parcel |  | ||||||
|      * the Parcel packs an Intent with LoginActivity and Authenticator response (requires valid account type) |  | ||||||
|      */ |  | ||||||
|     private fun addAccount(response: AccountAuthenticatorResponse): Bundle { |  | ||||||
|         val intent = Intent(context, LoginActivity::class.java) |  | ||||||
|             .putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response) |  | ||||||
|         return bundleOf(AccountManager.KEY_INTENT to intent) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Throws(NetworkErrorException::class) |  | ||||||
|     override fun getAccountRemovalAllowed( |  | ||||||
|         response: AccountAuthenticatorResponse?, |  | ||||||
|         account: Account? |  | ||||||
|     ): Bundle { |  | ||||||
|         val result = super.getAccountRemovalAllowed(response, account) |  | ||||||
| 
 |  | ||||||
|         if (result.containsKey(AccountManager.KEY_BOOLEAN_RESULT) |  | ||||||
|             && !result.containsKey(AccountManager.KEY_INTENT) |  | ||||||
|         ) { |  | ||||||
|             val allowed = result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT) |  | ||||||
| 
 |  | ||||||
|             if (allowed) { |  | ||||||
|                 for (auth in SYNC_AUTHORITIES) { |  | ||||||
|                     ContentResolver.cancelSync(account, auth) |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return result |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -0,0 +1,31 @@ | ||||||
|  | package fr.free.nrw.commons.auth; | ||||||
|  | 
 | ||||||
|  | import android.accounts.AbstractAccountAuthenticator; | ||||||
|  | import android.content.Intent; | ||||||
|  | import android.os.IBinder; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.Nullable; | ||||||
|  | 
 | ||||||
|  | import fr.free.nrw.commons.di.CommonsDaggerService; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Handles the Auth service of the App, see AndroidManifests for details | ||||||
|  |  * (Uses Dagger 2 as injector) | ||||||
|  |  */ | ||||||
|  | public class WikiAccountAuthenticatorService extends CommonsDaggerService { | ||||||
|  | 
 | ||||||
|  |     @Nullable | ||||||
|  |     private AbstractAccountAuthenticator authenticator; | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onCreate() { | ||||||
|  |         super.onCreate(); | ||||||
|  |         authenticator = new WikiAccountAuthenticator(this); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Nullable | ||||||
|  |     @Override | ||||||
|  |     public IBinder onBind(Intent intent) { | ||||||
|  |         return authenticator == null ? null : authenticator.getIBinder(); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,22 +0,0 @@ | ||||||
| package fr.free.nrw.commons.auth |  | ||||||
| 
 |  | ||||||
| import android.accounts.AbstractAccountAuthenticator |  | ||||||
| import android.content.Intent |  | ||||||
| import android.os.IBinder |  | ||||||
| import fr.free.nrw.commons.di.CommonsDaggerService |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Handles the Auth service of the App, see AndroidManifests for details |  | ||||||
|  * (Uses Dagger 2 as injector) |  | ||||||
|  */ |  | ||||||
| class WikiAccountAuthenticatorService : CommonsDaggerService() { |  | ||||||
|     private var authenticator: AbstractAccountAuthenticator? = null |  | ||||||
| 
 |  | ||||||
|     override fun onCreate() { |  | ||||||
|         super.onCreate() |  | ||||||
|         authenticator = WikiAccountAuthenticator(this) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override fun onBind(intent: Intent): IBinder? = |  | ||||||
|         authenticator?.iBinder |  | ||||||
| } |  | ||||||
|  | @ -1,217 +0,0 @@ | ||||||
| package fr.free.nrw.commons.auth.csrf |  | ||||||
| 
 |  | ||||||
| import androidx.annotation.VisibleForTesting |  | ||||||
| import fr.free.nrw.commons.auth.SessionManager |  | ||||||
| import fr.free.nrw.commons.auth.login.LoginCallback |  | ||||||
| import fr.free.nrw.commons.auth.login.LoginClient |  | ||||||
| import fr.free.nrw.commons.auth.login.LoginFailedException |  | ||||||
| import fr.free.nrw.commons.auth.login.LoginResult |  | ||||||
| import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse |  | ||||||
| import retrofit2.Call |  | ||||||
| import retrofit2.Response |  | ||||||
| import timber.log.Timber |  | ||||||
| import java.util.concurrent.Callable |  | ||||||
| import java.util.concurrent.Executors.newSingleThreadExecutor |  | ||||||
| 
 |  | ||||||
| class CsrfTokenClient( |  | ||||||
|     private val sessionManager: SessionManager, |  | ||||||
|     private val csrfTokenInterface: CsrfTokenInterface, |  | ||||||
|     private val loginClient: LoginClient, |  | ||||||
|     private val logoutClient: LogoutClient, |  | ||||||
| ) { |  | ||||||
|     private var retries = 0 |  | ||||||
|     private var csrfTokenCall: Call<MwQueryResponse?>? = null |  | ||||||
| 
 |  | ||||||
|     @Throws(Throwable::class) |  | ||||||
|     fun getTokenBlocking(): String { |  | ||||||
|         var token = "" |  | ||||||
|         val userName = sessionManager.userName ?: "" |  | ||||||
|         val password = sessionManager.password ?: "" |  | ||||||
| 
 |  | ||||||
|         for (retry in 0 until MAX_RETRIES_OF_LOGIN_BLOCKING) { |  | ||||||
|             try { |  | ||||||
|                 if (retry > 0) { |  | ||||||
|                     // Log in explicitly |  | ||||||
|                     loginClient.loginBlocking(userName, password) |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 // Get CSRFToken response off the main thread. |  | ||||||
|                 val response = |  | ||||||
|                     newSingleThreadExecutor() |  | ||||||
|                         .submit( |  | ||||||
|                             Callable { |  | ||||||
|                                 csrfTokenInterface.getCsrfTokenCall().execute() |  | ||||||
|                             }, |  | ||||||
|                         ).get() |  | ||||||
| 
 |  | ||||||
|                 if (response |  | ||||||
|                         .body() |  | ||||||
|                         ?.query() |  | ||||||
|                         ?.csrfToken() |  | ||||||
|                         .isNullOrEmpty() |  | ||||||
|                 ) { |  | ||||||
|                     continue |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 token = response.body()!!.query()!!.csrfToken()!! |  | ||||||
|                 if (sessionManager.isUserLoggedIn && token == ANON_TOKEN) { |  | ||||||
|                     throw InvalidLoginTokenException(ANONYMOUS_TOKEN_MESSAGE) |  | ||||||
|                 } |  | ||||||
|                 break |  | ||||||
|             } catch (e: LoginFailedException) { |  | ||||||
|                 throw InvalidLoginTokenException(ANONYMOUS_TOKEN_MESSAGE) |  | ||||||
|             } catch (t: Throwable) { |  | ||||||
|                 Timber.w(t) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if (token.isEmpty() || token == ANON_TOKEN) { |  | ||||||
|             throw InvalidLoginTokenException(ANONYMOUS_TOKEN_MESSAGE) |  | ||||||
|         } |  | ||||||
|         return token |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @VisibleForTesting |  | ||||||
|     fun request( |  | ||||||
|         service: CsrfTokenInterface, |  | ||||||
|         cb: Callback, |  | ||||||
|     ): Call<MwQueryResponse?> = |  | ||||||
|         requestToken( |  | ||||||
|             service, |  | ||||||
|             object : Callback { |  | ||||||
|                 override fun success(token: String?) { |  | ||||||
|                     if (sessionManager.isUserLoggedIn && token == ANON_TOKEN) { |  | ||||||
|                         retryWithLogin(cb) { |  | ||||||
|                             InvalidLoginTokenException(ANONYMOUS_TOKEN_MESSAGE) |  | ||||||
|                         } |  | ||||||
|                     } else { |  | ||||||
|                         cb.success(token) |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 override fun failure(caught: Throwable?) = retryWithLogin(cb) { caught } |  | ||||||
| 
 |  | ||||||
|                 override fun twoFactorPrompt() = cb.twoFactorPrompt() |  | ||||||
| 
 |  | ||||||
|                 override fun emailAuthPrompt() = cb.emailAuthPrompt() |  | ||||||
|             }, |  | ||||||
|         ) |  | ||||||
| 
 |  | ||||||
|     @VisibleForTesting |  | ||||||
|     fun requestToken( |  | ||||||
|         service: CsrfTokenInterface, |  | ||||||
|         cb: Callback, |  | ||||||
|     ): Call<MwQueryResponse?> { |  | ||||||
|         val call = service.getCsrfTokenCall() |  | ||||||
|         call.enqueue( |  | ||||||
|             object : retrofit2.Callback<MwQueryResponse?> { |  | ||||||
|                 override fun onResponse( |  | ||||||
|                     call: Call<MwQueryResponse?>, |  | ||||||
|                     response: Response<MwQueryResponse?>, |  | ||||||
|                 ) { |  | ||||||
|                     if (call.isCanceled) { |  | ||||||
|                         return |  | ||||||
|                     } |  | ||||||
|                     cb.success(response.body()!!.query()!!.csrfToken()) |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 override fun onFailure( |  | ||||||
|                     call: Call<MwQueryResponse?>, |  | ||||||
|                     t: Throwable, |  | ||||||
|                 ) { |  | ||||||
|                     if (call.isCanceled) { |  | ||||||
|                         return |  | ||||||
|                     } |  | ||||||
|                     cb.failure(t) |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|         ) |  | ||||||
|         return call |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private fun retryWithLogin( |  | ||||||
|         callback: Callback, |  | ||||||
|         caught: () -> Throwable?, |  | ||||||
|     ) { |  | ||||||
|         val userName = sessionManager.userName |  | ||||||
|         val password = sessionManager.password |  | ||||||
|         if (retries < MAX_RETRIES && !userName.isNullOrEmpty() && !password.isNullOrEmpty()) { |  | ||||||
|             retries++ |  | ||||||
|             logoutClient.logout() |  | ||||||
|             login(userName, password, callback) { |  | ||||||
|                 Timber.i("retrying...") |  | ||||||
|                 cancel() |  | ||||||
|                 csrfTokenCall = request(csrfTokenInterface, callback) |  | ||||||
|             } |  | ||||||
|         } else { |  | ||||||
|             callback.failure(caught()) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private fun login( |  | ||||||
|         username: String, |  | ||||||
|         password: String, |  | ||||||
|         callback: Callback, |  | ||||||
|         retryCallback: () -> Unit, |  | ||||||
|     ) = loginClient.request( |  | ||||||
|         username, |  | ||||||
|         password, |  | ||||||
|         object : LoginCallback { |  | ||||||
|             override fun success(loginResult: LoginResult) { |  | ||||||
|                 if (loginResult.pass) { |  | ||||||
|                     sessionManager.updateAccount(loginResult) |  | ||||||
|                     retryCallback() |  | ||||||
|                 } else { |  | ||||||
|                     callback.failure(LoginFailedException(loginResult.message)) |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             override fun twoFactorPrompt( |  | ||||||
|                 loginResult: LoginResult, |  | ||||||
|                 caught: Throwable, |  | ||||||
|                 token: String?, |  | ||||||
|             ) = callback.twoFactorPrompt() |  | ||||||
| 
 |  | ||||||
|             override fun emailAuthPrompt( |  | ||||||
|                 loginResult: LoginResult, |  | ||||||
|                 caught: Throwable, |  | ||||||
|                 token: String?, |  | ||||||
|             ) = callback.emailAuthPrompt() |  | ||||||
| 
 |  | ||||||
|             // Should not happen here, but call the callback just in case. |  | ||||||
|             override fun passwordResetPrompt(token: String?) = callback.failure(LoginFailedException("Logged in with temporary password.")) |  | ||||||
| 
 |  | ||||||
|             override fun error(caught: Throwable) = callback.failure(caught) |  | ||||||
|         }, |  | ||||||
|     ) |  | ||||||
| 
 |  | ||||||
|     private fun cancel() { |  | ||||||
|         loginClient.cancel() |  | ||||||
|         if (csrfTokenCall != null) { |  | ||||||
|             csrfTokenCall!!.cancel() |  | ||||||
|             csrfTokenCall = null |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     interface Callback { |  | ||||||
|         fun success(token: String?) |  | ||||||
| 
 |  | ||||||
|         fun failure(caught: Throwable?) |  | ||||||
| 
 |  | ||||||
|         fun twoFactorPrompt() |  | ||||||
| 
 |  | ||||||
|         fun emailAuthPrompt() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     companion object { |  | ||||||
|         private const val ANON_TOKEN = "+\\" |  | ||||||
|         private const val MAX_RETRIES = 1 |  | ||||||
|         private const val MAX_RETRIES_OF_LOGIN_BLOCKING = 2 |  | ||||||
|         const val INVALID_TOKEN_ERROR_MESSAGE = "Invalid token, or login failure." |  | ||||||
|         const val ANONYMOUS_TOKEN_MESSAGE = "App believes we're logged in, but got anonymous token." |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| class InvalidLoginTokenException( |  | ||||||
|     message: String, |  | ||||||
| ) : Exception(message) |  | ||||||
|  | @ -1,13 +0,0 @@ | ||||||
| package fr.free.nrw.commons.auth.csrf |  | ||||||
| 
 |  | ||||||
| import fr.free.nrw.commons.wikidata.WikidataConstants.MW_API_PREFIX |  | ||||||
| import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse |  | ||||||
| import retrofit2.Call |  | ||||||
| import retrofit2.http.GET |  | ||||||
| import retrofit2.http.Headers |  | ||||||
| 
 |  | ||||||
| interface CsrfTokenInterface { |  | ||||||
|     @Headers("Cache-Control: no-cache") |  | ||||||
|     @GET(MW_API_PREFIX + "action=query&meta=tokens&type=csrf") |  | ||||||
|     fun getCsrfTokenCall(): Call<MwQueryResponse?> |  | ||||||
| } |  | ||||||
|  | @ -1,12 +0,0 @@ | ||||||
| package fr.free.nrw.commons.auth.csrf |  | ||||||
| 
 |  | ||||||
| import fr.free.nrw.commons.wikidata.cookies.CommonsCookieStorage |  | ||||||
| import javax.inject.Inject |  | ||||||
| 
 |  | ||||||
| class LogoutClient |  | ||||||
|     @Inject |  | ||||||
|     constructor( |  | ||||||
|         private val store: CommonsCookieStorage, |  | ||||||
|     ) { |  | ||||||
|         fun logout() = store.clear() |  | ||||||
|     } |  | ||||||
|  | @ -1,21 +0,0 @@ | ||||||
| package fr.free.nrw.commons.auth.login |  | ||||||
| 
 |  | ||||||
| interface LoginCallback { |  | ||||||
|     fun success(loginResult: LoginResult) |  | ||||||
| 
 |  | ||||||
|     fun twoFactorPrompt( |  | ||||||
|         loginResult: LoginResult, |  | ||||||
|         caught: Throwable, |  | ||||||
|         token: String?, |  | ||||||
|     ) |  | ||||||
| 
 |  | ||||||
|     fun emailAuthPrompt( |  | ||||||
|         loginResult: LoginResult, |  | ||||||
|         caught: Throwable, |  | ||||||
|         token: String?, |  | ||||||
|     ) |  | ||||||
| 
 |  | ||||||
|     fun passwordResetPrompt(token: String?) |  | ||||||
| 
 |  | ||||||
|     fun error(caught: Throwable) |  | ||||||
| } |  | ||||||
|  | @ -1,276 +0,0 @@ | ||||||
| package fr.free.nrw.commons.auth.login |  | ||||||
| 
 |  | ||||||
| import android.text.TextUtils |  | ||||||
| import fr.free.nrw.commons.auth.login.LoginResult.EmailAuthResult |  | ||||||
| import fr.free.nrw.commons.auth.login.LoginResult.OAuthResult |  | ||||||
| import fr.free.nrw.commons.auth.login.LoginResult.ResetPasswordResult |  | ||||||
| import fr.free.nrw.commons.wikidata.WikidataConstants.WIKIPEDIA_URL |  | ||||||
| import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse |  | ||||||
| import io.reactivex.android.schedulers.AndroidSchedulers |  | ||||||
| import io.reactivex.schedulers.Schedulers |  | ||||||
| import retrofit2.Call |  | ||||||
| import retrofit2.Callback |  | ||||||
| import retrofit2.Response |  | ||||||
| import timber.log.Timber |  | ||||||
| import java.io.IOException |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Responsible for making login related requests to the server. |  | ||||||
|  */ |  | ||||||
| class LoginClient( |  | ||||||
|     private val loginInterface: LoginInterface, |  | ||||||
| ) { |  | ||||||
|     private var tokenCall: Call<MwQueryResponse?>? = null |  | ||||||
|     private var loginCall: Call<LoginResponse?>? = null |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * userLanguage |  | ||||||
|      * It holds the value of the user's device language code. |  | ||||||
|      * For example, if user's device language is English it will hold En |  | ||||||
|      * The value will be fetched when the user clicks Login Button in the LoginActivity |  | ||||||
|      */ |  | ||||||
|     private var userLanguage = "" |  | ||||||
| 
 |  | ||||||
|     private fun getLoginToken() = loginInterface.getLoginToken() |  | ||||||
| 
 |  | ||||||
|     fun request( |  | ||||||
|         userName: String, |  | ||||||
|         password: String, |  | ||||||
|         cb: LoginCallback, |  | ||||||
|     ) { |  | ||||||
|         cancel() |  | ||||||
| 
 |  | ||||||
|         tokenCall = getLoginToken() |  | ||||||
|         tokenCall!!.enqueue( |  | ||||||
|             object : Callback<MwQueryResponse?> { |  | ||||||
|                 override fun onResponse( |  | ||||||
|                     call: Call<MwQueryResponse?>, |  | ||||||
|                     response: Response<MwQueryResponse?>, |  | ||||||
|                 ) { |  | ||||||
|                     login( |  | ||||||
|                         userName, |  | ||||||
|                         password, |  | ||||||
|                         null, |  | ||||||
|                         null, |  | ||||||
|                         null, |  | ||||||
|                         response.body()!!.query()!!.loginToken(), |  | ||||||
|                         userLanguage, |  | ||||||
|                         cb, |  | ||||||
|                     ) |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 override fun onFailure( |  | ||||||
|                     call: Call<MwQueryResponse?>, |  | ||||||
|                     caught: Throwable, |  | ||||||
|                 ) { |  | ||||||
|                     if (call.isCanceled) { |  | ||||||
|                         return |  | ||||||
|                     } |  | ||||||
|                     cb.error(caught) |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|         ) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fun login( |  | ||||||
|         userName: String, |  | ||||||
|         password: String, |  | ||||||
|         retypedPassword: String?, |  | ||||||
|         twoFactorCode: String?, |  | ||||||
|         emailAuthCode: String?, |  | ||||||
|         loginToken: String?, |  | ||||||
|         userLanguage: String, |  | ||||||
|         cb: LoginCallback, |  | ||||||
|     ) { |  | ||||||
|         this.userLanguage = userLanguage |  | ||||||
| 
 |  | ||||||
|         loginCall = |  | ||||||
|             if (twoFactorCode.isNullOrEmpty() && emailAuthCode.isNullOrEmpty() && retypedPassword.isNullOrEmpty()) { |  | ||||||
|                 loginInterface.postLogIn(userName, password, loginToken, userLanguage, WIKIPEDIA_URL) |  | ||||||
|             } else { |  | ||||||
|                 loginInterface.postLogIn( |  | ||||||
|                     userName, |  | ||||||
|                     password, |  | ||||||
|                     retypedPassword, |  | ||||||
|                     twoFactorCode, |  | ||||||
|                     emailAuthCode, |  | ||||||
|                     loginToken, |  | ||||||
|                     userLanguage, |  | ||||||
|                     true, |  | ||||||
|                 ) |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|         loginCall!!.enqueue( |  | ||||||
|             object : Callback<LoginResponse?> { |  | ||||||
|                 override fun onResponse( |  | ||||||
|                     call: Call<LoginResponse?>, |  | ||||||
|                     response: Response<LoginResponse?>, |  | ||||||
|                 ) { |  | ||||||
|                     val loginResult = response.body()?.toLoginResult(password) |  | ||||||
|                     if (loginResult != null) { |  | ||||||
|                         if (loginResult.pass && !loginResult.userName.isNullOrEmpty()) { |  | ||||||
|                             // The server could do some transformations on user names, e.g. on some |  | ||||||
|                             // wikis is uppercases the first letter. |  | ||||||
|                             getExtendedInfo(loginResult.userName, loginResult, cb) |  | ||||||
|                         } else if ("UI" == loginResult.status) { |  | ||||||
|                             when (loginResult) { |  | ||||||
|                                 is OAuthResult -> |  | ||||||
|                                     cb.twoFactorPrompt( |  | ||||||
|                                         loginResult, |  | ||||||
|                                         LoginFailedException(loginResult.message), |  | ||||||
|                                         loginToken, |  | ||||||
|                                     ) |  | ||||||
| 
 |  | ||||||
|                                 is EmailAuthResult -> |  | ||||||
|                                     cb.emailAuthPrompt( |  | ||||||
|                                         loginResult, |  | ||||||
|                                         LoginFailedException(loginResult.message), |  | ||||||
|                                         loginToken |  | ||||||
|                                     ) |  | ||||||
| 
 |  | ||||||
|                                 is ResetPasswordResult -> cb.passwordResetPrompt(loginToken) |  | ||||||
| 
 |  | ||||||
|                                 is LoginResult.Result -> |  | ||||||
|                                     cb.error( |  | ||||||
|                                         LoginFailedException(loginResult.message), |  | ||||||
|                                     ) |  | ||||||
|                             } |  | ||||||
|                         } else { |  | ||||||
|                             cb.error(LoginFailedException(loginResult.message)) |  | ||||||
|                         } |  | ||||||
|                     } else { |  | ||||||
|                         cb.error(IOException("Login failed. Unexpected response.")) |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 override fun onFailure( |  | ||||||
|                     call: Call<LoginResponse?>, |  | ||||||
|                     t: Throwable, |  | ||||||
|                 ) { |  | ||||||
|                     if (call.isCanceled) { |  | ||||||
|                         return |  | ||||||
|                     } |  | ||||||
|                     cb.error(t) |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|         ) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fun doLogin( |  | ||||||
|         username: String, |  | ||||||
|         password: String, |  | ||||||
|         lastLoginResult: LoginResult?, |  | ||||||
|         twoFactorCode: String, |  | ||||||
|         userLanguage: String, |  | ||||||
|         loginCallback: LoginCallback, |  | ||||||
|     ) { |  | ||||||
|         getLoginToken().enqueue( |  | ||||||
|             object : Callback<MwQueryResponse?> { |  | ||||||
|                 override fun onResponse( |  | ||||||
|                     call: Call<MwQueryResponse?>, |  | ||||||
|                     response: Response<MwQueryResponse?>, |  | ||||||
|                 ) = if (response.isSuccessful) { |  | ||||||
|                     val loginToken = response.body()?.query()?.loginToken() |  | ||||||
|                     loginToken?.let { |  | ||||||
|                         login(username, password, null, |  | ||||||
|                             if (lastLoginResult is OAuthResult) twoFactorCode else null, |  | ||||||
|                             if (lastLoginResult is EmailAuthResult) twoFactorCode else null, |  | ||||||
|                             it, userLanguage, loginCallback) |  | ||||||
|                     } ?: run { |  | ||||||
|                         loginCallback.error(IOException("Failed to retrieve login token")) |  | ||||||
|                     } |  | ||||||
|                 } else { |  | ||||||
|                     loginCallback.error(IOException("Failed to retrieve login token")) |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 override fun onFailure( |  | ||||||
|                     call: Call<MwQueryResponse?>, |  | ||||||
|                     t: Throwable, |  | ||||||
|                 ) { |  | ||||||
|                     loginCallback.error(t) |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|         ) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Throws(Throwable::class) |  | ||||||
|     fun loginBlocking( |  | ||||||
|         userName: String, |  | ||||||
|         password: String, |  | ||||||
|         twoFactorCode: String? = null, |  | ||||||
|         emailAuthCode: String? = null |  | ||||||
|     ) { |  | ||||||
|         val tokenResponse = getLoginToken().execute() |  | ||||||
|         if (tokenResponse |  | ||||||
|                 .body() |  | ||||||
|                 ?.query() |  | ||||||
|                 ?.loginToken() |  | ||||||
|                 .isNullOrEmpty() |  | ||||||
|         ) { |  | ||||||
|             throw IOException("Unexpected response when getting login token.") |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         val loginToken = tokenResponse.body()?.query()?.loginToken() |  | ||||||
|         val tempLoginCall = |  | ||||||
|             if (twoFactorCode.isNullOrEmpty() && emailAuthCode.isNullOrEmpty()) { |  | ||||||
|                 loginInterface.postLogIn(userName, password, loginToken, userLanguage, WIKIPEDIA_URL) |  | ||||||
|             } else { |  | ||||||
|                 loginInterface.postLogIn( |  | ||||||
|                     userName, |  | ||||||
|                     password, |  | ||||||
|                     null, |  | ||||||
|                     twoFactorCode, |  | ||||||
|                     emailAuthCode, |  | ||||||
|                     loginToken, |  | ||||||
|                     userLanguage, |  | ||||||
|                     true, |  | ||||||
|                 ) |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|         val response = tempLoginCall.execute() |  | ||||||
|         val loginResponse = response.body() ?: throw IOException("Unexpected response when logging in.") |  | ||||||
|         val loginResult = loginResponse.toLoginResult(password) ?: throw IOException("Unexpected response when logging in.") |  | ||||||
| 
 |  | ||||||
|         if ("UI" == loginResult.status) { |  | ||||||
|             if (loginResult is OAuthResult || loginResult is EmailAuthResult) { |  | ||||||
|                 // TODO: Find a better way to boil up the warning about 2FA |  | ||||||
|                 throw LoginFailedException(loginResult.message) |  | ||||||
|             } |  | ||||||
|             throw LoginFailedException(loginResult.message) |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if (!loginResult.pass || TextUtils.isEmpty(loginResult.userName)) { |  | ||||||
|             throw LoginFailedException(loginResult.message) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private fun getExtendedInfo( |  | ||||||
|         userName: String, |  | ||||||
|         loginResult: LoginResult, |  | ||||||
|         cb: LoginCallback, |  | ||||||
|     ) = loginInterface |  | ||||||
|         .getUserInfo(userName) |  | ||||||
|         .subscribeOn(Schedulers.io()) |  | ||||||
|         .observeOn(AndroidSchedulers.mainThread()) |  | ||||||
|         .subscribe({ response: MwQueryResponse? -> |  | ||||||
|             loginResult.userId = response?.query()?.userInfo()?.id() ?: 0 |  | ||||||
|             loginResult.groups = |  | ||||||
|                 response?.query()?.getUserResponse(userName)?.getGroups() ?: emptySet() |  | ||||||
|             cb.success(loginResult) |  | ||||||
|         }, { caught: Throwable -> |  | ||||||
|             Timber.e(caught, "Login succeeded but getting group information failed. ") |  | ||||||
|             cb.error(caught) |  | ||||||
|         }) |  | ||||||
| 
 |  | ||||||
|     fun cancel() { |  | ||||||
|         tokenCall?.let { |  | ||||||
|             it.cancel() |  | ||||||
|             tokenCall = null |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         loginCall?.let { |  | ||||||
|             it.cancel() |  | ||||||
|             loginCall = null |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -1,5 +0,0 @@ | ||||||
| package fr.free.nrw.commons.auth.login |  | ||||||
| 
 |  | ||||||
| class LoginFailedException( |  | ||||||
|     message: String?, |  | ||||||
| ) : Throwable(message) |  | ||||||
|  | @ -1,48 +0,0 @@ | ||||||
| package fr.free.nrw.commons.auth.login |  | ||||||
| 
 |  | ||||||
| import fr.free.nrw.commons.wikidata.WikidataConstants.MW_API_PREFIX |  | ||||||
| import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse |  | ||||||
| import io.reactivex.Observable |  | ||||||
| import retrofit2.Call |  | ||||||
| import retrofit2.http.Field |  | ||||||
| import retrofit2.http.FormUrlEncoded |  | ||||||
| import retrofit2.http.GET |  | ||||||
| import retrofit2.http.Headers |  | ||||||
| import retrofit2.http.POST |  | ||||||
| import retrofit2.http.Query |  | ||||||
| 
 |  | ||||||
| interface LoginInterface { |  | ||||||
|     @Headers("Cache-Control: no-cache") |  | ||||||
|     @GET(MW_API_PREFIX + "action=query&meta=tokens&type=login") |  | ||||||
|     fun getLoginToken(): Call<MwQueryResponse?> |  | ||||||
| 
 |  | ||||||
|     @Headers("Cache-Control: no-cache") |  | ||||||
|     @FormUrlEncoded |  | ||||||
|     @POST(MW_API_PREFIX + "action=clientlogin&rememberMe=") |  | ||||||
|     fun postLogIn( |  | ||||||
|         @Field("username") user: String?, |  | ||||||
|         @Field("password") pass: String?, |  | ||||||
|         @Field("logintoken") token: String?, |  | ||||||
|         @Field("uselang") userLanguage: String?, |  | ||||||
|         @Field("loginreturnurl") url: String?, |  | ||||||
|     ): Call<LoginResponse?> |  | ||||||
| 
 |  | ||||||
|     @Headers("Cache-Control: no-cache") |  | ||||||
|     @FormUrlEncoded |  | ||||||
|     @POST(MW_API_PREFIX + "action=clientlogin&rememberMe=") |  | ||||||
|     fun postLogIn( |  | ||||||
|         @Field("username") user: String?, |  | ||||||
|         @Field("password") pass: String?, |  | ||||||
|         @Field("retype") retypedPass: String?, |  | ||||||
|         @Field("OATHToken") twoFactorCode: String?, |  | ||||||
|         @Field("token") emailAuthToken: String?, |  | ||||||
|         @Field("logintoken") loginToken: String?, |  | ||||||
|         @Field("uselang") userLanguage: String?, |  | ||||||
|         @Field("logincontinue") loginContinue: Boolean, |  | ||||||
|     ): Call<LoginResponse?> |  | ||||||
| 
 |  | ||||||
|     @GET(MW_API_PREFIX + "action=query&meta=userinfo&list=users&usprop=groups|cancreate") |  | ||||||
|     fun getUserInfo( |  | ||||||
|         @Query("ususers") userName: String, |  | ||||||
|     ): Observable<MwQueryResponse?> |  | ||||||
| } |  | ||||||
|  | @ -1,64 +0,0 @@ | ||||||
| package fr.free.nrw.commons.auth.login |  | ||||||
| 
 |  | ||||||
| import com.google.gson.annotations.SerializedName |  | ||||||
| import fr.free.nrw.commons.auth.login.LoginResult.OAuthResult |  | ||||||
| import fr.free.nrw.commons.auth.login.LoginResult.EmailAuthResult |  | ||||||
| import fr.free.nrw.commons.auth.login.LoginResult.ResetPasswordResult |  | ||||||
| import fr.free.nrw.commons.auth.login.LoginResult.Result |  | ||||||
| import fr.free.nrw.commons.wikidata.mwapi.MwServiceError |  | ||||||
| 
 |  | ||||||
| class LoginResponse { |  | ||||||
|     @SerializedName("error") |  | ||||||
|     val error: MwServiceError? = null |  | ||||||
| 
 |  | ||||||
|     @SerializedName("clientlogin") |  | ||||||
|     private val clientLogin: ClientLogin? = null |  | ||||||
| 
 |  | ||||||
|     fun toLoginResult(password: String): LoginResult? = clientLogin?.toLoginResult(password) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| internal class ClientLogin { |  | ||||||
|     private val status: String? = null |  | ||||||
|     private val requests: List<Request>? = null |  | ||||||
|     private val message: String? = null |  | ||||||
| 
 |  | ||||||
|     @SerializedName("username") |  | ||||||
|     private val userName: String? = null |  | ||||||
| 
 |  | ||||||
|     fun toLoginResult(password: String): LoginResult { |  | ||||||
|         var userMessage = message |  | ||||||
|         if ("UI" == status) { |  | ||||||
|             requests?.forEach { request -> |  | ||||||
|                 request.id()?.let { |  | ||||||
|                     if (it.endsWith("TOTPAuthenticationRequest")) { |  | ||||||
|                         return OAuthResult(status, userName, password, message) |  | ||||||
|                     } else if (it.endsWith("EmailAuthAuthenticationRequest")) { |  | ||||||
|                         return EmailAuthResult(status, userName, password, message) |  | ||||||
|                     } else if (it.endsWith("PasswordAuthenticationRequest")) { |  | ||||||
|                         return ResetPasswordResult(status, userName, password, message) |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } else if ("PASS" != status && "FAIL" != status) { |  | ||||||
|             // TODO: String resource -- Looks like needed for others in this class too |  | ||||||
|             userMessage = "An unknown error occurred." |  | ||||||
|         } |  | ||||||
|         return Result(status ?: "", userName, password, userMessage) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| internal class Request { |  | ||||||
|     private val id: String? = null |  | ||||||
|     private val required: String? = null |  | ||||||
|     private val provider: String? = null |  | ||||||
|     private val account: String? = null |  | ||||||
|     internal val fields: Map<String, RequestField>? = null |  | ||||||
| 
 |  | ||||||
|     fun id(): String? = id |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| internal class RequestField { |  | ||||||
|     private val type: String? = null |  | ||||||
|     private val label: String? = null |  | ||||||
|     internal val help: String? = null |  | ||||||
| } |  | ||||||
|  | @ -1,40 +0,0 @@ | ||||||
| package fr.free.nrw.commons.auth.login |  | ||||||
| 
 |  | ||||||
| sealed class LoginResult( |  | ||||||
|     val status: String, |  | ||||||
|     val userName: String?, |  | ||||||
|     val password: String?, |  | ||||||
|     val message: String?, |  | ||||||
| ) { |  | ||||||
|     var userId = 0 |  | ||||||
|     var groups = emptySet<String>() |  | ||||||
|     val pass: Boolean get() = "PASS" == status |  | ||||||
| 
 |  | ||||||
|     class Result( |  | ||||||
|         status: String, |  | ||||||
|         userName: String?, |  | ||||||
|         password: String?, |  | ||||||
|         message: String?, |  | ||||||
|     ) : LoginResult(status, userName, password, message) |  | ||||||
| 
 |  | ||||||
|     class OAuthResult( |  | ||||||
|         status: String, |  | ||||||
|         userName: String?, |  | ||||||
|         password: String?, |  | ||||||
|         message: String?, |  | ||||||
|     ) : LoginResult(status, userName, password, message) |  | ||||||
| 
 |  | ||||||
|     class EmailAuthResult( |  | ||||||
|         status: String, |  | ||||||
|         userName: String?, |  | ||||||
|         password: String?, |  | ||||||
|         message: String?, |  | ||||||
|     ) : LoginResult(status, userName, password, message) |  | ||||||
| 
 |  | ||||||
|     class ResetPasswordResult( |  | ||||||
|         status: String, |  | ||||||
|         userName: String?, |  | ||||||
|         password: String?, |  | ||||||
|         message: String?, |  | ||||||
|     ) : LoginResult(status, userName, password, message) |  | ||||||
| } |  | ||||||
|  | @ -0,0 +1,111 @@ | ||||||
|  | package fr.free.nrw.commons.bookmarks; | ||||||
|  | 
 | ||||||
|  | import android.os.Bundle; | ||||||
|  | import android.view.LayoutInflater; | ||||||
|  | import android.view.View; | ||||||
|  | import android.view.ViewGroup; | ||||||
|  | 
 | ||||||
|  | import android.widget.FrameLayout; | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.annotation.Nullable; | ||||||
|  | import androidx.fragment.app.FragmentManager; | ||||||
|  | 
 | ||||||
|  | import com.google.android.material.tabs.TabLayout; | ||||||
|  | 
 | ||||||
|  | import fr.free.nrw.commons.contributions.MainActivity; | ||||||
|  | import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; | ||||||
|  | import fr.free.nrw.commons.explore.ParentViewPager; | ||||||
|  | import fr.free.nrw.commons.kvstore.JsonKvStore; | ||||||
|  | import fr.free.nrw.commons.theme.BaseActivity; | ||||||
|  | import javax.inject.Inject; | ||||||
|  | 
 | ||||||
|  | import butterknife.BindView; | ||||||
|  | import butterknife.ButterKnife; | ||||||
|  | import fr.free.nrw.commons.R; | ||||||
|  | import fr.free.nrw.commons.contributions.ContributionController; | ||||||
|  | import javax.inject.Named; | ||||||
|  | 
 | ||||||
|  | public class BookmarkFragment extends CommonsDaggerSupportFragment { | ||||||
|  | 
 | ||||||
|  |     private FragmentManager supportFragmentManager; | ||||||
|  |     private BookmarksPagerAdapter adapter; | ||||||
|  |     @BindView(R.id.viewPagerBookmarks) | ||||||
|  |     ParentViewPager viewPager; | ||||||
|  |     @BindView(R.id.tab_layout) | ||||||
|  |     TabLayout tabLayout; | ||||||
|  |     @BindView(R.id.fragmentContainer) | ||||||
|  |     FrameLayout fragmentContainer; | ||||||
|  | 
 | ||||||
|  |     @Inject | ||||||
|  |     ContributionController controller; | ||||||
|  |     /** | ||||||
|  |      * To check if the user is loggedIn or not. | ||||||
|  |      */ | ||||||
|  |     @Inject | ||||||
|  |     @Named("default_preferences") | ||||||
|  |     public | ||||||
|  |     JsonKvStore applicationKvStore; | ||||||
|  | 
 | ||||||
|  |     @NonNull | ||||||
|  |     public static BookmarkFragment newInstance() { | ||||||
|  |         BookmarkFragment fragment = new BookmarkFragment(); | ||||||
|  |         fragment.setRetainInstance(true); | ||||||
|  |         return fragment; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void setScroll(boolean canScroll) { | ||||||
|  |         viewPager.setCanScroll(canScroll); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onCreate(@Nullable final Bundle savedInstanceState) { | ||||||
|  |         super.onCreate(savedInstanceState); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Nullable | ||||||
|  |     @Override | ||||||
|  |     public View onCreateView(@NonNull final LayoutInflater inflater, | ||||||
|  |         @Nullable final ViewGroup container, | ||||||
|  |         @Nullable final Bundle savedInstanceState) { | ||||||
|  |         super.onCreateView(inflater, container, savedInstanceState); | ||||||
|  |         View view = inflater.inflate(R.layout.fragment_bookmarks, container, false); | ||||||
|  |         ButterKnife.bind(this, view); | ||||||
|  | 
 | ||||||
|  |         // Activity can call methods in the fragment by acquiring a | ||||||
|  |         // reference to the Fragment from FragmentManager, using findFragmentById() | ||||||
|  |         supportFragmentManager = getChildFragmentManager(); | ||||||
|  | 
 | ||||||
|  |         adapter = new BookmarksPagerAdapter(supportFragmentManager, getContext(), | ||||||
|  |             applicationKvStore.getBoolean("login_skipped")); | ||||||
|  |         viewPager.setAdapter(adapter); | ||||||
|  |         tabLayout.setupWithViewPager(viewPager); | ||||||
|  | 
 | ||||||
|  |         ((MainActivity) getActivity()).showTabs(); | ||||||
|  |         ((BaseActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false); | ||||||
|  | 
 | ||||||
|  |         setupTabLayout(); | ||||||
|  |         return view; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * This method sets up the tab layout. If the adapter has only one element it sets the | ||||||
|  |      * visibility of tabLayout to gone. | ||||||
|  |      */ | ||||||
|  |     public void setupTabLayout() { | ||||||
|  |         tabLayout.setVisibility(View.VISIBLE); | ||||||
|  |         if (adapter.getCount() == 1) { | ||||||
|  |             tabLayout.setVisibility(View.GONE); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     public void onBackPressed() { | ||||||
|  |         if (((BookmarkListRootFragment) (adapter.getItem(tabLayout.getSelectedTabPosition()))) | ||||||
|  |             .backPressed()) { | ||||||
|  |             // The event is handled internally by the adapter , no further action required. | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         // Event is not handled by the adapter ( performed back action ) change action bar. | ||||||
|  |         ((BaseActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,98 +0,0 @@ | ||||||
| package fr.free.nrw.commons.bookmarks |  | ||||||
| 
 |  | ||||||
| import android.os.Bundle |  | ||||||
| import android.view.LayoutInflater |  | ||||||
| import android.view.View |  | ||||||
| import android.view.ViewGroup |  | ||||||
| import fr.free.nrw.commons.contributions.ContributionController |  | ||||||
| import fr.free.nrw.commons.contributions.MainActivity |  | ||||||
| import fr.free.nrw.commons.databinding.FragmentBookmarksBinding |  | ||||||
| import fr.free.nrw.commons.di.CommonsDaggerSupportFragment |  | ||||||
| import fr.free.nrw.commons.kvstore.JsonKvStore |  | ||||||
| import fr.free.nrw.commons.theme.BaseActivity |  | ||||||
| import javax.inject.Inject |  | ||||||
| import javax.inject.Named |  | ||||||
| 
 |  | ||||||
| class BookmarkFragment : CommonsDaggerSupportFragment() { |  | ||||||
|     private var adapter: BookmarksPagerAdapter? = null |  | ||||||
| 
 |  | ||||||
|     @JvmField |  | ||||||
|     var binding: FragmentBookmarksBinding? = null |  | ||||||
| 
 |  | ||||||
|     @JvmField |  | ||||||
|     @Inject |  | ||||||
|     var controller: ContributionController? = null |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * To check if the user is loggedIn or not. |  | ||||||
|      */ |  | ||||||
|     @JvmField |  | ||||||
|     @Inject |  | ||||||
|     @Named("default_preferences") |  | ||||||
|     var applicationKvStore: JsonKvStore? = null |  | ||||||
| 
 |  | ||||||
|     fun setScroll(canScroll: Boolean) { |  | ||||||
|         binding?.let { |  | ||||||
|             it.viewPagerBookmarks.canScroll = canScroll |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override fun onCreateView( |  | ||||||
|         inflater: LayoutInflater, |  | ||||||
|         container: ViewGroup?, |  | ||||||
|         savedInstanceState: Bundle? |  | ||||||
|     ): View { |  | ||||||
|         super.onCreateView(inflater, container, savedInstanceState) |  | ||||||
|         binding = FragmentBookmarksBinding.inflate(inflater, container, false) |  | ||||||
| 
 |  | ||||||
|         // Activity can call methods in the fragment by acquiring a |  | ||||||
|         // reference to the Fragment from FragmentManager, using findFragmentById() |  | ||||||
|         val supportFragmentManager = childFragmentManager |  | ||||||
| 
 |  | ||||||
|         adapter = BookmarksPagerAdapter( |  | ||||||
|             supportFragmentManager, requireContext(), |  | ||||||
|             applicationKvStore!!.getBoolean("login_skipped") |  | ||||||
|         ) |  | ||||||
|         binding!!.viewPagerBookmarks.adapter = adapter |  | ||||||
|         binding!!.tabLayout.setupWithViewPager(binding!!.viewPagerBookmarks) |  | ||||||
| 
 |  | ||||||
|         (requireActivity() as MainActivity).showTabs() |  | ||||||
|         (requireActivity() as BaseActivity).supportActionBar!!.setDisplayHomeAsUpEnabled(false) |  | ||||||
| 
 |  | ||||||
|         setupTabLayout() |  | ||||||
|         return binding!!.root |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * This method sets up the tab layout. If the adapter has only one element it sets the |  | ||||||
|      * visibility of tabLayout to gone. |  | ||||||
|      */ |  | ||||||
|     fun setupTabLayout() { |  | ||||||
|         binding!!.tabLayout.visibility = View.VISIBLE |  | ||||||
|         if (adapter!!.count == 1) { |  | ||||||
|             binding!!.tabLayout.visibility = View.GONE |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     fun onBackPressed() { |  | ||||||
|         if (((adapter!!.getItem(binding!!.tabLayout.selectedTabPosition)) as BookmarkListRootFragment).backPressed()) { |  | ||||||
|             // The event is handled internally by the adapter , no further action required. |  | ||||||
|             return |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         // Event is not handled by the adapter ( performed back action ) change action bar. |  | ||||||
|         (requireActivity() as BaseActivity).supportActionBar!!.setDisplayHomeAsUpEnabled(false) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override fun onDestroy() { |  | ||||||
|         super.onDestroy() |  | ||||||
|         binding = null |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     companion object { |  | ||||||
|         fun newInstance(): BookmarkFragment = BookmarkFragment().apply { |  | ||||||
|             retainInstance = true |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -0,0 +1,256 @@ | ||||||
|  | package fr.free.nrw.commons.bookmarks; | ||||||
|  | 
 | ||||||
|  | import android.content.Context; | ||||||
|  | import android.os.Bundle; | ||||||
|  | import android.util.Log; | ||||||
|  | import android.view.LayoutInflater; | ||||||
|  | import android.view.View; | ||||||
|  | import android.view.ViewGroup; | ||||||
|  | import android.widget.AdapterView; | ||||||
|  | import android.widget.FrameLayout; | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.annotation.Nullable; | ||||||
|  | import androidx.fragment.app.Fragment; | ||||||
|  | import androidx.fragment.app.FragmentManager; | ||||||
|  | import butterknife.BindView; | ||||||
|  | import butterknife.ButterKnife; | ||||||
|  | import fr.free.nrw.commons.Media; | ||||||
|  | import fr.free.nrw.commons.R; | ||||||
|  | import fr.free.nrw.commons.bookmarks.items.BookmarkItemsFragment; | ||||||
|  | import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsFragment; | ||||||
|  | import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment; | ||||||
|  | import fr.free.nrw.commons.category.CategoryImagesCallback; | ||||||
|  | import fr.free.nrw.commons.category.GridViewAdapter; | ||||||
|  | import fr.free.nrw.commons.contributions.MainActivity; | ||||||
|  | import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; | ||||||
|  | import fr.free.nrw.commons.media.MediaDetailPagerFragment; | ||||||
|  | import fr.free.nrw.commons.navtab.NavTab; | ||||||
|  | import java.util.ArrayList; | ||||||
|  | import java.util.Iterator; | ||||||
|  | 
 | ||||||
|  | public class BookmarkListRootFragment extends CommonsDaggerSupportFragment implements | ||||||
|  |     FragmentManager.OnBackStackChangedListener, | ||||||
|  |     MediaDetailPagerFragment.MediaDetailProvider, | ||||||
|  |     AdapterView.OnItemClickListener, CategoryImagesCallback { | ||||||
|  | 
 | ||||||
|  |     private MediaDetailPagerFragment mediaDetails; | ||||||
|  |     //private BookmarkPicturesFragment bookmarkPicturesFragment; | ||||||
|  |     private BookmarkLocationsFragment bookmarkLocationsFragment; | ||||||
|  |     public Fragment listFragment; | ||||||
|  |     private BookmarksPagerAdapter bookmarksPagerAdapter; | ||||||
|  | 
 | ||||||
|  |     @BindView(R.id.explore_container) | ||||||
|  |     FrameLayout container; | ||||||
|  | 
 | ||||||
|  |     public BookmarkListRootFragment() { | ||||||
|  |         //empty constructor necessary otherwise crashes on recreate | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public BookmarkListRootFragment(Bundle bundle, BookmarksPagerAdapter bookmarksPagerAdapter) { | ||||||
|  |         String title = bundle.getString("categoryName"); | ||||||
|  |         int order = bundle.getInt("order"); | ||||||
|  |         final int orderItem = bundle.getInt("orderItem"); | ||||||
|  |         if (order == 0) { | ||||||
|  |             listFragment = new BookmarkPicturesFragment(); | ||||||
|  |         } else { | ||||||
|  |             listFragment = new BookmarkLocationsFragment(); | ||||||
|  |             if(orderItem == 2) { | ||||||
|  |                 listFragment = new BookmarkItemsFragment(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         Bundle featuredArguments = new Bundle(); | ||||||
|  |         featuredArguments.putString("categoryName", title); | ||||||
|  |         listFragment.setArguments(featuredArguments); | ||||||
|  |         this.bookmarksPagerAdapter = bookmarksPagerAdapter; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Nullable | ||||||
|  |     @Override | ||||||
|  |     public View onCreateView(@NonNull final LayoutInflater inflater, | ||||||
|  |         @Nullable final ViewGroup container, | ||||||
|  |         @Nullable final Bundle savedInstanceState) { | ||||||
|  |         super.onCreate(savedInstanceState); | ||||||
|  |         View view = inflater.inflate(R.layout.fragment_featured_root, container, false); | ||||||
|  |         ButterKnife.bind(this, view); | ||||||
|  |         return view; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { | ||||||
|  |         super.onViewCreated(view, savedInstanceState); | ||||||
|  |         if (savedInstanceState == null) { | ||||||
|  |             setFragment(listFragment, mediaDetails); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void setFragment(Fragment fragment, Fragment otherFragment) { | ||||||
|  |         if (fragment.isAdded() && otherFragment != null) { | ||||||
|  |             getChildFragmentManager() | ||||||
|  |                 .beginTransaction() | ||||||
|  |                 .hide(otherFragment) | ||||||
|  |                 .show(fragment) | ||||||
|  |                 .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") | ||||||
|  |                 .commit(); | ||||||
|  |             getChildFragmentManager().executePendingTransactions(); | ||||||
|  |         } else if (fragment.isAdded() && otherFragment == null) { | ||||||
|  |             getChildFragmentManager() | ||||||
|  |                 .beginTransaction() | ||||||
|  |                 .show(fragment) | ||||||
|  |                 .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") | ||||||
|  |                 .commit(); | ||||||
|  |             getChildFragmentManager().executePendingTransactions(); | ||||||
|  |         } else if (!fragment.isAdded() && otherFragment != null) { | ||||||
|  |             getChildFragmentManager() | ||||||
|  |                 .beginTransaction() | ||||||
|  |                 .hide(otherFragment) | ||||||
|  |                 .add(R.id.explore_container, fragment) | ||||||
|  |                 .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") | ||||||
|  |                 .commit(); | ||||||
|  |             getChildFragmentManager().executePendingTransactions(); | ||||||
|  |         } else if (!fragment.isAdded()) { | ||||||
|  |             getChildFragmentManager() | ||||||
|  |                 .beginTransaction() | ||||||
|  |                 .replace(R.id.explore_container, fragment) | ||||||
|  |                 .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") | ||||||
|  |                 .commit(); | ||||||
|  |             getChildFragmentManager().executePendingTransactions(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void removeFragment(Fragment fragment) { | ||||||
|  |         getChildFragmentManager() | ||||||
|  |             .beginTransaction() | ||||||
|  |             .remove(fragment) | ||||||
|  |             .commit(); | ||||||
|  |         getChildFragmentManager().executePendingTransactions(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onAttach(final Context context) { | ||||||
|  |         super.onAttach(context); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onMediaClicked(int position) { | ||||||
|  |         Log.d("deneme8", "on media clicked"); | ||||||
|  |     /*container.setVisibility(View.VISIBLE); | ||||||
|  |     ((BookmarkFragment)getParentFragment()).tabLayout.setVisibility(View.GONE); | ||||||
|  |     mediaDetails = new MediaDetailPagerFragment(false, true, position); | ||||||
|  |     setFragment(mediaDetails, bookmarkPicturesFragment);*/ | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * This method is called mediaDetailPagerFragment. It returns the Media Object at that Index | ||||||
|  |      * | ||||||
|  |      * @param i It is the index of which media object is to be returned which is same as current | ||||||
|  |      *          index of viewPager. | ||||||
|  |      * @return Media Object | ||||||
|  |      */ | ||||||
|  |     @Override | ||||||
|  |     public Media getMediaAtPosition(int i) { | ||||||
|  |         if (bookmarksPagerAdapter.getMediaAdapter() == null) { | ||||||
|  |             // not yet ready to return data | ||||||
|  |             return null; | ||||||
|  |         } else { | ||||||
|  |             return (Media) bookmarksPagerAdapter.getMediaAdapter().getItem(i); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * This method is called on from getCount of MediaDetailPagerFragment The viewpager will contain | ||||||
|  |      * same number of media items as that of media elements in adapter. | ||||||
|  |      * | ||||||
|  |      * @return Total Media count in the adapter | ||||||
|  |      */ | ||||||
|  |     @Override | ||||||
|  |     public int getTotalMediaCount() { | ||||||
|  |         if (bookmarksPagerAdapter.getMediaAdapter() == null) { | ||||||
|  |             return 0; | ||||||
|  |         } | ||||||
|  |         return bookmarksPagerAdapter.getMediaAdapter().getCount(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public Integer getContributionStateAt(int position) { | ||||||
|  |         return null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Reload media detail fragment once media is nominated | ||||||
|  |      * | ||||||
|  |      * @param index item position that has been nominated | ||||||
|  |      */ | ||||||
|  |     @Override | ||||||
|  |     public void refreshNominatedMedia(int index) { | ||||||
|  |         if (mediaDetails != null && !listFragment.isVisible()) { | ||||||
|  |             removeFragment(mediaDetails); | ||||||
|  |             mediaDetails = new MediaDetailPagerFragment(false, true); | ||||||
|  |             ((BookmarkFragment) getParentFragment()).setScroll(false); | ||||||
|  |             setFragment(mediaDetails, listFragment); | ||||||
|  |             mediaDetails.showImage(index); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * This method is called on success of API call for featured images or mobile uploads. The | ||||||
|  |      * viewpager will notified that number of items have changed. | ||||||
|  |      */ | ||||||
|  |     @Override | ||||||
|  |     public void viewPagerNotifyDataSetChanged() { | ||||||
|  |         if (mediaDetails != null) { | ||||||
|  |             mediaDetails.notifyDataSetChanged(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public boolean backPressed() { | ||||||
|  |         //check mediaDetailPage fragment is not null then we check mediaDetail.is Visible or not to avoid NullPointerException | ||||||
|  |         if (mediaDetails != null) { | ||||||
|  |             if (mediaDetails.isVisible()) { | ||||||
|  |                 // todo add get list fragment | ||||||
|  |                 ((BookmarkFragment) getParentFragment()).setupTabLayout(); | ||||||
|  |                 ArrayList<Integer> removed = mediaDetails.getRemovedItems(); | ||||||
|  |                 removeFragment(mediaDetails); | ||||||
|  |                 ((BookmarkFragment) getParentFragment()).setScroll(true); | ||||||
|  |                 setFragment(listFragment, mediaDetails); | ||||||
|  |                 ((MainActivity) getActivity()).showTabs(); | ||||||
|  |                 if (listFragment instanceof BookmarkPicturesFragment) { | ||||||
|  |                     GridViewAdapter adapter = ((GridViewAdapter) ((BookmarkPicturesFragment) listFragment) | ||||||
|  |                         .getAdapter()); | ||||||
|  |                     Iterator i = removed.iterator(); | ||||||
|  |                     while (i.hasNext()) { | ||||||
|  |                         adapter.remove(adapter.getItem((int) i.next())); | ||||||
|  |                     } | ||||||
|  |                     mediaDetails.clearRemoved(); | ||||||
|  | 
 | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 moveToContributionsFragment(); | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             moveToContributionsFragment(); | ||||||
|  |         } | ||||||
|  |         // notify mediaDetails did not handled the backPressed further actions required. | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     void moveToContributionsFragment() { | ||||||
|  |         ((MainActivity) getActivity()).setSelectedItemId(NavTab.CONTRIBUTIONS.code()); | ||||||
|  |         ((MainActivity) getActivity()).showTabs(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onItemClick(AdapterView<?> parent, View view, int position, long id) { | ||||||
|  |         Log.d("deneme8", "on media clicked"); | ||||||
|  |         container.setVisibility(View.VISIBLE); | ||||||
|  |         ((BookmarkFragment) getParentFragment()).tabLayout.setVisibility(View.GONE); | ||||||
|  |         mediaDetails = new MediaDetailPagerFragment(false, true); | ||||||
|  |         ((BookmarkFragment) getParentFragment()).setScroll(false); | ||||||
|  |         setFragment(mediaDetails, listFragment); | ||||||
|  |         mediaDetails.showImage(position); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onBackStackChanged() { | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,226 +0,0 @@ | ||||||
| package fr.free.nrw.commons.bookmarks |  | ||||||
| 
 |  | ||||||
| import android.os.Bundle |  | ||||||
| import android.view.LayoutInflater |  | ||||||
| import android.view.View |  | ||||||
| import android.view.ViewGroup |  | ||||||
| import android.widget.AdapterView |  | ||||||
| import android.widget.AdapterView.OnItemClickListener |  | ||||||
| import androidx.fragment.app.Fragment |  | ||||||
| import androidx.fragment.app.FragmentManager |  | ||||||
| import fr.free.nrw.commons.Media |  | ||||||
| import fr.free.nrw.commons.R |  | ||||||
| import fr.free.nrw.commons.bookmarks.category.BookmarkCategoriesFragment |  | ||||||
| import fr.free.nrw.commons.bookmarks.items.BookmarkItemsFragment |  | ||||||
| import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsFragment |  | ||||||
| import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment |  | ||||||
| import fr.free.nrw.commons.category.CategoryImagesCallback |  | ||||||
| import fr.free.nrw.commons.category.GridViewAdapter |  | ||||||
| import fr.free.nrw.commons.contributions.MainActivity |  | ||||||
| import fr.free.nrw.commons.databinding.FragmentFeaturedRootBinding |  | ||||||
| import fr.free.nrw.commons.di.CommonsDaggerSupportFragment |  | ||||||
| import fr.free.nrw.commons.media.MediaDetailPagerFragment |  | ||||||
| import fr.free.nrw.commons.media.MediaDetailPagerFragment.Companion.newInstance |  | ||||||
| import fr.free.nrw.commons.media.MediaDetailProvider |  | ||||||
| import fr.free.nrw.commons.navtab.NavTab |  | ||||||
| import timber.log.Timber |  | ||||||
| 
 |  | ||||||
| class BookmarkListRootFragment : CommonsDaggerSupportFragment, |  | ||||||
|     FragmentManager.OnBackStackChangedListener, MediaDetailProvider, OnItemClickListener, |  | ||||||
|     CategoryImagesCallback { |  | ||||||
|     private var mediaDetails: MediaDetailPagerFragment? = null |  | ||||||
|     private val bookmarkLocationsFragment: BookmarkLocationsFragment? = null |  | ||||||
|     var listFragment: Fragment? = null |  | ||||||
|     private var bookmarksPagerAdapter: BookmarksPagerAdapter? = null |  | ||||||
| 
 |  | ||||||
|     var binding: FragmentFeaturedRootBinding? = null |  | ||||||
| 
 |  | ||||||
|     constructor() |  | ||||||
| 
 |  | ||||||
|     constructor(bundle: Bundle, bookmarksPagerAdapter: BookmarksPagerAdapter) { |  | ||||||
|         val title = bundle.getString("categoryName") |  | ||||||
|         val order = bundle.getInt("order") |  | ||||||
|         val orderItem = bundle.getInt("orderItem") |  | ||||||
| 
 |  | ||||||
|         when (order) { |  | ||||||
|             0 -> listFragment = BookmarkPicturesFragment() |  | ||||||
|             1 -> listFragment = BookmarkLocationsFragment() |  | ||||||
|             3 -> listFragment = BookmarkCategoriesFragment() |  | ||||||
|         } |  | ||||||
|         if (orderItem == 2) { |  | ||||||
|             listFragment = BookmarkItemsFragment() |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         val featuredArguments = Bundle() |  | ||||||
|         featuredArguments.putString("categoryName", title) |  | ||||||
|         listFragment!!.setArguments(featuredArguments) |  | ||||||
|         this.bookmarksPagerAdapter = bookmarksPagerAdapter |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override fun onCreateView( |  | ||||||
|         inflater: LayoutInflater, |  | ||||||
|         container: ViewGroup?, |  | ||||||
|         savedInstanceState: Bundle? |  | ||||||
|     ): View? { |  | ||||||
|         super.onCreate(savedInstanceState) |  | ||||||
|         binding = FragmentFeaturedRootBinding.inflate(inflater, container, false) |  | ||||||
|         return binding!!.getRoot() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |  | ||||||
|         super.onViewCreated(view, savedInstanceState) |  | ||||||
|         if (savedInstanceState == null) { |  | ||||||
|             setFragment(listFragment!!, mediaDetails) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fun setFragment(fragment: Fragment, otherFragment: Fragment?) { |  | ||||||
|         if (fragment.isAdded() && otherFragment != null) { |  | ||||||
|             getChildFragmentManager() |  | ||||||
|                 .beginTransaction() |  | ||||||
|                 .hide(otherFragment) |  | ||||||
|                 .show(fragment) |  | ||||||
|                 .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") |  | ||||||
|                 .commit() |  | ||||||
|             getChildFragmentManager().executePendingTransactions() |  | ||||||
|         } else if (fragment.isAdded() && otherFragment == null) { |  | ||||||
|             getChildFragmentManager() |  | ||||||
|                 .beginTransaction() |  | ||||||
|                 .show(fragment) |  | ||||||
|                 .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") |  | ||||||
|                 .commit() |  | ||||||
|             getChildFragmentManager().executePendingTransactions() |  | ||||||
|         } else if (!fragment.isAdded() && otherFragment != null) { |  | ||||||
|             getChildFragmentManager() |  | ||||||
|                 .beginTransaction() |  | ||||||
|                 .hide(otherFragment) |  | ||||||
|                 .add(R.id.explore_container, fragment) |  | ||||||
|                 .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") |  | ||||||
|                 .commit() |  | ||||||
|             getChildFragmentManager().executePendingTransactions() |  | ||||||
|         } else if (!fragment.isAdded()) { |  | ||||||
|             getChildFragmentManager() |  | ||||||
|                 .beginTransaction() |  | ||||||
|                 .replace(R.id.explore_container, fragment) |  | ||||||
|                 .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") |  | ||||||
|                 .commit() |  | ||||||
|             getChildFragmentManager().executePendingTransactions() |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fun removeFragment(fragment: Fragment) { |  | ||||||
|         getChildFragmentManager() |  | ||||||
|             .beginTransaction() |  | ||||||
|             .remove(fragment) |  | ||||||
|             .commit() |  | ||||||
|         getChildFragmentManager().executePendingTransactions() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override fun onMediaClicked(position: Int) { |  | ||||||
|         Timber.d("on media clicked") |  | ||||||
|         /*container.setVisibility(View.VISIBLE); |  | ||||||
|     ((BookmarkFragment)getParentFragment()).tabLayout.setVisibility(View.GONE); |  | ||||||
|     mediaDetails = new MediaDetailPagerFragment(false, true, position); |  | ||||||
|     setFragment(mediaDetails, bookmarkPicturesFragment);*/ |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * This method is called mediaDetailPagerFragment. It returns the Media Object at that Index |  | ||||||
|      * |  | ||||||
|      * @param i It is the index of which media object is to be returned which is same as current |  | ||||||
|      * index of viewPager. |  | ||||||
|      * @return Media Object |  | ||||||
|      */ |  | ||||||
|     override fun getMediaAtPosition(i: Int): Media? = |  | ||||||
|         bookmarksPagerAdapter!!.mediaAdapter?.getItem(i) as Media? |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * This method is called on from getCount of MediaDetailPagerFragment The viewpager will contain |  | ||||||
|      * same number of media items as that of media elements in adapter. |  | ||||||
|      * |  | ||||||
|      * @return Total Media count in the adapter |  | ||||||
|      */ |  | ||||||
|     override fun getTotalMediaCount(): Int = |  | ||||||
|         bookmarksPagerAdapter!!.mediaAdapter?.count ?: 0 |  | ||||||
| 
 |  | ||||||
|     override fun getContributionStateAt(position: Int): Int? { |  | ||||||
|         return null |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Reload media detail fragment once media is nominated |  | ||||||
|      * |  | ||||||
|      * @param index item position that has been nominated |  | ||||||
|      */ |  | ||||||
|     override fun refreshNominatedMedia(index: Int) { |  | ||||||
|         if (mediaDetails != null && !listFragment!!.isVisible()) { |  | ||||||
|             removeFragment(mediaDetails!!) |  | ||||||
|             mediaDetails = newInstance(false, true) |  | ||||||
|             (parentFragment as BookmarkFragment).setScroll(false) |  | ||||||
|             setFragment(mediaDetails!!, listFragment) |  | ||||||
|             mediaDetails!!.showImage(index) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * This method is called on success of API call for featured images or mobile uploads. The |  | ||||||
|      * viewpager will notified that number of items have changed. |  | ||||||
|      */ |  | ||||||
|     override fun viewPagerNotifyDataSetChanged() { |  | ||||||
|         if (mediaDetails != null) { |  | ||||||
|             mediaDetails!!.notifyDataSetChanged() |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fun backPressed(): Boolean { |  | ||||||
|         //check mediaDetailPage fragment is not null then we check mediaDetail.is Visible or not to avoid NullPointerException |  | ||||||
|         if (mediaDetails != null) { |  | ||||||
|             if (mediaDetails!!.isVisible()) { |  | ||||||
|                 // todo add get list fragment |  | ||||||
|                 (parentFragment as BookmarkFragment).setupTabLayout() |  | ||||||
|                 val removed: ArrayList<Int> = mediaDetails!!.removedItems |  | ||||||
|                 removeFragment(mediaDetails!!) |  | ||||||
|                 (parentFragment as BookmarkFragment).setScroll(true) |  | ||||||
|                 setFragment(listFragment!!, mediaDetails) |  | ||||||
|                 (requireActivity() as MainActivity).showTabs() |  | ||||||
|                 if (listFragment is BookmarkPicturesFragment) { |  | ||||||
|                     val adapter = ((listFragment as BookmarkPicturesFragment) |  | ||||||
|                         .getAdapter() as GridViewAdapter?) |  | ||||||
|                     val i: MutableIterator<*> = removed.iterator() |  | ||||||
|                     while (i.hasNext()) { |  | ||||||
|                         adapter!!.remove(adapter.getItem(i.next() as Int)) |  | ||||||
|                     } |  | ||||||
|                     mediaDetails!!.clearRemoved() |  | ||||||
|                 } |  | ||||||
|             } else { |  | ||||||
|                 moveToContributionsFragment() |  | ||||||
|             } |  | ||||||
|         } else { |  | ||||||
|             moveToContributionsFragment() |  | ||||||
|         } |  | ||||||
|         // notify mediaDetails did not handled the backPressed further actions required. |  | ||||||
|         return false |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fun moveToContributionsFragment() { |  | ||||||
|         (requireActivity() as MainActivity).setSelectedItemId(NavTab.CONTRIBUTIONS.code()) |  | ||||||
|         (requireActivity() as MainActivity).showTabs() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override fun onItemClick(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { |  | ||||||
|         Timber.d("on media clicked") |  | ||||||
|         binding!!.exploreContainer.visibility = View.VISIBLE |  | ||||||
|         (parentFragment as BookmarkFragment).binding!!.tabLayout.setVisibility(View.GONE) |  | ||||||
|         mediaDetails = newInstance(false, true) |  | ||||||
|         (parentFragment as BookmarkFragment).setScroll(false) |  | ||||||
|         setFragment(mediaDetails!!, listFragment) |  | ||||||
|         mediaDetails!!.showImage(position) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override fun onBackStackChanged() = Unit |  | ||||||
| 
 |  | ||||||
|     override fun onDestroy() { |  | ||||||
|         super.onDestroy() |  | ||||||
|         binding = null |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -0,0 +1,32 @@ | ||||||
|  | package fr.free.nrw.commons.bookmarks; | ||||||
|  | 
 | ||||||
|  | import androidx.fragment.app.Fragment; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Data class for handling a bookmark fragment and it title | ||||||
|  |  */ | ||||||
|  | public class BookmarkPages { | ||||||
|  |     private Fragment page; | ||||||
|  |     private String title; | ||||||
|  | 
 | ||||||
|  |     BookmarkPages(Fragment fragment, String title) { | ||||||
|  |         this.title = title; | ||||||
|  |         this.page = fragment; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Return the fragment | ||||||
|  |      * @return fragment object | ||||||
|  |      */ | ||||||
|  |     public Fragment getPage() { | ||||||
|  |         return page; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Return the fragment title | ||||||
|  |      * @return title | ||||||
|  |      */ | ||||||
|  |     public String getTitle() { | ||||||
|  |         return title; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,8 +0,0 @@ | ||||||
| package fr.free.nrw.commons.bookmarks |  | ||||||
| 
 |  | ||||||
| import androidx.fragment.app.Fragment |  | ||||||
| 
 |  | ||||||
| data class BookmarkPages ( |  | ||||||
|     val page: Fragment? = null, |  | ||||||
|     val title: String? = null |  | ||||||
| ) |  | ||||||
|  | @ -0,0 +1,88 @@ | ||||||
|  | package fr.free.nrw.commons.bookmarks; | ||||||
|  | 
 | ||||||
|  | import android.content.Context; | ||||||
|  | import android.os.Bundle; | ||||||
|  | import android.widget.ListAdapter; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.Nullable; | ||||||
|  | import androidx.fragment.app.Fragment; | ||||||
|  | import androidx.fragment.app.FragmentManager; | ||||||
|  | import androidx.fragment.app.FragmentPagerAdapter; | ||||||
|  | 
 | ||||||
|  | import java.util.ArrayList; | ||||||
|  | 
 | ||||||
|  | import fr.free.nrw.commons.R; | ||||||
|  | import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsFragment; | ||||||
|  | import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment; | ||||||
|  | 
 | ||||||
|  | public class BookmarksPagerAdapter extends FragmentPagerAdapter { | ||||||
|  | 
 | ||||||
|  |     private ArrayList<BookmarkPages> pages; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Default Constructor | ||||||
|  |      * @param fm | ||||||
|  |      * @param context | ||||||
|  |      * @param onlyPictures is true if the fragment requires only BookmarkPictureFragment | ||||||
|  |      *                     (i.e. when no user is logged in). | ||||||
|  |      */ | ||||||
|  |     BookmarksPagerAdapter(FragmentManager fm, Context context,boolean onlyPictures) { | ||||||
|  |         super(fm); | ||||||
|  |         pages = new ArrayList<>(); | ||||||
|  |         Bundle picturesBundle = new Bundle(); | ||||||
|  |         picturesBundle.putString("categoryName", context.getString(R.string.title_page_bookmarks_pictures)); | ||||||
|  |         picturesBundle.putInt("order", 0); | ||||||
|  |         pages.add(new BookmarkPages( | ||||||
|  |                 new BookmarkListRootFragment(picturesBundle, this), | ||||||
|  |                 context.getString(R.string.title_page_bookmarks_pictures))); | ||||||
|  |         if (!onlyPictures) { | ||||||
|  |             // if onlyPictures is false we also add the location fragment. | ||||||
|  |             Bundle locationBundle = new Bundle(); | ||||||
|  |             locationBundle.putString("categoryName", | ||||||
|  |                 context.getString(R.string.title_page_bookmarks_locations)); | ||||||
|  |             locationBundle.putInt("order", 1); | ||||||
|  |             pages.add(new BookmarkPages( | ||||||
|  |                 new BookmarkListRootFragment(locationBundle, this), | ||||||
|  |                 context.getString(R.string.title_page_bookmarks_locations))); | ||||||
|  | 
 | ||||||
|  |             locationBundle.putInt("orderItem", 2); | ||||||
|  |             pages.add(new BookmarkPages( | ||||||
|  |                 new BookmarkListRootFragment(locationBundle, this), | ||||||
|  |                 context.getString(R.string.title_page_bookmarks_items))); | ||||||
|  |         } | ||||||
|  |         notifyDataSetChanged(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public Fragment getItem(int position) { | ||||||
|  |         return pages.get(position).getPage(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public int getCount() { | ||||||
|  |         return pages.size(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Nullable | ||||||
|  |     @Override | ||||||
|  |     public CharSequence getPageTitle(int position) { | ||||||
|  |         return pages.get(position).getTitle(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Return the Adapter used to display the picture gridview | ||||||
|  |      * @return adapter | ||||||
|  |      */ | ||||||
|  |     public ListAdapter getMediaAdapter() { | ||||||
|  |         BookmarkPicturesFragment fragment = (BookmarkPicturesFragment)(((BookmarkListRootFragment)pages.get(0).getPage()).listFragment); | ||||||
|  |         return fragment.getAdapter(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Update the pictures list for the bookmark fragment | ||||||
|  |      */ | ||||||
|  |     public void requestPictureListUpdate() { | ||||||
|  |         BookmarkPicturesFragment fragment = (BookmarkPicturesFragment)(((BookmarkListRootFragment)pages.get(0).getPage()).listFragment); | ||||||
|  |         fragment.onResume(); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,82 +0,0 @@ | ||||||
| package fr.free.nrw.commons.bookmarks |  | ||||||
| 
 |  | ||||||
| import android.content.Context |  | ||||||
| import android.widget.ListAdapter |  | ||||||
| import androidx.core.os.bundleOf |  | ||||||
| import androidx.fragment.app.Fragment |  | ||||||
| import androidx.fragment.app.FragmentManager |  | ||||||
| import androidx.fragment.app.FragmentPagerAdapter |  | ||||||
| import fr.free.nrw.commons.R |  | ||||||
| import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment |  | ||||||
| 
 |  | ||||||
| class BookmarksPagerAdapter internal constructor( |  | ||||||
|     fm: FragmentManager, context: Context, onlyPictures: Boolean |  | ||||||
| ) : FragmentPagerAdapter(fm) { |  | ||||||
|     private val pages = mutableListOf<BookmarkPages>() |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Default Constructor |  | ||||||
|      * @param fm |  | ||||||
|      * @param context |  | ||||||
|      * @param onlyPictures is true if the fragment requires only BookmarkPictureFragment |  | ||||||
|      * (i.e. when no user is logged in). |  | ||||||
|      */ |  | ||||||
|     init { |  | ||||||
|         pages.add( |  | ||||||
|             BookmarkPages( |  | ||||||
|                 BookmarkListRootFragment( |  | ||||||
|                     bundleOf( |  | ||||||
|                         "categoryName" to context.getString(R.string.title_page_bookmarks_pictures), |  | ||||||
|                         "order" to 0 |  | ||||||
|                     ), this |  | ||||||
|                 ), context.getString(R.string.title_page_bookmarks_pictures) |  | ||||||
|             ) |  | ||||||
|         ) |  | ||||||
|         if (!onlyPictures) { |  | ||||||
|             // if onlyPictures is false we also add the location fragment. |  | ||||||
|             val locationBundle = bundleOf( |  | ||||||
|                 "categoryName" to context.getString(R.string.title_page_bookmarks_locations), |  | ||||||
|                 "order" to 1 |  | ||||||
|             ) |  | ||||||
| 
 |  | ||||||
|             pages.add( |  | ||||||
|                 BookmarkPages( |  | ||||||
|                     BookmarkListRootFragment(locationBundle, this), |  | ||||||
|                     context.getString(R.string.title_page_bookmarks_locations) |  | ||||||
|                 ) |  | ||||||
|             ) |  | ||||||
| 
 |  | ||||||
|             locationBundle.putInt("orderItem", 2) |  | ||||||
|             pages.add( |  | ||||||
|                 BookmarkPages( |  | ||||||
|                     BookmarkListRootFragment(locationBundle, this), |  | ||||||
|                     context.getString(R.string.title_page_bookmarks_items) |  | ||||||
|                 ) |  | ||||||
|             ) |  | ||||||
|         } |  | ||||||
|         pages.add( |  | ||||||
|             BookmarkPages( |  | ||||||
|                 BookmarkListRootFragment( |  | ||||||
|                     bundleOf( |  | ||||||
|                         "categoryName" to context.getString(R.string.title_page_bookmarks_categories), |  | ||||||
|                         "order" to 3 |  | ||||||
|                     ), this), |  | ||||||
|                 context.getString(R.string.title_page_bookmarks_categories) |  | ||||||
|             ) |  | ||||||
|         ) |  | ||||||
|         notifyDataSetChanged() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override fun getItem(position: Int): Fragment = pages[position].page!! |  | ||||||
| 
 |  | ||||||
|     override fun getCount(): Int = pages.size |  | ||||||
| 
 |  | ||||||
|     override fun getPageTitle(position: Int): CharSequence? = pages[position].title |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Return the Adapter used to display the picture gridview |  | ||||||
|      * @return adapter |  | ||||||
|      */ |  | ||||||
|     val mediaAdapter: ListAdapter? |  | ||||||
|         get() = (((pages[0].page as BookmarkListRootFragment).listFragment) as BookmarkPicturesFragment).getAdapter() |  | ||||||
| } |  | ||||||
|  | @ -1,52 +0,0 @@ | ||||||
| package fr.free.nrw.commons.bookmarks.category |  | ||||||
| 
 |  | ||||||
| import androidx.room.Dao |  | ||||||
| import androidx.room.Delete |  | ||||||
| import androidx.room.Insert |  | ||||||
| import androidx.room.OnConflictStrategy |  | ||||||
| import androidx.room.Query |  | ||||||
| import kotlinx.coroutines.flow.Flow |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Bookmark categories dao |  | ||||||
|  * |  | ||||||
|  * @constructor Create empty Bookmark categories dao |  | ||||||
|  */ |  | ||||||
| @Dao |  | ||||||
| interface BookmarkCategoriesDao { |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Insert or Delete category bookmark into DB |  | ||||||
|      * |  | ||||||
|      * @param bookmarksCategoryModal |  | ||||||
|      */ |  | ||||||
|     @Insert(onConflict = OnConflictStrategy.REPLACE) |  | ||||||
|     suspend fun insert(bookmarksCategoryModal: BookmarksCategoryModal) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Delete category bookmark from DB |  | ||||||
|      * |  | ||||||
|      * @param bookmarksCategoryModal |  | ||||||
|      */ |  | ||||||
|     @Delete |  | ||||||
|     suspend fun delete(bookmarksCategoryModal: BookmarksCategoryModal) |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Checks if given category exist in DB |  | ||||||
|      * |  | ||||||
|      * @param categoryName |  | ||||||
|      * @return |  | ||||||
|      */ |  | ||||||
|     @Query("SELECT EXISTS (SELECT 1 FROM bookmarks_categories WHERE categoryName = :categoryName)") |  | ||||||
|     suspend fun doesExist(categoryName: String): Boolean |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Get all categories |  | ||||||
|      * |  | ||||||
|      * @return |  | ||||||
|      */ |  | ||||||
|     @Query("SELECT * FROM bookmarks_categories") |  | ||||||
|     fun getAllCategories(): Flow<List<BookmarksCategoryModal>> |  | ||||||
| 
 |  | ||||||
| } |  | ||||||
|  | @ -1,143 +0,0 @@ | ||||||
| package fr.free.nrw.commons.bookmarks.category |  | ||||||
| 
 |  | ||||||
| import android.content.Intent |  | ||||||
| import android.os.Bundle |  | ||||||
| import android.view.LayoutInflater |  | ||||||
| import android.view.View |  | ||||||
| import android.view.ViewGroup |  | ||||||
| import androidx.compose.foundation.Image |  | ||||||
| import androidx.compose.foundation.clickable |  | ||||||
| import androidx.compose.foundation.isSystemInDarkTheme |  | ||||||
| import androidx.compose.foundation.layout.Box |  | ||||||
| import androidx.compose.foundation.layout.Row |  | ||||||
| import androidx.compose.foundation.layout.fillMaxSize |  | ||||||
| import androidx.compose.foundation.layout.size |  | ||||||
| import androidx.compose.foundation.lazy.LazyColumn |  | ||||||
| import androidx.compose.foundation.lazy.items |  | ||||||
| import androidx.compose.material3.ListItem |  | ||||||
| import androidx.compose.material3.MaterialTheme |  | ||||||
| import androidx.compose.material3.Surface |  | ||||||
| import androidx.compose.material3.Text |  | ||||||
| import androidx.compose.material3.darkColorScheme |  | ||||||
| import androidx.compose.material3.lightColorScheme |  | ||||||
| import androidx.compose.runtime.Composable |  | ||||||
| import androidx.compose.runtime.getValue |  | ||||||
| import androidx.compose.ui.Alignment |  | ||||||
| import androidx.compose.ui.Modifier |  | ||||||
| import androidx.compose.ui.graphics.Color |  | ||||||
| import androidx.compose.ui.platform.ComposeView |  | ||||||
| import androidx.compose.ui.platform.ViewCompositionStrategy |  | ||||||
| import androidx.compose.ui.res.colorResource |  | ||||||
| import androidx.compose.ui.res.painterResource |  | ||||||
| import androidx.compose.ui.res.stringResource |  | ||||||
| import androidx.compose.ui.text.font.FontWeight |  | ||||||
| import androidx.compose.ui.tooling.preview.Preview |  | ||||||
| import androidx.compose.ui.unit.dp |  | ||||||
| import androidx.lifecycle.compose.collectAsStateWithLifecycle |  | ||||||
| import dagger.android.support.DaggerFragment |  | ||||||
| import fr.free.nrw.commons.R |  | ||||||
| import fr.free.nrw.commons.category.CategoryDetailsActivity |  | ||||||
| import javax.inject.Inject |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Tab fragment to show list of bookmarked Categories |  | ||||||
|  */ |  | ||||||
| class BookmarkCategoriesFragment : DaggerFragment() { |  | ||||||
| 
 |  | ||||||
|     @Inject |  | ||||||
|     lateinit var bookmarkCategoriesDao: BookmarkCategoriesDao |  | ||||||
| 
 |  | ||||||
|     override fun onCreateView( |  | ||||||
|         inflater: LayoutInflater, container: ViewGroup?, |  | ||||||
|         savedInstanceState: Bundle? |  | ||||||
|     ): View { |  | ||||||
|         return ComposeView(requireContext()).apply { |  | ||||||
|             setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) |  | ||||||
|             setContent { |  | ||||||
|                 MaterialTheme( |  | ||||||
|                     colorScheme = if (isSystemInDarkTheme()) darkColorScheme( |  | ||||||
|                         primary = colorResource(R.color.primaryDarkColor), |  | ||||||
|                         surface = colorResource(R.color.main_background_dark), |  | ||||||
|                         background = colorResource(R.color.main_background_dark) |  | ||||||
|                     ) else lightColorScheme( |  | ||||||
|                         primary = colorResource(R.color.primaryColor), |  | ||||||
|                         surface = colorResource(R.color.main_background_light), |  | ||||||
|                         background = colorResource(R.color.main_background_light) |  | ||||||
|                     ) |  | ||||||
|                 ) { |  | ||||||
|                     val listOfBookmarks by bookmarkCategoriesDao.getAllCategories() |  | ||||||
|                         .collectAsStateWithLifecycle(initialValue = emptyList()) |  | ||||||
|                     Surface(modifier = Modifier.fillMaxSize()) { |  | ||||||
|                         Box(contentAlignment = Alignment.Center) { |  | ||||||
|                             if (listOfBookmarks.isEmpty()) { |  | ||||||
|                                 Text( |  | ||||||
|                                     text = stringResource(R.string.bookmark_empty), |  | ||||||
|                                     style = MaterialTheme.typography.bodyMedium, |  | ||||||
|                                     color = if (isSystemInDarkTheme()) Color(0xB3FFFFFF) |  | ||||||
|                                     else Color( |  | ||||||
|                                         0x8A000000 |  | ||||||
|                                     ) |  | ||||||
|                                 ) |  | ||||||
|                             } else { |  | ||||||
|                                 LazyColumn(modifier = Modifier.fillMaxSize()) { |  | ||||||
|                                     items(items = listOfBookmarks) { bookmarkItem -> |  | ||||||
|                                         CategoryItem( |  | ||||||
|                                             categoryName = bookmarkItem.categoryName, |  | ||||||
|                                             onClick = { |  | ||||||
|                                                 val categoryDetailsIntent = Intent( |  | ||||||
|                                                     requireContext(), |  | ||||||
|                                                     CategoryDetailsActivity::class.java |  | ||||||
|                                                 ).putExtra("categoryName", it) |  | ||||||
|                                                 startActivity(categoryDetailsIntent) |  | ||||||
|                                             } |  | ||||||
|                                         ) |  | ||||||
|                                     } |  | ||||||
|                                 } |  | ||||||
|                             } |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     @Composable |  | ||||||
|     fun CategoryItem( |  | ||||||
|         modifier: Modifier = Modifier, |  | ||||||
|         onClick: (String) -> Unit, |  | ||||||
|         categoryName: String |  | ||||||
|     ) { |  | ||||||
|         Row(modifier = modifier.clickable { |  | ||||||
|             onClick(categoryName) |  | ||||||
|         }) { |  | ||||||
|             ListItem( |  | ||||||
|                 leadingContent = { |  | ||||||
|                     Image( |  | ||||||
|                         modifier = Modifier.size(48.dp), |  | ||||||
|                         painter = painterResource(R.drawable.commons), |  | ||||||
|                         contentDescription = null |  | ||||||
|                     ) |  | ||||||
|                 }, |  | ||||||
|                 headlineContent = { |  | ||||||
|                     Text( |  | ||||||
|                         text = categoryName, |  | ||||||
|                         maxLines = 2, |  | ||||||
|                         color = if (isSystemInDarkTheme()) Color.White else Color.Black, |  | ||||||
|                         style = MaterialTheme.typography.bodyMedium, |  | ||||||
|                         fontWeight = FontWeight.SemiBold |  | ||||||
|                     ) |  | ||||||
|                 } |  | ||||||
|             ) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Preview |  | ||||||
|     @Composable |  | ||||||
|     private fun CategoryItemPreview() { |  | ||||||
|         CategoryItem( |  | ||||||
|             onClick = {}, |  | ||||||
|             categoryName = "Test Category" |  | ||||||
|         ) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
Some files were not shown because too many files have changed in this diff Show more
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue