mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 20:33:53 +01:00
Compare commits
198 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63f621cb56 | ||
|
|
e81f916626 | ||
|
|
28fa7b1a20 | ||
|
|
aae9d4a387 | ||
|
|
6873f63cf8 | ||
|
|
2d0255e5fb | ||
|
|
32ae406cca | ||
|
|
3e04a1f036 | ||
|
|
6487191394 | ||
|
|
beaf211f39 | ||
|
|
3549789cdf | ||
|
|
def33552f9 | ||
|
|
3a55583460 | ||
|
|
717a855149 | ||
|
|
29b6d0f8fe | ||
|
|
b5b5d8a8e4 | ||
|
|
714e5f8a4b | ||
|
|
7d96e94689 | ||
|
|
7a865df909 | ||
|
|
864884e7b2 | ||
|
|
1ecaf09f21 | ||
|
|
1ff2a28326 | ||
|
|
b48905a153 | ||
|
|
09c8d987e1 | ||
|
|
2e52adbef8 | ||
|
|
61c9de6fcc | ||
|
|
41d95814c9 | ||
|
|
c4cb65fc3c | ||
|
|
a1c5974e93 | ||
|
|
0c244f369c | ||
|
|
b6014b017c | ||
|
|
91ea4a6e7b | ||
|
|
1e51c4c5d0 | ||
|
|
fbd28a0564 | ||
|
|
d0965206cd | ||
|
|
bb330c1771 | ||
|
|
14d6c80241 | ||
|
|
4c621364c9 | ||
|
|
2a9d5db51e | ||
|
|
b8d340fbe8 | ||
|
|
dd1814c793 | ||
|
|
adb6181e9f | ||
|
|
0a4b179db5 | ||
|
|
e78db7fa08 | ||
|
|
7be615bacb | ||
|
|
95d58023c7 | ||
|
|
7b8fbc239b | ||
|
|
30d1107cef | ||
|
|
fe16c44caa | ||
|
|
4ed9ad5085 | ||
|
|
755d8311dc | ||
|
|
b6457cc6b9 | ||
|
|
2d51a7ce9a | ||
|
|
0ade0705e2 | ||
|
|
6bc25ccd9b | ||
|
|
ed7007fc8c | ||
|
|
71ad6a2ce5 | ||
|
|
e9a1af0f52 | ||
|
|
10c384ffa7 | ||
|
|
4e51977fb6 | ||
|
|
d632c268ae | ||
|
|
be371e5236 | ||
|
|
25d3068faf | ||
|
|
179c7c1855 | ||
|
|
8018000584 | ||
|
|
657af4fe04 | ||
|
|
219fcd3dd8 | ||
|
|
2e9726b84f | ||
|
|
64c6b0c8d0 | ||
|
|
fcc63b9f09 | ||
|
|
a283ffe2bc | ||
|
|
2811b181b7 | ||
|
|
730f314200 | ||
|
|
81da5c9a1a | ||
|
|
a59bf64677 | ||
|
|
e2c8f85a5b | ||
|
|
dd96c64182 | ||
|
|
9ba702eaa9 | ||
|
|
296b4c1f52 | ||
|
|
48e7effd0a | ||
|
|
b9f353bb5a | ||
|
|
c22e8447b3 | ||
|
|
f810a2d49b | ||
|
|
4f3f7b97fd | ||
|
|
718c466505 | ||
|
|
b8a558303b | ||
|
|
a892aa6dee | ||
|
|
5a6b3cbf09 | ||
|
|
5bdfbf5f6f | ||
|
|
1d7d2801e4 | ||
|
|
5201af70cd | ||
|
|
d0e95bc3c2 | ||
|
|
ffb9af1f1c | ||
|
|
6dcce45c59 | ||
|
|
6f36cae767 | ||
|
|
516039c91d | ||
|
|
8de57304bf | ||
|
|
869371b485 | ||
|
|
929711da98 | ||
|
|
b2816e1459 | ||
|
|
532bd8baa6 | ||
|
|
90ab7a2766 | ||
|
|
ee33a9350f | ||
|
|
f1e6f1ad31 | ||
|
|
11e3e37263 | ||
|
|
da694022ac | ||
|
|
29ade1e5b7 | ||
|
|
88565b70c5 | ||
|
|
e5dbcfc2a1 | ||
|
|
0cda8e4d70 | ||
|
|
7500b6d374 | ||
|
|
a4c7a9c4f7 | ||
|
|
8fc7e1039b | ||
|
|
79f52db929 | ||
|
|
13048cc2fd | ||
|
|
66395b9871 | ||
|
|
65f41beed8 | ||
|
|
f98b49608e | ||
|
|
3bd0ec4466 | ||
|
|
4befff8f42 | ||
|
|
89436b0a75 | ||
|
|
6de5a07e0d | ||
|
|
27b9d70333 | ||
|
|
9a94dc2548 | ||
|
|
b1a8308aaf | ||
|
|
ad7dddaac4 | ||
|
|
5d7f42d127 | ||
|
|
d9e8917418 | ||
|
|
09da7b8d68 | ||
|
|
ca5c7ec966 | ||
|
|
9eff9e8e82 | ||
|
|
5665bc7f93 | ||
|
|
20e5df7d49 | ||
|
|
d3ae925567 | ||
|
|
af82cb2123 | ||
|
|
7df52e3f9c | ||
|
|
6b40560dfc | ||
|
|
54bb789461 | ||
|
|
7979be17c1 | ||
|
|
91564a1dff | ||
|
|
2b5f0e4ac9 | ||
|
|
9b04031c91 | ||
|
|
8ff52e6815 | ||
|
|
c41b5cc9da | ||
|
|
767b625289 | ||
|
|
f45f26e602 | ||
|
|
06a613e855 | ||
|
|
62c5231dc9 | ||
|
|
7a224a9120 | ||
|
|
593335aea3 | ||
|
|
6edc6a22e4 | ||
|
|
230604f5ef | ||
|
|
73f5200c2d | ||
|
|
95b8ac74b9 | ||
|
|
cfc2cfcca1 | ||
|
|
ed1485ca22 | ||
|
|
c49c85e68b | ||
|
|
91ca2e6672 | ||
|
|
8849f8984b | ||
|
|
bb21e4bdcd | ||
|
|
eb617ae8ca | ||
|
|
b3c1474b31 | ||
|
|
21ffcb56fd | ||
|
|
f977e16774 | ||
|
|
012020735f | ||
|
|
3f2077a6db | ||
|
|
f06ae4ebfe | ||
|
|
865824a8e3 | ||
|
|
4d2170257a | ||
|
|
0024e72a2e | ||
|
|
60aca9a5e3 | ||
|
|
d0f6c16878 | ||
|
|
8fded5ef6e | ||
|
|
329a68216e | ||
|
|
30762971db | ||
|
|
7479d96675 | ||
|
|
ed42d85f67 | ||
|
|
78d29bcf20 | ||
|
|
1a13cb3383 | ||
|
|
9289dcc42c | ||
|
|
efdc9c5548 | ||
|
|
69b3544107 | ||
|
|
5b5aeead88 | ||
|
|
4bacac1f8b | ||
|
|
6aeb3c07cc | ||
|
|
2c41176a6e | ||
|
|
e3dd00bcfa | ||
|
|
262efe4d8c | ||
|
|
2eed441462 | ||
|
|
56fa8ceb5a | ||
|
|
7bf9276d1a | ||
|
|
51da9e4dd6 | ||
|
|
731ff62faf | ||
|
|
fdfd7781e9 | ||
|
|
6e090c8d7a | ||
|
|
44966645ca | ||
|
|
669f3043ae | ||
|
|
5a5e660a43 |
452 changed files with 17617 additions and 12870 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]: "
|
||||||
labels: ["bug"]
|
type: Bug # Retained to categorize the issue as per organization-level type
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
|
|
@ -70,7 +70,7 @@ body:
|
||||||
required: false
|
required: false
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Screen-shots
|
label: Screenshots
|
||||||
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
|
||||||
|
|
|
||||||
1
.idea/codeStyles/Project.xml
generated
1
.idea/codeStyles/Project.xml
generated
|
|
@ -16,6 +16,7 @@
|
||||||
<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" />
|
||||||
|
|
|
||||||
45
.idea/inspectionProfiles/Project_Default.xml
generated
45
.idea/inspectionProfiles/Project_Default.xml
generated
|
|
@ -2,11 +2,35 @@
|
||||||
<profile version="1.0">
|
<profile version="1.0">
|
||||||
<option name="myName" value="Project Default" />
|
<option name="myName" value="Project Default" />
|
||||||
<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="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">
|
||||||
|
<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" />
|
||||||
|
|
@ -20,6 +44,27 @@
|
||||||
<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="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">
|
||||||
|
|
|
||||||
85
CHANGELOG.md
85
CHANGELOG.md
|
|
@ -1,5 +1,90 @@
|
||||||
# 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
|
## v5.1.2
|
||||||
|
|
||||||
### What's changed
|
### What's changed
|
||||||
|
|
|
||||||
11
README.md
11
README.md
|
|
@ -29,11 +29,12 @@ 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/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/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/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/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/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/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/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/12448084?v=4" width="100px;"/><br /><sub><b>sivaraam</b></sub>](https://github.com/sivaraam) | [<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/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/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/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/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/65972015?v=4" width="100px;"/><br /><sub><b>Prince-kushwaha</b></sub>](https://github.com/Prince-kushwaha) | [<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/54016427?v=4" width="100px;"/><br /><sub><b>4D17Y4</b></sub>](https://github.com/4D17Y4) | [<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/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) |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.. 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).
|
||||||
|
|
|
||||||
428
app/build.gradle
428
app/build.gradle
|
|
@ -1,428 +0,0 @@
|
||||||
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-parcelize'
|
|
||||||
apply from: "$rootDir/jacoco.gradle"
|
|
||||||
|
|
||||||
def isRunningOnTravisAndIsNotPRBuild = System.getenv("CI") == "true" && file('../play.p12').exists()
|
|
||||||
|
|
||||||
if (isRunningOnTravisAndIsNotPRBuild) {
|
|
||||||
apply plugin: 'com.github.triplet.play'
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
|
|
||||||
// Utils
|
|
||||||
implementation 'in.yuvi:http.fluent:1.3'
|
|
||||||
implementation 'com.google.code.gson:gson:2.8.5'
|
|
||||||
implementation ("com.squareup.okhttp3:okhttp:$OKHTTP_VERSION!!"){
|
|
||||||
// Forcing dependency versions using force = true on a first-level dependency has been deprecated.
|
|
||||||
// Ref: https://docs.gradle.org/7.5/userguide/upgrading_version_5.html#forced_dependencies
|
|
||||||
//force = true //API 19 support
|
|
||||||
}
|
|
||||||
implementation 'com.squareup.retrofit2:retrofit:2.8.1'
|
|
||||||
implementation "com.squareup.retrofit2:converter-gson:2.8.1"
|
|
||||||
implementation "com.squareup.retrofit2:adapter-rxjava2:2.8.1"
|
|
||||||
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 "org.maplibre.gl:android-sdk:$MAPLIBRE_VERSION"
|
|
||||||
implementation 'org.maplibre.gl:android-plugin-scalebar-v9:1.0.0'
|
|
||||||
|
|
||||||
implementation 'com.jakewharton.timber:timber:4.7.1'
|
|
||||||
implementation 'com.github.deano2390:MaterialShowcaseView:1.2.0'
|
|
||||||
implementation "com.google.android.material:material:1.12.0"
|
|
||||||
implementation 'com.karumi:dexter:5.0.0'
|
|
||||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
|
||||||
implementation 'androidx.compose.ui:ui-tooling-preview'
|
|
||||||
androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
|
|
||||||
|
|
||||||
// Jetpack Compose
|
|
||||||
def composeBom = platform('androidx.compose:compose-bom:2024.11.00')
|
|
||||||
|
|
||||||
implementation "androidx.activity:activity-compose:1.9.3"
|
|
||||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.8.4"
|
|
||||||
implementation (composeBom)
|
|
||||||
implementation "androidx.compose.runtime:runtime"
|
|
||||||
implementation "androidx.compose.ui:ui"
|
|
||||||
implementation "androidx.compose.ui:ui-viewbinding"
|
|
||||||
implementation "androidx.compose.ui:ui-graphics"
|
|
||||||
implementation "androidx.compose.ui:ui-tooling"
|
|
||||||
implementation "androidx.compose.foundation:foundation"
|
|
||||||
implementation "androidx.compose.foundation:foundation-layout"
|
|
||||||
implementation "androidx.compose.material3:material3"
|
|
||||||
androidTestImplementation(composeBom)
|
|
||||||
|
|
||||||
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"
|
|
||||||
debugImplementation 'androidx.compose.ui:ui-tooling'
|
|
||||||
debugImplementation 'androidx.compose.ui:ui-test-manifest'
|
|
||||||
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-reflect:$KOTLIN_VERSION"
|
|
||||||
|
|
||||||
//Mocking
|
|
||||||
testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0'
|
|
||||||
testImplementation 'org.mockito:mockito-inline:5.2.0'
|
|
||||||
testImplementation 'org.mockito:mockito-core:5.6.0'
|
|
||||||
testImplementation "org.powermock:powermock-module-junit4:2.0.9"
|
|
||||||
testImplementation "org.powermock:powermock-api-mockito2:2.0.9"
|
|
||||||
testImplementation("io.mockk:mockk:1.13.5")
|
|
||||||
|
|
||||||
// Unit testing
|
|
||||||
testImplementation 'junit:junit:4.13.2'
|
|
||||||
testImplementation 'org.robolectric:robolectric:4.11.1'
|
|
||||||
testImplementation 'androidx.test:core:1.5.0'
|
|
||||||
testImplementation "androidx.test:runner:1.5.2"
|
|
||||||
testImplementation 'androidx.test.ext:junit:1.1.5'
|
|
||||||
testImplementation "androidx.test:rules:1.5.0"
|
|
||||||
testImplementation "com.squareup.okhttp3:mockwebserver:$OKHTTP_VERSION"
|
|
||||||
testImplementation "com.jraska.livedata:testing-ktx:1.2.0"
|
|
||||||
testImplementation "androidx.arch.core:core-testing:2.2.0"
|
|
||||||
testImplementation "org.junit.jupiter:junit-jupiter-api:5.10.0"
|
|
||||||
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.10.0"
|
|
||||||
testImplementation 'com.facebook.soloader:soloader:0.10.5'
|
|
||||||
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3"
|
|
||||||
debugImplementation("androidx.fragment:fragment-testing:1.6.2")
|
|
||||||
testImplementation "commons-io:commons-io:2.6"
|
|
||||||
|
|
||||||
// Android testing
|
|
||||||
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"
|
|
||||||
|
|
||||||
// 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.7'
|
|
||||||
implementation "androidx.core:core-ktx:$CORE_KTX_VERSION"
|
|
||||||
implementation 'com.simplecityapps:recyclerview-fastscroll: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
|
|
||||||
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"
|
|
||||||
//Android Media
|
|
||||||
implementation 'com.github.juanitobananas:AndroidMediaUtil:v1.0-1'
|
|
||||||
|
|
||||||
implementation "androidx.multidex:multidex:$MULTIDEX_VERSION"
|
|
||||||
|
|
||||||
def work_version = "2.8.1"
|
|
||||||
// Kotlin + coroutines
|
|
||||||
implementation "androidx.work:work-runtime-ktx:$work_version"
|
|
||||||
implementation("androidx.work:work-runtime:$work_version")
|
|
||||||
testImplementation "androidx.work:work-testing:$work_version"
|
|
||||||
|
|
||||||
//Glide
|
|
||||||
implementation 'com.github.bumptech.glide:glide:4.16.0'
|
|
||||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
|
|
||||||
kaptTest "androidx.databinding:databinding-compiler:8.0.2"
|
|
||||||
kaptAndroidTest "androidx.databinding:databinding-compiler:8.0.2"
|
|
||||||
|
|
||||||
implementation("io.github.coordinates2country:coordinates2country-android:1.8") { exclude group: 'com.google.android', module: 'android' }
|
|
||||||
|
|
||||||
//OSMDroid
|
|
||||||
implementation ("org.osmdroid:osmdroid-android:$OSMDROID_VERSION")
|
|
||||||
constraints {
|
|
||||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.0") {
|
|
||||||
because("kotlin-stdlib-jdk7 is now a part of kotlin-stdlib")
|
|
||||||
}
|
|
||||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.0") {
|
|
||||||
because("kotlin-stdlib-jdk8 is now a part of kotlin-stdlib")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 34
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
//applicationId 'fr.free.nrw.commons'
|
|
||||||
|
|
||||||
versionCode 1049
|
|
||||||
versionName '5.2.0'
|
|
||||||
setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName())
|
|
||||||
|
|
||||||
minSdkVersion 21
|
|
||||||
targetSdkVersion 34
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
||||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
|
||||||
|
|
||||||
multiDexEnabled true
|
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
||||||
|
|
||||||
vectorDrawables.useSupportLibrary = true
|
|
||||||
}
|
|
||||||
packagingOptions {
|
|
||||||
jniLibs {
|
|
||||||
excludes += ['META-INF/androidx.*']
|
|
||||||
}
|
|
||||||
resources {
|
|
||||||
excludes += ['META-INF/androidx.*', 'META-INF/proguard/androidx-annotations.pro', '/META-INF/LICENSE.md', '/META-INF/LICENSE-notice.md']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
testOptions {
|
|
||||||
animationsDisabled true
|
|
||||||
|
|
||||||
unitTests {
|
|
||||||
returnDefaultValues = true
|
|
||||||
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'
|
|
||||||
signingConfig signingConfigs.debug
|
|
||||||
if (isRunningOnTravisAndIsNotPRBuild) {
|
|
||||||
signingConfig signingConfigs.release
|
|
||||||
}
|
|
||||||
}
|
|
||||||
debug {
|
|
||||||
minifyEnabled false
|
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
|
||||||
testProguardFile 'test-proguard-rules.txt'
|
|
||||||
versionNameSuffix "-debug-" + getBranchName()
|
|
||||||
enableUnitTestCoverage true
|
|
||||||
enableAndroidTestCoverage true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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'
|
|
||||||
resolutionStrategy.force 'com.jakewharton.timber:timber:4.7.1'
|
|
||||||
resolutionStrategy.force 'androidx.fragment:fragment:1.3.6'
|
|
||||||
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", "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://commons-app.github.io/privacy-policy\""
|
|
||||||
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\""
|
|
||||||
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", "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://commons-app.github.io/privacy-policy\""
|
|
||||||
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\""
|
|
||||||
dimension 'tier'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility JavaVersion.VERSION_17
|
|
||||||
targetCompatibility JavaVersion.VERSION_17
|
|
||||||
}
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = "17"
|
|
||||||
}
|
|
||||||
|
|
||||||
buildToolsVersion buildToolsVersion
|
|
||||||
|
|
||||||
buildFeatures {
|
|
||||||
viewBinding true
|
|
||||||
compose true
|
|
||||||
}
|
|
||||||
composeOptions {
|
|
||||||
kotlinCompilerExtensionVersion '1.5.8'
|
|
||||||
}
|
|
||||||
namespace 'fr.free.nrw.commons'
|
|
||||||
lint {
|
|
||||||
abortOnError false
|
|
||||||
disable 'MissingTranslation', 'ExtraTranslation'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
447
app/build.gradle.kts
Normal file
447
app/build.gradle.kts
Normal file
|
|
@ -0,0 +1,447 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -66,6 +66,9 @@
|
||||||
# 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
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ class UploadCancelledTest {
|
||||||
fun setup() {
|
fun setup() {
|
||||||
try {
|
try {
|
||||||
Intents.init()
|
Intents.init()
|
||||||
} catch (ex: IllegalStateException) {
|
} catch (_: IllegalStateException) {
|
||||||
}
|
}
|
||||||
device.unfreezeRotation()
|
device.unfreezeRotation()
|
||||||
device.setOrientationNatural()
|
device.setOrientationNatural()
|
||||||
|
|
@ -65,7 +65,7 @@ class UploadCancelledTest {
|
||||||
fun teardown() {
|
fun teardown() {
|
||||||
try {
|
try {
|
||||||
Intents.release()
|
Intents.release()
|
||||||
} catch (ex: IllegalStateException) {
|
} catch (_: IllegalStateException) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ class UploadTest {
|
||||||
fun setup() {
|
fun setup() {
|
||||||
try {
|
try {
|
||||||
Intents.init()
|
Intents.init()
|
||||||
} catch (ex: IllegalStateException) {
|
} catch (_: IllegalStateException) {
|
||||||
}
|
}
|
||||||
UITestHelper.loginUser()
|
UITestHelper.loginUser()
|
||||||
UITestHelper.skipWelcome()
|
UITestHelper.skipWelcome()
|
||||||
|
|
|
||||||
|
|
@ -57,8 +57,7 @@
|
||||||
tools:replace="android:appComponentFactory">
|
tools:replace="android:appComponentFactory">
|
||||||
<activity
|
<activity
|
||||||
android:name=".activity.SingleWebViewActivity"
|
android:name=".activity.SingleWebViewActivity"
|
||||||
android:exported="false"
|
android:exported="false" />
|
||||||
android:label="@string/title_activity_single_web_view" />
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".nearby.WikidataFeedback"
|
android:name=".nearby.WikidataFeedback"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
@ -85,6 +84,7 @@
|
||||||
android:parentActivityName=".customselector.ui.selector.CustomSelectorActivity" />
|
android:parentActivityName=".customselector.ui.selector.CustomSelectorActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".auth.LoginActivity"
|
android:name=".auth.LoginActivity"
|
||||||
|
android:windowSoftInputMode="adjustPan"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
|
@ -103,7 +103,7 @@
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:hardwareAccelerated="false"
|
android:hardwareAccelerated="false"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustPan">
|
||||||
<intent-filter android:label="@string/intent_share_upload_label">
|
<intent-filter android:label="@string/intent_share_upload_label">
|
||||||
<action android:name="android.intent.action.SEND" />
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
package fr.free.nrw.commons
|
package fr.free.nrw.commons
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.Intent.ACTION_VIEW
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
|
|
@ -16,6 +18,10 @@ import fr.free.nrw.commons.theme.BaseActivity
|
||||||
import fr.free.nrw.commons.utils.ConfigUtils.getVersionNameWithSha
|
import fr.free.nrw.commons.utils.ConfigUtils.getVersionNameWithSha
|
||||||
import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog
|
import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog
|
||||||
import java.util.Collections
|
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
|
* Represents about screen of this app
|
||||||
|
|
@ -42,6 +48,7 @@ class AboutActivity : BaseActivity() {
|
||||||
*/
|
*/
|
||||||
binding = ActivityAboutBinding.inflate(layoutInflater)
|
binding = ActivityAboutBinding.inflate(layoutInflater)
|
||||||
val view: View = binding!!.root
|
val view: View = binding!!.root
|
||||||
|
applyEdgeToEdgeTopInsets(binding!!.toolbarLayout)
|
||||||
setContentView(view)
|
setContentView(view)
|
||||||
|
|
||||||
setSupportActionBar(binding!!.toolbarBinding.toolbar)
|
setSupportActionBar(binding!!.toolbarBinding.toolbar)
|
||||||
|
|
@ -59,30 +66,12 @@ class AboutActivity : BaseActivity() {
|
||||||
binding!!.aboutImprove.setHtmlText(improveText)
|
binding!!.aboutImprove.setHtmlText(improveText)
|
||||||
binding!!.aboutVersion.text = applicationContext.getVersionNameWithSha()
|
binding!!.aboutVersion.text = applicationContext.getVersionNameWithSha()
|
||||||
|
|
||||||
Utils.setUnderlinedText(
|
binding!!.aboutFaq.setUnderlinedText(R.string.about_faq)
|
||||||
binding!!.aboutFaq, R.string.about_faq,
|
binding!!.aboutRateUs.setUnderlinedText(R.string.about_rate_us)
|
||||||
applicationContext
|
binding!!.aboutUserGuide.setUnderlinedText(R.string.user_guide)
|
||||||
)
|
binding!!.aboutPrivacyPolicy.setUnderlinedText(R.string.about_privacy_policy)
|
||||||
Utils.setUnderlinedText(
|
binding!!.aboutTranslate.setUnderlinedText(R.string.about_translate)
|
||||||
binding!!.aboutRateUs, R.string.about_rate_us,
|
binding!!.aboutCredits.setUnderlinedText(R.string.about_credits)
|
||||||
applicationContext
|
|
||||||
)
|
|
||||||
Utils.setUnderlinedText(
|
|
||||||
binding!!.aboutUserGuide, R.string.user_guide,
|
|
||||||
applicationContext
|
|
||||||
)
|
|
||||||
Utils.setUnderlinedText(
|
|
||||||
binding!!.aboutPrivacyPolicy, R.string.about_privacy_policy,
|
|
||||||
applicationContext
|
|
||||||
)
|
|
||||||
Utils.setUnderlinedText(
|
|
||||||
binding!!.aboutTranslate, R.string.about_translate,
|
|
||||||
applicationContext
|
|
||||||
)
|
|
||||||
Utils.setUnderlinedText(
|
|
||||||
binding!!.aboutCredits, R.string.about_credits,
|
|
||||||
applicationContext
|
|
||||||
)
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
To set listeners, we can create a separate method and use lambda syntax.
|
To set listeners, we can create a separate method and use lambda syntax.
|
||||||
|
|
@ -106,47 +95,56 @@ class AboutActivity : BaseActivity() {
|
||||||
fun launchFacebook(view: View?) {
|
fun launchFacebook(view: View?) {
|
||||||
val intent: Intent
|
val intent: Intent
|
||||||
try {
|
try {
|
||||||
intent = Intent(Intent.ACTION_VIEW, Uri.parse(Urls.FACEBOOK_APP_URL))
|
intent = Intent(ACTION_VIEW, Urls.FACEBOOK_APP_URL.toUri())
|
||||||
intent.setPackage(Urls.FACEBOOK_PACKAGE_NAME)
|
intent.setPackage(Urls.FACEBOOK_PACKAGE_NAME)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Utils.handleWebUrl(this, Uri.parse(Urls.FACEBOOK_WEB_URL))
|
handleWebUrl(this, Urls.FACEBOOK_WEB_URL.toUri())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun launchGithub(view: View?) {
|
fun launchGithub(view: View?) {
|
||||||
val intent: Intent
|
val intent: Intent
|
||||||
try {
|
try {
|
||||||
intent = Intent(Intent.ACTION_VIEW, Uri.parse(Urls.GITHUB_REPO_URL))
|
intent = Intent(ACTION_VIEW, Urls.GITHUB_REPO_URL.toUri())
|
||||||
intent.setPackage(Urls.GITHUB_PACKAGE_NAME)
|
intent.setPackage(Urls.GITHUB_PACKAGE_NAME)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Utils.handleWebUrl(this, Uri.parse(Urls.GITHUB_REPO_URL))
|
handleWebUrl(this, Urls.GITHUB_REPO_URL.toUri())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun launchWebsite(view: View?) {
|
fun launchWebsite(view: View?) {
|
||||||
Utils.handleWebUrl(this, Uri.parse(Urls.WEBSITE_URL))
|
handleWebUrl(this, Urls.WEBSITE_URL.toUri())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun launchRatings(view: View?) {
|
fun launchRatings(view: View?) {
|
||||||
Utils.rateApp(this)
|
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?) {
|
fun launchCredits(view: View?) {
|
||||||
Utils.handleWebUrl(this, Uri.parse(Urls.CREDITS_URL))
|
handleWebUrl(this, Urls.CREDITS_URL.toUri())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun launchUserGuide(view: View?) {
|
fun launchUserGuide(view: View?) {
|
||||||
Utils.handleWebUrl(this, Uri.parse(Urls.USER_GUIDE_URL))
|
handleWebUrl(this, Urls.USER_GUIDE_URL.toUri())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun launchPrivacyPolicy(view: View?) {
|
fun launchPrivacyPolicy(view: View?) {
|
||||||
Utils.handleWebUrl(this, Uri.parse(BuildConfig.PRIVACY_POLICY_URL))
|
handleWebUrl(this, BuildConfig.PRIVACY_POLICY_URL.toUri())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun launchFrequentlyAskedQuesions(view: View?) {
|
fun launchFrequentlyAskedQuesions(view: View?) {
|
||||||
Utils.handleWebUrl(this, Uri.parse(Urls.FAQ_URL))
|
handleWebUrl(this, Urls.FAQ_URL.toUri())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
|
|
@ -193,7 +191,7 @@ class AboutActivity : BaseActivity() {
|
||||||
|
|
||||||
val positiveButtonRunnable = Runnable {
|
val positiveButtonRunnable = Runnable {
|
||||||
val langCode = instance.languageLookUpTable!!.getCodes()[spinner.selectedItemPosition]
|
val langCode = instance.languageLookUpTable!!.getCodes()[spinner.selectedItemPosition]
|
||||||
Utils.handleWebUrl(this@AboutActivity, Uri.parse(Urls.TRANSLATE_WIKI_URL + langCode))
|
handleWebUrl(this@AboutActivity, (Urls.TRANSLATE_WIKI_URL + langCode).toUri())
|
||||||
}
|
}
|
||||||
showAlertDialog(
|
showAlertDialog(
|
||||||
this,
|
this,
|
||||||
|
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
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();
|
|
||||||
}
|
|
||||||
10
app/src/main/java/fr/free/nrw/commons/BasePresenter.kt
Normal file
10
app/src/main/java/fr/free/nrw/commons/BasePresenter.kt
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
package fr.free.nrw.commons
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base presenter, enforcing contracts to attach and detach view
|
||||||
|
*/
|
||||||
|
interface BasePresenter<T> {
|
||||||
|
fun onAttachView(view: T)
|
||||||
|
|
||||||
|
fun onDetachView()
|
||||||
|
}
|
||||||
|
|
@ -15,9 +15,8 @@ import com.facebook.drawee.backends.pipeline.Fresco
|
||||||
import com.facebook.imagepipeline.core.ImagePipelineConfig
|
import com.facebook.imagepipeline.core.ImagePipelineConfig
|
||||||
import fr.free.nrw.commons.auth.LoginActivity
|
import fr.free.nrw.commons.auth.LoginActivity
|
||||||
import fr.free.nrw.commons.auth.SessionManager
|
import fr.free.nrw.commons.auth.SessionManager
|
||||||
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao
|
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable
|
||||||
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao
|
import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable
|
||||||
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao
|
|
||||||
import fr.free.nrw.commons.category.CategoryDao
|
import fr.free.nrw.commons.category.CategoryDao
|
||||||
import fr.free.nrw.commons.concurrency.BackgroundPoolExceptionHandler
|
import fr.free.nrw.commons.concurrency.BackgroundPoolExceptionHandler
|
||||||
import fr.free.nrw.commons.concurrency.ThreadPoolService
|
import fr.free.nrw.commons.concurrency.ThreadPoolService
|
||||||
|
|
@ -257,8 +256,8 @@ class CommonsApplication : MultiDexApplication() {
|
||||||
} catch (e: SQLiteException) {
|
} catch (e: SQLiteException) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
}
|
}
|
||||||
BookmarkPicturesDao.Table.onDelete(db)
|
BookmarksTable.onDelete(db)
|
||||||
BookmarkItemsDao.Table.onDelete(db)
|
BookmarkItemsTable.onDelete(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
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 currentLatLng; // 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 currentLatLng; // 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
46
app/src/main/java/fr/free/nrw/commons/MapController.kt
Normal file
46
app/src/main/java/fr/free/nrw/commons/MapController.kt
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
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,7 +1,9 @@
|
||||||
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 fr.free.nrw.commons.wikidata.model.page.PageTitle
|
import fr.free.nrw.commons.wikidata.model.page.PageTitle
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
@ -28,9 +30,7 @@ class Media constructor(
|
||||||
*/
|
*/
|
||||||
var filename: String? = null,
|
var filename: String? = null,
|
||||||
/**
|
/**
|
||||||
* Gets or sets the file description.
|
* The fallback description of the file, used if no other description is provided.
|
||||||
* @return file description as a string
|
|
||||||
* @param fallbackDescription the new description of the file
|
|
||||||
*/
|
*/
|
||||||
var fallbackDescription: String? = null,
|
var fallbackDescription: String? = null,
|
||||||
/**
|
/**
|
||||||
|
|
@ -40,19 +40,25 @@ class Media constructor(
|
||||||
*/
|
*/
|
||||||
var dateUploaded: Date? = null,
|
var dateUploaded: Date? = null,
|
||||||
/**
|
/**
|
||||||
* Gets or sets the license name of the file.
|
* The license name of the file.
|
||||||
* @return license as a String
|
|
||||||
* @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,
|
||||||
/**
|
/**
|
||||||
* Gets or sets the name of the creator of the file.
|
* The name of the creator of the file.
|
||||||
* @return author name as a String
|
|
||||||
* @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
|
||||||
|
|
@ -66,6 +72,7 @@ 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
|
||||||
|
|
@ -130,6 +137,7 @@ class Media constructor(
|
||||||
* returns user
|
* returns user
|
||||||
* @return Author or 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? {
|
fun getAuthorOrUser(): String? {
|
||||||
return if (!author.isNullOrEmpty()) {
|
return if (!author.isNullOrEmpty()) {
|
||||||
author
|
author
|
||||||
|
|
@ -138,6 +146,19 @@ class Media constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
|
@ -154,7 +175,8 @@ class Media constructor(
|
||||||
* Gets file page title
|
* Gets file page title
|
||||||
* @return New media page title
|
* @return New media page title
|
||||||
*/
|
*/
|
||||||
val pageTitle: PageTitle get() = Utils.getPageTitle(filename!!)
|
val pageTitle: PageTitle
|
||||||
|
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
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
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.IdAndCaptions
|
import fr.free.nrw.commons.media.IdAndLabels
|
||||||
import fr.free.nrw.commons.media.MediaClient
|
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 io.reactivex.Single
|
import io.reactivex.Single
|
||||||
|
|
@ -23,13 +23,23 @@ class MediaDataExtractor
|
||||||
private val mediaClient: MediaClient,
|
private val mediaClient: MediaClient,
|
||||||
) {
|
) {
|
||||||
fun fetchDepictionIdsAndLabels(media: Media) =
|
fun fetchDepictionIdsAndLabels(media: Media) =
|
||||||
mediaClient
|
mediaClient
|
||||||
.getEntities(media.depictionIds)
|
.getEntities(media.depictionIds)
|
||||||
.map {
|
.map {
|
||||||
it
|
it
|
||||||
.entities()
|
.entities()
|
||||||
.mapValues { entry -> entry.value.labels().mapValues { it.value.value() } }
|
.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() }
|
||||||
|
|
||||||
|
fun fetchCreatorIdsAndLabels(media: Media) =
|
||||||
|
mediaClient
|
||||||
|
.getEntities(media.creatorIds)
|
||||||
|
.map {
|
||||||
|
it
|
||||||
|
.entities()
|
||||||
|
.mapValues { entry -> entry.value.labels().mapValues { it.value.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)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
package fr.free.nrw.commons;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base interface for all the views
|
|
||||||
*/
|
|
||||||
public interface MvpView {
|
|
||||||
void showMessage(String message);
|
|
||||||
}
|
|
||||||
|
|
@ -1,154 +0,0 @@
|
||||||
package fr.free.nrw.commons;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import fr.free.nrw.commons.wikidata.cookies.CommonsCookieJar;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
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 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;
|
|
||||||
|
|
||||||
public static OkHttpClient CLIENT;
|
|
||||||
|
|
||||||
@NonNull public static OkHttpClient getClient(final CommonsCookieJar cookieJar) {
|
|
||||||
if (CLIENT == null) {
|
|
||||||
CLIENT = createClient(cookieJar);
|
|
||||||
}
|
|
||||||
return CLIENT;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private static OkHttpClient createClient(final CommonsCookieJar cookieJar) {
|
|
||||||
return new OkHttpClient.Builder()
|
|
||||||
.cookieJar(cookieJar)
|
|
||||||
.cache((CommonsApplication.getInstance()!=null) ? new Cache(new File(CommonsApplication.getInstance().getCacheDir(), CACHE_DIR_NAME), NET_CACHE_SIZE) : null)
|
|
||||||
.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 String SUPPRESS_ERROR_LOG = "x-commons-suppress-error-log";
|
|
||||||
public static final String SUPPRESS_ERROR_LOG_HEADER = SUPPRESS_ERROR_LOG+": true";
|
|
||||||
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 Request 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.
|
|
||||||
final boolean suppressErrors = rq.headers().names().contains(SUPPRESS_ERROR_LOG);
|
|
||||||
final Request request = rq.newBuilder()
|
|
||||||
.removeHeader(SUPPRESS_ERROR_LOG)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
final Response 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 (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) {
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class HttpStatusException extends IOException {
|
|
||||||
private final int code;
|
|
||||||
private final String url;
|
|
||||||
public HttpStatusException(@NonNull Response rsp) {
|
|
||||||
this.code = rsp.code();
|
|
||||||
this.url = rsp.request().url().uri().toString();
|
|
||||||
try {
|
|
||||||
if (rsp.body() != null && rsp.body().contentType() != null
|
|
||||||
&& rsp.body().contentType().toString().contains("json")) {
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
// Log?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public int code() {
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getMessage() {
|
|
||||||
String str = "Code: " + code + ", URL: " + url;
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
135
app/src/main/java/fr/free/nrw/commons/OkHttpConnectionFactory.kt
Normal file
135
app/src/main/java/fr/free/nrw/commons/OkHttpConnectionFactory.kt
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,264 +0,0 @@
|
||||||
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 androidx.annotation.NonNull;
|
|
||||||
import androidx.browser.customtabs.CustomTabColorSchemeParams;
|
|
||||||
import androidx.browser.customtabs.CustomTabsIntent;
|
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
|
|
||||||
import java.util.Calendar;
|
|
||||||
import java.util.Date;
|
|
||||||
import fr.free.nrw.commons.wikidata.model.WikiSite;
|
|
||||||
import fr.free.nrw.commons.wikidata.model.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;
|
|
||||||
|
|
||||||
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());
|
|
||||||
|
|
||||||
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 The context for launching intent
|
|
||||||
* @param latLng The latitude and longitude of the location
|
|
||||||
*/
|
|
||||||
public static void handleGeoCoordinates(final Context context, final LatLng latLng) {
|
|
||||||
handleGeoCoordinates(context, latLng, 16);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Util function to handle geo coordinates with specified zoom level. It no longer depends on
|
|
||||||
* google maps and any app capable of handling the map intent can handle it
|
|
||||||
*
|
|
||||||
* @param context The context for launching intent
|
|
||||||
* @param latLng The latitude and longitude of the location
|
|
||||||
* @param zoomLevel The zoom level
|
|
||||||
*/
|
|
||||||
public static void handleGeoCoordinates(final Context context, final LatLng latLng,
|
|
||||||
final double zoomLevel) {
|
|
||||||
final Intent mapIntent = new Intent(Intent.ACTION_VIEW, latLng.getGmmIntentUri(zoomLevel));
|
|
||||||
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";
|
|
||||||
}
|
|
||||||
|
|
||||||
/***
|
|
||||||
* Function to get the current WLM year
|
|
||||||
* It increments at the start of September in line with the other WLM functions
|
|
||||||
* (No consideration of locales for now)
|
|
||||||
* @param calendar
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public static int getWikiLovesMonumentsYear(Calendar calendar) {
|
|
||||||
int year = calendar.get(Calendar.YEAR);
|
|
||||||
if (calendar.get(Calendar.MONTH) < Calendar.SEPTEMBER) {
|
|
||||||
year -= 1;
|
|
||||||
}
|
|
||||||
return year;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
package fr.free.nrw.commons;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
public interface ViewHolder<T> {
|
|
||||||
void bindModel(Context context, T model);
|
|
||||||
}
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
44
app/src/main/java/fr/free/nrw/commons/ViewPagerAdapter.kt
Normal file
44
app/src/main/java/fr/free/nrw/commons/ViewPagerAdapter.kt
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,109 +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.ConfigUtils;
|
|
||||||
|
|
||||||
public class WelcomeActivity extends BaseActivity {
|
|
||||||
|
|
||||||
private ActivityWelcomeBinding binding;
|
|
||||||
private PopupForCopyrightBinding copyrightBinding;
|
|
||||||
|
|
||||||
private final WelcomePagerAdapter adapter = new WelcomePagerAdapter();
|
|
||||||
private boolean isQuiz;
|
|
||||||
private AlertDialog.Builder dialogBuilder;
|
|
||||||
private AlertDialog dialog;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialises exiting fields and dependencies
|
|
||||||
*
|
|
||||||
* @param savedInstanceState WelcomeActivity bundled data
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void onCreate(final Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
binding = ActivityWelcomeBinding.inflate(getLayoutInflater());
|
|
||||||
final View view = binding.getRoot();
|
|
||||||
setContentView(view);
|
|
||||||
|
|
||||||
if (getIntent() != null) {
|
|
||||||
final Bundle bundle = getIntent().getExtras();
|
|
||||||
if (bundle != null) {
|
|
||||||
isQuiz = bundle.getBoolean("isQuiz");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
isQuiz = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enable skip button if beta flavor
|
|
||||||
if (ConfigUtils.isBetaFlavour()) {
|
|
||||||
binding.finishTutorialButton.setVisibility(View.VISIBLE);
|
|
||||||
|
|
||||||
dialogBuilder = new AlertDialog.Builder(this);
|
|
||||||
copyrightBinding = PopupForCopyrightBinding.inflate(getLayoutInflater());
|
|
||||||
final View contactPopupView = copyrightBinding.getRoot();
|
|
||||||
dialogBuilder.setView(contactPopupView);
|
|
||||||
dialogBuilder.setCancelable(false);
|
|
||||||
dialog = dialogBuilder.create();
|
|
||||||
dialog.show();
|
|
||||||
|
|
||||||
copyrightBinding.buttonOk.setOnClickListener(v -> dialog.dismiss());
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.welcomePager.setAdapter(adapter);
|
|
||||||
binding.welcomePagerIndicator.setViewPager(binding.welcomePager);
|
|
||||||
|
|
||||||
binding.finishTutorialButton.setOnClickListener(v -> finishTutorial());
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* References WelcomePageAdapter to null before the activity is destroyed
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
if (isQuiz) {
|
|
||||||
final Intent i = new Intent(this, QuizActivity.class);
|
|
||||||
startActivity(i);
|
|
||||||
}
|
|
||||||
super.onDestroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a way to change current activity to WelcomeActivity
|
|
||||||
*
|
|
||||||
* @param context Activity context
|
|
||||||
*/
|
|
||||||
public static void startYourself(final Context context) {
|
|
||||||
final 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 (binding.welcomePager.getCurrentItem() != 0) {
|
|
||||||
binding.welcomePager.setCurrentItem(binding.welcomePager.getCurrentItem() - 1, true);
|
|
||||||
} else {
|
|
||||||
if (defaultKvStore.getBoolean("firstrun", true)) {
|
|
||||||
finishAffinity();
|
|
||||||
} else {
|
|
||||||
super.onBackPressed();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void finishTutorial() {
|
|
||||||
defaultKvStore.putBoolean("firstrun", false);
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
80
app/src/main/java/fr/free/nrw/commons/WelcomeActivity.kt
Normal file
80
app/src/main/java/fr/free/nrw/commons/WelcomeActivity.kt
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
package fr.free.nrw.commons;
|
|
||||||
|
|
||||||
import android.net.Uri;
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
70
app/src/main/java/fr/free/nrw/commons/WelcomePagerAdapter.kt
Normal file
70
app/src/main/java/fr/free/nrw/commons/WelcomePagerAdapter.kt
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -129,9 +129,10 @@ interface PageEditInterface {
|
||||||
): Observable<Entities>
|
): Observable<Entities>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get wiki text for provided file names
|
* Gets the wiki text for the provided file name.
|
||||||
* @param titles : Name of the file
|
*
|
||||||
* @return Single<MwQueryResult>
|
* @param title The title (name) of the file to fetch wiki text for.
|
||||||
|
* @return A Single emitting the wiki query response.
|
||||||
*/
|
*/
|
||||||
@GET(MW_API_PREFIX + "action=query&prop=revisions&rvprop=content|timestamp&rvlimit=1&converttitles=")
|
@GET(MW_API_PREFIX + "action=query&prop=revisions&rvprop=content|timestamp&rvlimit=1&converttitles=")
|
||||||
fun getWikiText(
|
fun getWikiText(
|
||||||
|
|
|
||||||
|
|
@ -158,7 +158,9 @@ class SingleWebViewActivity : ComponentActivity() {
|
||||||
|
|
||||||
webChromeClient = object : WebChromeClient() {
|
webChromeClient = object : WebChromeClient() {
|
||||||
override fun onConsoleMessage(message: ConsoleMessage): Boolean {
|
override fun onConsoleMessage(message: ConsoleMessage): Boolean {
|
||||||
Timber.d("Console: ${message.message()} -- From line ${message.lineNumber()} of ${message.sourceId()}")
|
Timber.d("%s%s",
|
||||||
|
"Console: ${message.message()} -- From line ",
|
||||||
|
"${message.lineNumber()} of ${message.sourceId()}")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,10 @@ import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.core.app.NavUtils
|
import androidx.core.app.NavUtils
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
import fr.free.nrw.commons.BuildConfig
|
import fr.free.nrw.commons.BuildConfig
|
||||||
import fr.free.nrw.commons.CommonsApplication
|
import fr.free.nrw.commons.CommonsApplication
|
||||||
import fr.free.nrw.commons.R
|
import fr.free.nrw.commons.R
|
||||||
import fr.free.nrw.commons.Utils
|
|
||||||
import fr.free.nrw.commons.auth.login.LoginCallback
|
import fr.free.nrw.commons.auth.login.LoginCallback
|
||||||
import fr.free.nrw.commons.auth.login.LoginClient
|
import fr.free.nrw.commons.auth.login.LoginClient
|
||||||
import fr.free.nrw.commons.auth.login.LoginResult
|
import fr.free.nrw.commons.auth.login.LoginResult
|
||||||
|
|
@ -33,11 +33,14 @@ import fr.free.nrw.commons.contributions.MainActivity
|
||||||
import fr.free.nrw.commons.databinding.ActivityLoginBinding
|
import fr.free.nrw.commons.databinding.ActivityLoginBinding
|
||||||
import fr.free.nrw.commons.di.ApplicationlessInjection
|
import fr.free.nrw.commons.di.ApplicationlessInjection
|
||||||
import fr.free.nrw.commons.kvstore.JsonKvStore
|
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.AbstractTextWatcher
|
||||||
import fr.free.nrw.commons.utils.ActivityUtils.startActivityWithFlags
|
import fr.free.nrw.commons.utils.ActivityUtils.startActivityWithFlags
|
||||||
import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour
|
import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour
|
||||||
import fr.free.nrw.commons.utils.SystemThemeUtils
|
import fr.free.nrw.commons.utils.SystemThemeUtils
|
||||||
import fr.free.nrw.commons.utils.ViewUtil.hideKeyboard
|
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 io.reactivex.disposables.CompositeDisposable
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
@ -65,6 +68,7 @@ class LoginActivity : AccountAuthenticatorActivity() {
|
||||||
private val delegate: AppCompatDelegate by lazy {
|
private val delegate: AppCompatDelegate by lazy {
|
||||||
AppCompatDelegate.create(this, null)
|
AppCompatDelegate.create(this, null)
|
||||||
}
|
}
|
||||||
|
private var lastLoginResult: LoginResult? = null
|
||||||
|
|
||||||
public override fun onCreate(savedInstanceState: Bundle?) {
|
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
@ -78,7 +82,14 @@ class LoginActivity : AccountAuthenticatorActivity() {
|
||||||
delegate.installViewFactory()
|
delegate.installViewFactory()
|
||||||
delegate.onCreate(savedInstanceState)
|
delegate.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
WindowCompat.getInsetsController(window, window.decorView)
|
||||||
|
.isAppearanceLightStatusBars = !isDarkTheme
|
||||||
|
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
|
||||||
binding = ActivityLoginBinding.inflate(layoutInflater)
|
binding = ActivityLoginBinding.inflate(layoutInflater)
|
||||||
|
applyEdgeToEdgeAllInsets(binding!!.root)
|
||||||
|
binding!!.root.handleKeyboardInsets()
|
||||||
with(binding!!) {
|
with(binding!!) {
|
||||||
setContentView(root)
|
setContentView(root)
|
||||||
|
|
||||||
|
|
@ -91,7 +102,19 @@ class LoginActivity : AccountAuthenticatorActivity() {
|
||||||
aboutPrivacyPolicy.setOnClickListener { onPrivacyPolicyClicked() }
|
aboutPrivacyPolicy.setOnClickListener { onPrivacyPolicyClicked() }
|
||||||
signUpButton.setOnClickListener { signUp() }
|
signUpButton.setOnClickListener { signUp() }
|
||||||
loginButton.setOnClickListener { performLogin() }
|
loginButton.setOnClickListener { performLogin() }
|
||||||
loginPassword.setOnEditorActionListener(::onEditorAction)
|
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 =
|
loginPassword.onFocusChangeListener =
|
||||||
View.OnFocusChangeListener(::onPasswordFocusChanged)
|
View.OnFocusChangeListener(::onPasswordFocusChanged)
|
||||||
|
|
@ -112,6 +135,39 @@ class LoginActivity : AccountAuthenticatorActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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?) {
|
override fun onPostCreate(savedInstanceState: Bundle?) {
|
||||||
super.onPostCreate(savedInstanceState)
|
super.onPostCreate(savedInstanceState)
|
||||||
delegate.onPostCreate(savedInstanceState)
|
delegate.onPostCreate(savedInstanceState)
|
||||||
|
|
@ -235,7 +291,7 @@ class LoginActivity : AccountAuthenticatorActivity() {
|
||||||
} else false
|
} else false
|
||||||
|
|
||||||
private fun isTriggerAction(actionId: Int, keyEvent: KeyEvent?) =
|
private fun isTriggerAction(actionId: Int, keyEvent: KeyEvent?) =
|
||||||
actionId == EditorInfo.IME_ACTION_DONE || keyEvent?.keyCode == KeyEvent.KEYCODE_ENTER
|
actionId == EditorInfo.IME_ACTION_NEXT || actionId == EditorInfo.IME_ACTION_DONE || keyEvent?.keyCode == KeyEvent.KEYCODE_ENTER
|
||||||
|
|
||||||
private fun skipLogin() {
|
private fun skipLogin() {
|
||||||
AlertDialog.Builder(this)
|
AlertDialog.Builder(this)
|
||||||
|
|
@ -253,10 +309,10 @@ class LoginActivity : AccountAuthenticatorActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun forgotPassword() =
|
private fun forgotPassword() =
|
||||||
Utils.handleWebUrl(this, Uri.parse(BuildConfig.FORGOT_PASSWORD_URL))
|
handleWebUrl(this, Uri.parse(BuildConfig.FORGOT_PASSWORD_URL))
|
||||||
|
|
||||||
private fun onPrivacyPolicyClicked() =
|
private fun onPrivacyPolicyClicked() =
|
||||||
Utils.handleWebUrl(this, Uri.parse(BuildConfig.PRIVACY_POLICY_URL))
|
handleWebUrl(this, Uri.parse(BuildConfig.PRIVACY_POLICY_URL))
|
||||||
|
|
||||||
private fun signUp() =
|
private fun signUp() =
|
||||||
startActivity(Intent(this, SignupActivity::class.java))
|
startActivity(Intent(this, SignupActivity::class.java))
|
||||||
|
|
@ -271,6 +327,7 @@ class LoginActivity : AccountAuthenticatorActivity() {
|
||||||
showLoggingProgressBar()
|
showLoggingProgressBar()
|
||||||
loginClient.doLogin(username,
|
loginClient.doLogin(username,
|
||||||
password,
|
password,
|
||||||
|
lastLoginResult,
|
||||||
twoFactorCode,
|
twoFactorCode,
|
||||||
Locale.getDefault().language,
|
Locale.getDefault().language,
|
||||||
object : LoginCallback {
|
object : LoginCallback {
|
||||||
|
|
@ -280,10 +337,18 @@ class LoginActivity : AccountAuthenticatorActivity() {
|
||||||
onLoginSuccess(loginResult)
|
onLoginSuccess(loginResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun twoFactorPrompt(caught: Throwable, token: String?) = runOnUiThread {
|
override fun twoFactorPrompt(loginResult: LoginResult, caught: Throwable, token: String?) = runOnUiThread {
|
||||||
Timber.d("Requesting 2FA prompt")
|
Timber.d("Requesting 2FA prompt")
|
||||||
progressDialog!!.dismiss()
|
progressDialog!!.dismiss()
|
||||||
askUserForTwoFactorAuth()
|
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 {
|
override fun passwordResetPrompt(token: String?) = runOnUiThread {
|
||||||
|
|
@ -338,15 +403,35 @@ class LoginActivity : AccountAuthenticatorActivity() {
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
fun askUserForTwoFactorAuth() {
|
fun askUserForTwoFactorAuth() {
|
||||||
|
if (binding == null) {
|
||||||
|
Timber.w("Binding is null, reinitializing in askUserForTwoFactorAuth")
|
||||||
|
binding = ActivityLoginBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding!!.root)
|
||||||
|
}
|
||||||
progressDialog!!.dismiss()
|
progressDialog!!.dismiss()
|
||||||
with(binding!!) {
|
if (binding != null) {
|
||||||
twoFactorContainer.visibility = View.VISIBLE
|
with(binding!!) {
|
||||||
loginTwoFactor.visibility = View.VISIBLE
|
twoFactorContainer.visibility = View.VISIBLE
|
||||||
loginTwoFactor.requestFocus()
|
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
|
val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
|
||||||
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY)
|
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY)
|
||||||
showMessageAndCancelDialog(R.string.login_failed_2fa_needed)
|
showMessageAndCancelDialog(getString(if (lastLoginResult is LoginResult.EmailAuthResult) R.string.login_failed_email_auth_needed else R.string.login_failed_2fa_needed))
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import android.widget.Toast
|
||||||
import fr.free.nrw.commons.BuildConfig
|
import fr.free.nrw.commons.BuildConfig
|
||||||
import fr.free.nrw.commons.R
|
import fr.free.nrw.commons.R
|
||||||
import fr.free.nrw.commons.theme.BaseActivity
|
import fr.free.nrw.commons.theme.BaseActivity
|
||||||
|
import fr.free.nrw.commons.utils.applyEdgeToEdgeAllInsets
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
class SignupActivity : BaseActivity() {
|
class SignupActivity : BaseActivity() {
|
||||||
|
|
@ -21,6 +22,7 @@ class SignupActivity : BaseActivity() {
|
||||||
Timber.d("Signup Activity started")
|
Timber.d("Signup Activity started")
|
||||||
|
|
||||||
webView = WebView(this)
|
webView = WebView(this)
|
||||||
|
applyEdgeToEdgeAllInsets(webView!!)
|
||||||
with(webView!!) {
|
with(webView!!) {
|
||||||
setContentView(this)
|
setContentView(this)
|
||||||
webViewClient = MyWebViewClient()
|
webViewClient = MyWebViewClient()
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ class CsrfTokenClient(
|
||||||
try {
|
try {
|
||||||
if (retry > 0) {
|
if (retry > 0) {
|
||||||
// Log in explicitly
|
// Log in explicitly
|
||||||
loginClient.loginBlocking(userName, password, "")
|
loginClient.loginBlocking(userName, password)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get CSRFToken response off the main thread.
|
// Get CSRFToken response off the main thread.
|
||||||
|
|
@ -92,6 +92,8 @@ class CsrfTokenClient(
|
||||||
override fun failure(caught: Throwable?) = retryWithLogin(cb) { caught }
|
override fun failure(caught: Throwable?) = retryWithLogin(cb) { caught }
|
||||||
|
|
||||||
override fun twoFactorPrompt() = cb.twoFactorPrompt()
|
override fun twoFactorPrompt() = cb.twoFactorPrompt()
|
||||||
|
|
||||||
|
override fun emailAuthPrompt() = cb.emailAuthPrompt()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -165,10 +167,17 @@ class CsrfTokenClient(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun twoFactorPrompt(
|
override fun twoFactorPrompt(
|
||||||
|
loginResult: LoginResult,
|
||||||
caught: Throwable,
|
caught: Throwable,
|
||||||
token: String?,
|
token: String?,
|
||||||
) = callback.twoFactorPrompt()
|
) = callback.twoFactorPrompt()
|
||||||
|
|
||||||
|
override fun emailAuthPrompt(
|
||||||
|
loginResult: LoginResult,
|
||||||
|
caught: Throwable,
|
||||||
|
token: String?,
|
||||||
|
) = callback.emailAuthPrompt()
|
||||||
|
|
||||||
// Should not happen here, but call the callback just in case.
|
// 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 passwordResetPrompt(token: String?) = callback.failure(LoginFailedException("Logged in with temporary password."))
|
||||||
|
|
||||||
|
|
@ -190,6 +199,8 @@ class CsrfTokenClient(
|
||||||
fun failure(caught: Throwable?)
|
fun failure(caught: Throwable?)
|
||||||
|
|
||||||
fun twoFactorPrompt()
|
fun twoFactorPrompt()
|
||||||
|
|
||||||
|
fun emailAuthPrompt()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,13 @@ interface LoginCallback {
|
||||||
fun success(loginResult: LoginResult)
|
fun success(loginResult: LoginResult)
|
||||||
|
|
||||||
fun twoFactorPrompt(
|
fun twoFactorPrompt(
|
||||||
|
loginResult: LoginResult,
|
||||||
|
caught: Throwable,
|
||||||
|
token: String?,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun emailAuthPrompt(
|
||||||
|
loginResult: LoginResult,
|
||||||
caught: Throwable,
|
caught: Throwable,
|
||||||
token: String?,
|
token: String?,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package fr.free.nrw.commons.auth.login
|
package fr.free.nrw.commons.auth.login
|
||||||
|
|
||||||
import android.text.TextUtils
|
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.OAuthResult
|
||||||
import fr.free.nrw.commons.auth.login.LoginResult.ResetPasswordResult
|
import fr.free.nrw.commons.auth.login.LoginResult.ResetPasswordResult
|
||||||
import fr.free.nrw.commons.wikidata.WikidataConstants.WIKIPEDIA_URL
|
import fr.free.nrw.commons.wikidata.WikidataConstants.WIKIPEDIA_URL
|
||||||
|
|
@ -51,6 +52,7 @@ class LoginClient(
|
||||||
password,
|
password,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
null,
|
||||||
response.body()!!.query()!!.loginToken(),
|
response.body()!!.query()!!.loginToken(),
|
||||||
userLanguage,
|
userLanguage,
|
||||||
cb,
|
cb,
|
||||||
|
|
@ -75,6 +77,7 @@ class LoginClient(
|
||||||
password: String,
|
password: String,
|
||||||
retypedPassword: String?,
|
retypedPassword: String?,
|
||||||
twoFactorCode: String?,
|
twoFactorCode: String?,
|
||||||
|
emailAuthCode: String?,
|
||||||
loginToken: String?,
|
loginToken: String?,
|
||||||
userLanguage: String,
|
userLanguage: String,
|
||||||
cb: LoginCallback,
|
cb: LoginCallback,
|
||||||
|
|
@ -82,7 +85,7 @@ class LoginClient(
|
||||||
this.userLanguage = userLanguage
|
this.userLanguage = userLanguage
|
||||||
|
|
||||||
loginCall =
|
loginCall =
|
||||||
if (twoFactorCode.isNullOrEmpty() && retypedPassword.isNullOrEmpty()) {
|
if (twoFactorCode.isNullOrEmpty() && emailAuthCode.isNullOrEmpty() && retypedPassword.isNullOrEmpty()) {
|
||||||
loginInterface.postLogIn(userName, password, loginToken, userLanguage, WIKIPEDIA_URL)
|
loginInterface.postLogIn(userName, password, loginToken, userLanguage, WIKIPEDIA_URL)
|
||||||
} else {
|
} else {
|
||||||
loginInterface.postLogIn(
|
loginInterface.postLogIn(
|
||||||
|
|
@ -90,6 +93,7 @@ class LoginClient(
|
||||||
password,
|
password,
|
||||||
retypedPassword,
|
retypedPassword,
|
||||||
twoFactorCode,
|
twoFactorCode,
|
||||||
|
emailAuthCode,
|
||||||
loginToken,
|
loginToken,
|
||||||
userLanguage,
|
userLanguage,
|
||||||
true,
|
true,
|
||||||
|
|
@ -112,10 +116,18 @@ class LoginClient(
|
||||||
when (loginResult) {
|
when (loginResult) {
|
||||||
is OAuthResult ->
|
is OAuthResult ->
|
||||||
cb.twoFactorPrompt(
|
cb.twoFactorPrompt(
|
||||||
|
loginResult,
|
||||||
LoginFailedException(loginResult.message),
|
LoginFailedException(loginResult.message),
|
||||||
loginToken,
|
loginToken,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
is EmailAuthResult ->
|
||||||
|
cb.emailAuthPrompt(
|
||||||
|
loginResult,
|
||||||
|
LoginFailedException(loginResult.message),
|
||||||
|
loginToken
|
||||||
|
)
|
||||||
|
|
||||||
is ResetPasswordResult -> cb.passwordResetPrompt(loginToken)
|
is ResetPasswordResult -> cb.passwordResetPrompt(loginToken)
|
||||||
|
|
||||||
is LoginResult.Result ->
|
is LoginResult.Result ->
|
||||||
|
|
@ -147,6 +159,7 @@ class LoginClient(
|
||||||
fun doLogin(
|
fun doLogin(
|
||||||
username: String,
|
username: String,
|
||||||
password: String,
|
password: String,
|
||||||
|
lastLoginResult: LoginResult?,
|
||||||
twoFactorCode: String,
|
twoFactorCode: String,
|
||||||
userLanguage: String,
|
userLanguage: String,
|
||||||
loginCallback: LoginCallback,
|
loginCallback: LoginCallback,
|
||||||
|
|
@ -159,7 +172,10 @@ class LoginClient(
|
||||||
) = if (response.isSuccessful) {
|
) = if (response.isSuccessful) {
|
||||||
val loginToken = response.body()?.query()?.loginToken()
|
val loginToken = response.body()?.query()?.loginToken()
|
||||||
loginToken?.let {
|
loginToken?.let {
|
||||||
login(username, password, null, twoFactorCode, it, userLanguage, loginCallback)
|
login(username, password, null,
|
||||||
|
if (lastLoginResult is OAuthResult) twoFactorCode else null,
|
||||||
|
if (lastLoginResult is EmailAuthResult) twoFactorCode else null,
|
||||||
|
it, userLanguage, loginCallback)
|
||||||
} ?: run {
|
} ?: run {
|
||||||
loginCallback.error(IOException("Failed to retrieve login token"))
|
loginCallback.error(IOException("Failed to retrieve login token"))
|
||||||
}
|
}
|
||||||
|
|
@ -181,7 +197,8 @@ class LoginClient(
|
||||||
fun loginBlocking(
|
fun loginBlocking(
|
||||||
userName: String,
|
userName: String,
|
||||||
password: String,
|
password: String,
|
||||||
twoFactorCode: String?,
|
twoFactorCode: String? = null,
|
||||||
|
emailAuthCode: String? = null
|
||||||
) {
|
) {
|
||||||
val tokenResponse = getLoginToken().execute()
|
val tokenResponse = getLoginToken().execute()
|
||||||
if (tokenResponse
|
if (tokenResponse
|
||||||
|
|
@ -195,7 +212,7 @@ class LoginClient(
|
||||||
|
|
||||||
val loginToken = tokenResponse.body()?.query()?.loginToken()
|
val loginToken = tokenResponse.body()?.query()?.loginToken()
|
||||||
val tempLoginCall =
|
val tempLoginCall =
|
||||||
if (twoFactorCode.isNullOrEmpty()) {
|
if (twoFactorCode.isNullOrEmpty() && emailAuthCode.isNullOrEmpty()) {
|
||||||
loginInterface.postLogIn(userName, password, loginToken, userLanguage, WIKIPEDIA_URL)
|
loginInterface.postLogIn(userName, password, loginToken, userLanguage, WIKIPEDIA_URL)
|
||||||
} else {
|
} else {
|
||||||
loginInterface.postLogIn(
|
loginInterface.postLogIn(
|
||||||
|
|
@ -203,6 +220,7 @@ class LoginClient(
|
||||||
password,
|
password,
|
||||||
null,
|
null,
|
||||||
twoFactorCode,
|
twoFactorCode,
|
||||||
|
emailAuthCode,
|
||||||
loginToken,
|
loginToken,
|
||||||
userLanguage,
|
userLanguage,
|
||||||
true,
|
true,
|
||||||
|
|
@ -214,7 +232,7 @@ class LoginClient(
|
||||||
val loginResult = loginResponse.toLoginResult(password) ?: throw IOException("Unexpected response when logging in.")
|
val loginResult = loginResponse.toLoginResult(password) ?: throw IOException("Unexpected response when logging in.")
|
||||||
|
|
||||||
if ("UI" == loginResult.status) {
|
if ("UI" == loginResult.status) {
|
||||||
if (loginResult is OAuthResult) {
|
if (loginResult is OAuthResult || loginResult is EmailAuthResult) {
|
||||||
// TODO: Find a better way to boil up the warning about 2FA
|
// TODO: Find a better way to boil up the warning about 2FA
|
||||||
throw LoginFailedException(loginResult.message)
|
throw LoginFailedException(loginResult.message)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,8 @@ interface LoginInterface {
|
||||||
@Field("password") pass: String?,
|
@Field("password") pass: String?,
|
||||||
@Field("retype") retypedPass: String?,
|
@Field("retype") retypedPass: String?,
|
||||||
@Field("OATHToken") twoFactorCode: String?,
|
@Field("OATHToken") twoFactorCode: String?,
|
||||||
@Field("logintoken") token: String?,
|
@Field("token") emailAuthToken: String?,
|
||||||
|
@Field("logintoken") loginToken: String?,
|
||||||
@Field("uselang") userLanguage: String?,
|
@Field("uselang") userLanguage: String?,
|
||||||
@Field("logincontinue") loginContinue: Boolean,
|
@Field("logincontinue") loginContinue: Boolean,
|
||||||
): Call<LoginResponse?>
|
): Call<LoginResponse?>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package fr.free.nrw.commons.auth.login
|
||||||
|
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
import fr.free.nrw.commons.auth.login.LoginResult.OAuthResult
|
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.ResetPasswordResult
|
||||||
import fr.free.nrw.commons.auth.login.LoginResult.Result
|
import fr.free.nrw.commons.auth.login.LoginResult.Result
|
||||||
import fr.free.nrw.commons.wikidata.mwapi.MwServiceError
|
import fr.free.nrw.commons.wikidata.mwapi.MwServiceError
|
||||||
|
|
@ -27,11 +28,13 @@ internal class ClientLogin {
|
||||||
fun toLoginResult(password: String): LoginResult {
|
fun toLoginResult(password: String): LoginResult {
|
||||||
var userMessage = message
|
var userMessage = message
|
||||||
if ("UI" == status) {
|
if ("UI" == status) {
|
||||||
if (requests != null) {
|
requests?.forEach { request ->
|
||||||
for (req in requests) {
|
request.id()?.let {
|
||||||
if ("MediaWiki\\Extension\\OATHAuth\\Auth\\TOTPAuthenticationRequest" == req.id()) {
|
if (it.endsWith("TOTPAuthenticationRequest")) {
|
||||||
return OAuthResult(status, userName, password, message)
|
return OAuthResult(status, userName, password, message)
|
||||||
} else if ("MediaWiki\\Auth\\PasswordAuthenticationRequest" == req.id()) {
|
} else if (it.endsWith("EmailAuthAuthenticationRequest")) {
|
||||||
|
return EmailAuthResult(status, userName, password, message)
|
||||||
|
} else if (it.endsWith("PasswordAuthenticationRequest")) {
|
||||||
return ResetPasswordResult(status, userName, password, message)
|
return ResetPasswordResult(status, userName, password, message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -49,7 +52,7 @@ internal class Request {
|
||||||
private val required: String? = null
|
private val required: String? = null
|
||||||
private val provider: String? = null
|
private val provider: String? = null
|
||||||
private val account: String? = null
|
private val account: String? = null
|
||||||
private val fields: Map<String, RequestField>? = null
|
internal val fields: Map<String, RequestField>? = null
|
||||||
|
|
||||||
fun id(): String? = id
|
fun id(): String? = id
|
||||||
}
|
}
|
||||||
|
|
@ -57,5 +60,5 @@ internal class Request {
|
||||||
internal class RequestField {
|
internal class RequestField {
|
||||||
private val type: String? = null
|
private val type: String? = null
|
||||||
private val label: String? = null
|
private val label: String? = null
|
||||||
private val help: String? = null
|
internal val help: String? = null
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,13 @@ sealed class LoginResult(
|
||||||
message: String?,
|
message: String?,
|
||||||
) : LoginResult(status, userName, password, message)
|
) : LoginResult(status, userName, password, message)
|
||||||
|
|
||||||
|
class EmailAuthResult(
|
||||||
|
status: String,
|
||||||
|
userName: String?,
|
||||||
|
password: String?,
|
||||||
|
message: String?,
|
||||||
|
) : LoginResult(status, userName, password, message)
|
||||||
|
|
||||||
class ResetPasswordResult(
|
class ResetPasswordResult(
|
||||||
status: String,
|
status: String,
|
||||||
userName: String?,
|
userName: String?,
|
||||||
|
|
|
||||||
|
|
@ -1,105 +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 androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.fragment.app.FragmentManager;
|
|
||||||
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 fr.free.nrw.commons.contributions.ContributionController;
|
|
||||||
import javax.inject.Named;
|
|
||||||
|
|
||||||
public class BookmarkFragment extends CommonsDaggerSupportFragment {
|
|
||||||
|
|
||||||
private FragmentManager supportFragmentManager;
|
|
||||||
private BookmarksPagerAdapter adapter;
|
|
||||||
FragmentBookmarksBinding binding;
|
|
||||||
|
|
||||||
@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) {
|
|
||||||
if (binding!=null) {
|
|
||||||
binding.viewPagerBookmarks.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);
|
|
||||||
binding = FragmentBookmarksBinding.inflate(inflater, container, false);
|
|
||||||
|
|
||||||
// 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"));
|
|
||||||
binding.viewPagerBookmarks.setAdapter(adapter);
|
|
||||||
binding.tabLayout.setupWithViewPager(binding.viewPagerBookmarks);
|
|
||||||
|
|
||||||
((MainActivity) getActivity()).showTabs();
|
|
||||||
((BaseActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false);
|
|
||||||
|
|
||||||
setupTabLayout();
|
|
||||||
return binding.getRoot();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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() {
|
|
||||||
binding.tabLayout.setVisibility(View.VISIBLE);
|
|
||||||
if (adapter.getCount() == 1) {
|
|
||||||
binding.tabLayout.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void onBackPressed() {
|
|
||||||
if (((BookmarkListRootFragment) (adapter.getItem(binding.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);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
binding = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,266 +0,0 @@
|
||||||
package fr.free.nrw.commons.bookmarks;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.AdapterView;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
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.navtab.NavTab;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
FragmentFeaturedRootBinding binding;
|
|
||||||
|
|
||||||
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");
|
|
||||||
|
|
||||||
switch (order){
|
|
||||||
case 0: listFragment = new BookmarkPicturesFragment();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 1: listFragment = new BookmarkLocationsFragment();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 3: listFragment = new BookmarkCategoriesFragment();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
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);
|
|
||||||
binding = FragmentFeaturedRootBinding.inflate(inflater, container, false);
|
|
||||||
return binding.getRoot();
|
|
||||||
}
|
|
||||||
|
|
||||||
@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) {
|
|
||||||
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
|
|
||||||
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 = MediaDetailPagerFragment.newInstance(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) {
|
|
||||||
Timber.d("on media clicked");
|
|
||||||
binding.exploreContainer.setVisibility(View.VISIBLE);
|
|
||||||
((BookmarkFragment) getParentFragment()).binding.tabLayout.setVisibility(View.GONE);
|
|
||||||
mediaDetails = MediaDetailPagerFragment.newInstance(false, true);
|
|
||||||
((BookmarkFragment) getParentFragment()).setScroll(false);
|
|
||||||
setFragment(mediaDetails, listFragment);
|
|
||||||
mediaDetails.showImage(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBackStackChanged() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
binding = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,226 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,94 +0,0 @@
|
||||||
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.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)));
|
|
||||||
}
|
|
||||||
final Bundle categoriesBundle = new Bundle();
|
|
||||||
categoriesBundle.putString("categoryName",
|
|
||||||
context.getString(R.string.title_page_bookmarks_categories));
|
|
||||||
categoriesBundle.putInt("order", 3);
|
|
||||||
pages.add(new BookmarkPages(
|
|
||||||
new BookmarkListRootFragment(categoriesBundle, this),
|
|
||||||
context.getString(R.string.title_page_bookmarks_categories)));
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
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,129 +0,0 @@
|
||||||
package fr.free.nrw.commons.bookmarks.items;
|
|
||||||
|
|
||||||
import static fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table.COLUMN_ID;
|
|
||||||
import static fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table.TABLE_NAME;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
import android.database.sqlite.SQLiteQueryBuilder;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import fr.free.nrw.commons.BuildConfig;
|
|
||||||
import fr.free.nrw.commons.data.DBOpenHelper;
|
|
||||||
import fr.free.nrw.commons.di.CommonsDaggerContentProvider;
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles private storage for bookmarked items
|
|
||||||
*/
|
|
||||||
public class BookmarkItemsContentProvider extends CommonsDaggerContentProvider {
|
|
||||||
|
|
||||||
private static final String BASE_PATH = "bookmarksItems";
|
|
||||||
public static final Uri BASE_URI =
|
|
||||||
Uri.parse("content://" + BuildConfig.BOOKMARK_ITEMS_AUTHORITY + "/" + BASE_PATH);
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Append bookmark items ID to the base uri
|
|
||||||
*/
|
|
||||||
public static Uri uriForName(final String id) {
|
|
||||||
return Uri.parse(BASE_URI + "/" + id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
DBOpenHelper dbOpenHelper;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getType(@NonNull final Uri uri) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Queries the SQLite database for the bookmark items
|
|
||||||
* @param uri : contains the uri for bookmark items
|
|
||||||
* @param projection : contains the all fields of the table
|
|
||||||
* @param selection : handles Where
|
|
||||||
* @param selectionArgs : the condition of Where clause
|
|
||||||
* @param sortOrder : ascending or descending
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("ConstantConditions")
|
|
||||||
@Override
|
|
||||||
public Cursor query(@NonNull final Uri uri, final String[] projection, final String selection,
|
|
||||||
final String[] selectionArgs, final String sortOrder) {
|
|
||||||
final SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
|
|
||||||
queryBuilder.setTables(TABLE_NAME);
|
|
||||||
final SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
|
|
||||||
final Cursor cursor = queryBuilder.query(db, projection, selection,
|
|
||||||
selectionArgs, null, null, sortOrder);
|
|
||||||
cursor.setNotificationUri(getContext().getContentResolver(), uri);
|
|
||||||
return cursor;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the update query of local SQLite Database
|
|
||||||
* @param uri : contains the uri for bookmark items
|
|
||||||
* @param contentValues : new values to be entered to db
|
|
||||||
* @param selection : handles Where
|
|
||||||
* @param selectionArgs : the condition of Where clause
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("ConstantConditions")
|
|
||||||
@Override
|
|
||||||
public int update(@NonNull final Uri uri, final ContentValues contentValues,
|
|
||||||
final String selection, final String[] selectionArgs) {
|
|
||||||
final SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
|
||||||
final int rowsUpdated;
|
|
||||||
if (TextUtils.isEmpty(selection)) {
|
|
||||||
final int id = Integer.parseInt(uri.getLastPathSegment());
|
|
||||||
rowsUpdated = sqlDB.update(TABLE_NAME,
|
|
||||||
contentValues,
|
|
||||||
COLUMN_ID + " = ?",
|
|
||||||
new String[]{String.valueOf(id)});
|
|
||||||
} else {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"Parameter `selection` should be empty when updating an ID");
|
|
||||||
}
|
|
||||||
|
|
||||||
getContext().getContentResolver().notifyChange(uri, null);
|
|
||||||
return rowsUpdated;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the insertion of new bookmark items record to local SQLite Database
|
|
||||||
* @param uri
|
|
||||||
* @param contentValues
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("ConstantConditions")
|
|
||||||
@Override
|
|
||||||
public Uri insert(@NonNull final Uri uri, final ContentValues contentValues) {
|
|
||||||
final SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
|
||||||
final long id = sqlDB.insert(TABLE_NAME, null, contentValues);
|
|
||||||
getContext().getContentResolver().notifyChange(uri, null);
|
|
||||||
return Uri.parse(BASE_URI + "/" + id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the deletion of new bookmark items record to local SQLite Database
|
|
||||||
* @param uri
|
|
||||||
* @param s
|
|
||||||
* @param strings
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("ConstantConditions")
|
|
||||||
@Override
|
|
||||||
public int delete(@NonNull final Uri uri, final String s, final String[] strings) {
|
|
||||||
final int rows;
|
|
||||||
final SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
|
|
||||||
Timber.d("Deleting bookmark name %s", uri.getLastPathSegment());
|
|
||||||
rows = db.delete(
|
|
||||||
TABLE_NAME,
|
|
||||||
"item_id = ?",
|
|
||||||
new String[]{uri.getLastPathSegment()}
|
|
||||||
);
|
|
||||||
getContext().getContentResolver().notifyChange(uri, null);
|
|
||||||
return rows;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
package fr.free.nrw.commons.bookmarks.items
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.database.Cursor
|
||||||
|
import android.database.sqlite.SQLiteQueryBuilder
|
||||||
|
import android.net.Uri
|
||||||
|
import fr.free.nrw.commons.BuildConfig
|
||||||
|
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.TABLE_NAME
|
||||||
|
import fr.free.nrw.commons.di.CommonsDaggerContentProvider
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_ID
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles private storage for bookmarked items
|
||||||
|
*/
|
||||||
|
class BookmarkItemsContentProvider : CommonsDaggerContentProvider() {
|
||||||
|
override fun getType(uri: Uri): String? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queries the SQLite database for the bookmark items
|
||||||
|
* @param uri : contains the uri for bookmark items
|
||||||
|
* @param projection : contains the all fields of the table
|
||||||
|
* @param selection : handles Where
|
||||||
|
* @param selectionArgs : the condition of Where clause
|
||||||
|
* @param sortOrder : ascending or descending
|
||||||
|
*/
|
||||||
|
override fun query(
|
||||||
|
uri: Uri, projection: Array<String>?, selection: String?,
|
||||||
|
selectionArgs: Array<String>?, sortOrder: String?
|
||||||
|
): Cursor {
|
||||||
|
val queryBuilder = SQLiteQueryBuilder().apply {
|
||||||
|
tables = TABLE_NAME
|
||||||
|
}
|
||||||
|
|
||||||
|
return queryBuilder.query(
|
||||||
|
requireDb(), projection, selection,
|
||||||
|
selectionArgs, null, null, sortOrder
|
||||||
|
).apply {
|
||||||
|
setNotificationUri(context?.contentResolver, uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the update query of local SQLite Database
|
||||||
|
* @param uri : contains the uri for bookmark items
|
||||||
|
* @param contentValues : new values to be entered to db
|
||||||
|
* @param selection : handles Where
|
||||||
|
* @param selectionArgs : the condition of Where clause
|
||||||
|
*/
|
||||||
|
override fun update(
|
||||||
|
uri: Uri, contentValues: ContentValues?,
|
||||||
|
selection: String?, selectionArgs: Array<String>?
|
||||||
|
): Int {
|
||||||
|
val rowsUpdated: Int
|
||||||
|
if (selection.isNullOrEmpty()) {
|
||||||
|
val id = uri.lastPathSegment!!.toInt()
|
||||||
|
rowsUpdated = requireDb().update(
|
||||||
|
TABLE_NAME,
|
||||||
|
contentValues,
|
||||||
|
"$COLUMN_ID = ?",
|
||||||
|
arrayOf(id.toString())
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
throw IllegalArgumentException(
|
||||||
|
"Parameter `selection` should be empty when updating an ID"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
context?.contentResolver?.notifyChange(uri, null)
|
||||||
|
return rowsUpdated
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the insertion of new bookmark items record to local SQLite Database
|
||||||
|
*/
|
||||||
|
override fun insert(uri: Uri, contentValues: ContentValues?): Uri? {
|
||||||
|
val id = requireDb().insert(TABLE_NAME, null, contentValues)
|
||||||
|
context?.contentResolver?.notifyChange(uri, null)
|
||||||
|
return "$BASE_URI/$id".toUri()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the deletion of new bookmark items record to local SQLite Database
|
||||||
|
*/
|
||||||
|
override fun delete(uri: Uri, s: String?, strings: Array<String>?): Int {
|
||||||
|
val rows: Int = requireDb().delete(
|
||||||
|
TABLE_NAME,
|
||||||
|
"$COLUMN_ID = ?",
|
||||||
|
arrayOf(uri.lastPathSegment)
|
||||||
|
)
|
||||||
|
context?.contentResolver?.notifyChange(uri, null)
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val BASE_PATH = "bookmarksItems"
|
||||||
|
val BASE_URI: Uri = "content://${BuildConfig.BOOKMARK_ITEMS_AUTHORITY}/$BASE_PATH".toUri()
|
||||||
|
fun uriForName(id: String) = "$BASE_URI/$id".toUri()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
package fr.free.nrw.commons.bookmarks.items;
|
|
||||||
|
|
||||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
|
|
||||||
import java.util.List;
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javax.inject.Singleton;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles loading bookmarked items from Database
|
|
||||||
*/
|
|
||||||
@Singleton
|
|
||||||
public class BookmarkItemsController {
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
BookmarkItemsDao bookmarkItemsDao;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public BookmarkItemsController() {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load from DB the bookmarked items
|
|
||||||
* @return a list of DepictedItem objects.
|
|
||||||
*/
|
|
||||||
public List<DepictedItem> loadFavoritesItems() {
|
|
||||||
return bookmarkItemsDao.getAllBookmarksItems();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
package fr.free.nrw.commons.bookmarks.items
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles loading bookmarked items from Database
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
class BookmarkItemsController @Inject constructor() {
|
||||||
|
@JvmField
|
||||||
|
@Inject
|
||||||
|
var bookmarkItemsDao: BookmarkItemsDao? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load from DB the bookmarked items
|
||||||
|
* @return a list of DepictedItem objects.
|
||||||
|
*/
|
||||||
|
fun loadFavoritesItems(): List<DepictedItem> {
|
||||||
|
return bookmarkItemsDao?.getAllBookmarksItems() ?: emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,329 +0,0 @@
|
||||||
package fr.free.nrw.commons.bookmarks.items;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.content.ContentProviderClient;
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
import android.os.RemoteException;
|
|
||||||
import fr.free.nrw.commons.category.CategoryItem;
|
|
||||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javax.inject.Named;
|
|
||||||
import javax.inject.Provider;
|
|
||||||
import javax.inject.Singleton;
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles database operations for bookmarked items
|
|
||||||
*/
|
|
||||||
@Singleton
|
|
||||||
public class BookmarkItemsDao {
|
|
||||||
|
|
||||||
private final Provider<ContentProviderClient> clientProvider;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public BookmarkItemsDao(
|
|
||||||
@Named("bookmarksItem") final Provider<ContentProviderClient> clientProvider) {
|
|
||||||
this.clientProvider = clientProvider;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find all persisted items bookmarks on database
|
|
||||||
* @return list of bookmarks
|
|
||||||
*/
|
|
||||||
public List<DepictedItem> getAllBookmarksItems() {
|
|
||||||
final List<DepictedItem> items = new ArrayList<>();
|
|
||||||
final ContentProviderClient db = clientProvider.get();
|
|
||||||
try (final Cursor cursor = db.query(
|
|
||||||
BookmarkItemsContentProvider.BASE_URI,
|
|
||||||
Table.ALL_FIELDS,
|
|
||||||
null,
|
|
||||||
new String[]{},
|
|
||||||
null)) {
|
|
||||||
while (cursor != null && cursor.moveToNext()) {
|
|
||||||
items.add(fromCursor(cursor));
|
|
||||||
}
|
|
||||||
} catch (final RemoteException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
} finally {
|
|
||||||
db.release();
|
|
||||||
}
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Look for a bookmark in database and in order to insert or delete it
|
|
||||||
* @param depictedItem : Bookmark object
|
|
||||||
* @return boolean : is bookmark now favorite ?
|
|
||||||
*/
|
|
||||||
public boolean updateBookmarkItem(final DepictedItem depictedItem) {
|
|
||||||
final boolean bookmarkExists = findBookmarkItem(depictedItem.getId());
|
|
||||||
if (bookmarkExists) {
|
|
||||||
deleteBookmarkItem(depictedItem);
|
|
||||||
} else {
|
|
||||||
addBookmarkItem(depictedItem);
|
|
||||||
}
|
|
||||||
return !bookmarkExists;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a Bookmark to database
|
|
||||||
* @param depictedItem : Bookmark to add
|
|
||||||
*/
|
|
||||||
private void addBookmarkItem(final DepictedItem depictedItem) {
|
|
||||||
final ContentProviderClient db = clientProvider.get();
|
|
||||||
try {
|
|
||||||
db.insert(BookmarkItemsContentProvider.BASE_URI, toContentValues(depictedItem));
|
|
||||||
} catch (final RemoteException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
} finally {
|
|
||||||
db.release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a bookmark from database
|
|
||||||
* @param depictedItem : Bookmark to delete
|
|
||||||
*/
|
|
||||||
private void deleteBookmarkItem(final DepictedItem depictedItem) {
|
|
||||||
final ContentProviderClient db = clientProvider.get();
|
|
||||||
try {
|
|
||||||
db.delete(BookmarkItemsContentProvider.uriForName(depictedItem.getId()), null, null);
|
|
||||||
} catch (final RemoteException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
} finally {
|
|
||||||
db.release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find a bookmark from database based on its name
|
|
||||||
* @param depictedItemID : Bookmark to find
|
|
||||||
* @return boolean : is bookmark in database ?
|
|
||||||
*/
|
|
||||||
public boolean findBookmarkItem(final String depictedItemID) {
|
|
||||||
if (depictedItemID == null) { //Avoiding NPE's
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
final ContentProviderClient db = clientProvider.get();
|
|
||||||
try (final Cursor cursor = db.query(
|
|
||||||
BookmarkItemsContentProvider.BASE_URI,
|
|
||||||
Table.ALL_FIELDS,
|
|
||||||
Table.COLUMN_ID + "=?",
|
|
||||||
new String[]{depictedItemID},
|
|
||||||
null
|
|
||||||
)) {
|
|
||||||
if (cursor != null && cursor.moveToFirst()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch (final RemoteException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
} finally {
|
|
||||||
db.release();
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recives real data from cursor
|
|
||||||
* @param cursor : Object for storing database data
|
|
||||||
* @return DepictedItem
|
|
||||||
*/
|
|
||||||
@SuppressLint("Range")
|
|
||||||
DepictedItem fromCursor(final Cursor cursor) {
|
|
||||||
final String fileName = cursor.getString(cursor.getColumnIndex(Table.COLUMN_NAME));
|
|
||||||
final String description
|
|
||||||
= cursor.getString(cursor.getColumnIndex(Table.COLUMN_DESCRIPTION));
|
|
||||||
final String imageUrl = cursor.getString(cursor.getColumnIndex(Table.COLUMN_IMAGE));
|
|
||||||
final String instanceListString
|
|
||||||
= cursor.getString(cursor.getColumnIndex(Table.COLUMN_INSTANCE_LIST));
|
|
||||||
final List<String> instanceList = StringToArray(instanceListString);
|
|
||||||
final String categoryNameListString = cursor.getString(cursor
|
|
||||||
.getColumnIndex(Table.COLUMN_CATEGORIES_NAME_LIST));
|
|
||||||
final List<String> categoryNameList = StringToArray(categoryNameListString);
|
|
||||||
final String categoryDescriptionListString = cursor.getString(cursor
|
|
||||||
.getColumnIndex(Table.COLUMN_CATEGORIES_DESCRIPTION_LIST));
|
|
||||||
final List<String> categoryDescriptionList = StringToArray(categoryDescriptionListString);
|
|
||||||
final String categoryThumbnailListString = cursor.getString(cursor
|
|
||||||
.getColumnIndex(Table.COLUMN_CATEGORIES_THUMBNAIL_LIST));
|
|
||||||
final List<String> categoryThumbnailList = StringToArray(categoryThumbnailListString);
|
|
||||||
final List<CategoryItem> categoryList = convertToCategoryItems(categoryNameList,
|
|
||||||
categoryDescriptionList, categoryThumbnailList);
|
|
||||||
final boolean isSelected
|
|
||||||
= Boolean.parseBoolean(cursor.getString(cursor
|
|
||||||
.getColumnIndex(Table.COLUMN_IS_SELECTED)));
|
|
||||||
final String id = cursor.getString(cursor.getColumnIndex(Table.COLUMN_ID));
|
|
||||||
|
|
||||||
return new DepictedItem(
|
|
||||||
fileName,
|
|
||||||
description,
|
|
||||||
imageUrl,
|
|
||||||
instanceList,
|
|
||||||
categoryList,
|
|
||||||
isSelected,
|
|
||||||
id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<CategoryItem> convertToCategoryItems(List<String> categoryNameList,
|
|
||||||
List<String> categoryDescriptionList, List<String> categoryThumbnailList) {
|
|
||||||
List<CategoryItem> categoryItems = new ArrayList<>();
|
|
||||||
for(int i=0; i<categoryNameList.size(); i++){
|
|
||||||
categoryItems.add(new CategoryItem(categoryNameList.get(i),
|
|
||||||
categoryDescriptionList.get(i),
|
|
||||||
categoryThumbnailList.get(i), false));
|
|
||||||
}
|
|
||||||
return categoryItems;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts string to List
|
|
||||||
* @param listString comma separated single string from of list items
|
|
||||||
* @return List of string
|
|
||||||
*/
|
|
||||||
private List<String> StringToArray(final String listString) {
|
|
||||||
final String[] elements = listString.split(",");
|
|
||||||
return Arrays.asList(elements);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts string to List
|
|
||||||
* @param list list of items
|
|
||||||
* @return string comma separated single string of items
|
|
||||||
*/
|
|
||||||
private String ArrayToString(final List<String> list) {
|
|
||||||
if (list != null) {
|
|
||||||
return StringUtils.join(list, ',');
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Takes data from DepictedItem and create a content value object
|
|
||||||
* @param depictedItem depicted item
|
|
||||||
* @return ContentValues
|
|
||||||
*/
|
|
||||||
private ContentValues toContentValues(final DepictedItem depictedItem) {
|
|
||||||
|
|
||||||
final List<String> namesOfCommonsCategories = new ArrayList<>();
|
|
||||||
for (final CategoryItem category :
|
|
||||||
depictedItem.getCommonsCategories()) {
|
|
||||||
namesOfCommonsCategories.add(category.getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
final List<String> descriptionsOfCommonsCategories = new ArrayList<>();
|
|
||||||
for (final CategoryItem category :
|
|
||||||
depictedItem.getCommonsCategories()) {
|
|
||||||
descriptionsOfCommonsCategories.add(category.getDescription());
|
|
||||||
}
|
|
||||||
|
|
||||||
final List<String> thumbnailsOfCommonsCategories = new ArrayList<>();
|
|
||||||
for (final CategoryItem category :
|
|
||||||
depictedItem.getCommonsCategories()) {
|
|
||||||
thumbnailsOfCommonsCategories.add(category.getThumbnail());
|
|
||||||
}
|
|
||||||
|
|
||||||
final ContentValues cv = new ContentValues();
|
|
||||||
cv.put(Table.COLUMN_NAME, depictedItem.getName());
|
|
||||||
cv.put(Table.COLUMN_DESCRIPTION, depictedItem.getDescription());
|
|
||||||
cv.put(Table.COLUMN_IMAGE, depictedItem.getImageUrl());
|
|
||||||
cv.put(Table.COLUMN_INSTANCE_LIST, ArrayToString(depictedItem.getInstanceOfs()));
|
|
||||||
cv.put(Table.COLUMN_CATEGORIES_NAME_LIST, ArrayToString(namesOfCommonsCategories));
|
|
||||||
cv.put(Table.COLUMN_CATEGORIES_DESCRIPTION_LIST,
|
|
||||||
ArrayToString(descriptionsOfCommonsCategories));
|
|
||||||
cv.put(Table.COLUMN_CATEGORIES_THUMBNAIL_LIST,
|
|
||||||
ArrayToString(thumbnailsOfCommonsCategories));
|
|
||||||
cv.put(Table.COLUMN_IS_SELECTED, depictedItem.isSelected());
|
|
||||||
cv.put(Table.COLUMN_ID, depictedItem.getId());
|
|
||||||
return cv;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Table of bookmarksItems data
|
|
||||||
*/
|
|
||||||
public static final class Table {
|
|
||||||
public static final String TABLE_NAME = "bookmarksItems";
|
|
||||||
public static final String COLUMN_NAME = "item_name";
|
|
||||||
public static final String COLUMN_DESCRIPTION = "item_description";
|
|
||||||
public static final String COLUMN_IMAGE = "item_image_url";
|
|
||||||
public static final String COLUMN_INSTANCE_LIST = "item_instance_of";
|
|
||||||
public static final String COLUMN_CATEGORIES_NAME_LIST = "item_name_categories";
|
|
||||||
public static final String COLUMN_CATEGORIES_DESCRIPTION_LIST = "item_description_categories";
|
|
||||||
public static final String COLUMN_CATEGORIES_THUMBNAIL_LIST = "item_thumbnail_categories";
|
|
||||||
public static final String COLUMN_IS_SELECTED = "item_is_selected";
|
|
||||||
public static final String COLUMN_ID = "item_id";
|
|
||||||
|
|
||||||
public static final String[] ALL_FIELDS = {
|
|
||||||
COLUMN_NAME,
|
|
||||||
COLUMN_DESCRIPTION,
|
|
||||||
COLUMN_IMAGE,
|
|
||||||
COLUMN_INSTANCE_LIST,
|
|
||||||
COLUMN_CATEGORIES_NAME_LIST,
|
|
||||||
COLUMN_CATEGORIES_DESCRIPTION_LIST,
|
|
||||||
COLUMN_CATEGORIES_THUMBNAIL_LIST,
|
|
||||||
COLUMN_IS_SELECTED,
|
|
||||||
COLUMN_ID
|
|
||||||
};
|
|
||||||
|
|
||||||
static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME;
|
|
||||||
static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
|
|
||||||
+ COLUMN_NAME + " STRING,"
|
|
||||||
+ COLUMN_DESCRIPTION + " STRING,"
|
|
||||||
+ COLUMN_IMAGE + " STRING,"
|
|
||||||
+ COLUMN_INSTANCE_LIST + " STRING,"
|
|
||||||
+ COLUMN_CATEGORIES_NAME_LIST + " STRING,"
|
|
||||||
+ COLUMN_CATEGORIES_DESCRIPTION_LIST + " STRING,"
|
|
||||||
+ COLUMN_CATEGORIES_THUMBNAIL_LIST + " STRING,"
|
|
||||||
+ COLUMN_IS_SELECTED + " STRING,"
|
|
||||||
+ COLUMN_ID + " STRING PRIMARY KEY"
|
|
||||||
+ ");";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates table
|
|
||||||
* @param db SQLiteDatabase
|
|
||||||
*/
|
|
||||||
public static void onCreate(final SQLiteDatabase db) {
|
|
||||||
db.execSQL(CREATE_TABLE_STATEMENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes database
|
|
||||||
* @param db SQLiteDatabase
|
|
||||||
*/
|
|
||||||
public static void onDelete(final SQLiteDatabase db) {
|
|
||||||
db.execSQL(DROP_TABLE_STATEMENT);
|
|
||||||
onCreate(db);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates database
|
|
||||||
* @param db SQLiteDatabase
|
|
||||||
* @param from starting
|
|
||||||
* @param to end
|
|
||||||
*/
|
|
||||||
public static void onUpdate(final SQLiteDatabase db, int from, final int to) {
|
|
||||||
if (from == to) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (from < 18) {
|
|
||||||
// doesn't exist yet
|
|
||||||
from++;
|
|
||||||
onUpdate(db, from, to);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (from == 18) {
|
|
||||||
// table added in version 19
|
|
||||||
onCreate(db);
|
|
||||||
from++;
|
|
||||||
onUpdate(db, from, to);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,203 @@
|
||||||
|
package fr.free.nrw.commons.bookmarks.items
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.ContentProviderClient
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.database.Cursor
|
||||||
|
import android.os.RemoteException
|
||||||
|
import androidx.core.content.contentValuesOf
|
||||||
|
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsContentProvider.Companion.BASE_URI
|
||||||
|
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsContentProvider.Companion.uriForName
|
||||||
|
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_CATEGORIES_DESCRIPTION_LIST
|
||||||
|
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_CATEGORIES_NAME_LIST
|
||||||
|
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_CATEGORIES_THUMBNAIL_LIST
|
||||||
|
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_DESCRIPTION
|
||||||
|
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_ID
|
||||||
|
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_IMAGE
|
||||||
|
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_INSTANCE_LIST
|
||||||
|
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_IS_SELECTED
|
||||||
|
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_NAME
|
||||||
|
import fr.free.nrw.commons.category.CategoryItem
|
||||||
|
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
|
||||||
|
import fr.free.nrw.commons.utils.arrayToString
|
||||||
|
import fr.free.nrw.commons.utils.getString
|
||||||
|
import fr.free.nrw.commons.utils.getStringArray
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Named
|
||||||
|
import javax.inject.Provider
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles database operations for bookmarked items
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
class BookmarkItemsDao @Inject constructor(
|
||||||
|
@param:Named("bookmarksItem") private val clientProvider: Provider<ContentProviderClient>
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Find all persisted items bookmarks on database
|
||||||
|
* @return list of bookmarks
|
||||||
|
*/
|
||||||
|
fun getAllBookmarksItems(): List<DepictedItem> {
|
||||||
|
val items: MutableList<DepictedItem> = mutableListOf()
|
||||||
|
val db = clientProvider.get()
|
||||||
|
try {
|
||||||
|
db.query(
|
||||||
|
BASE_URI,
|
||||||
|
BookmarkItemsTable.ALL_FIELDS,
|
||||||
|
null,
|
||||||
|
arrayOf(),
|
||||||
|
null
|
||||||
|
).use { cursor ->
|
||||||
|
while (cursor != null && cursor.moveToNext()) {
|
||||||
|
items.add(fromCursor(cursor))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: RemoteException) {
|
||||||
|
throw RuntimeException(e)
|
||||||
|
} finally {
|
||||||
|
db.release()
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Look for a bookmark in database and in order to insert or delete it
|
||||||
|
* @param depictedItem : Bookmark object
|
||||||
|
* @return boolean : is bookmark now favorite ?
|
||||||
|
*/
|
||||||
|
fun updateBookmarkItem(depictedItem: DepictedItem): Boolean {
|
||||||
|
val bookmarkExists = findBookmarkItem(depictedItem.id)
|
||||||
|
if (bookmarkExists) {
|
||||||
|
deleteBookmarkItem(depictedItem)
|
||||||
|
} else {
|
||||||
|
addBookmarkItem(depictedItem)
|
||||||
|
}
|
||||||
|
return !bookmarkExists
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a Bookmark to database
|
||||||
|
* @param depictedItem : Bookmark to add
|
||||||
|
*/
|
||||||
|
private fun addBookmarkItem(depictedItem: DepictedItem) {
|
||||||
|
val db = clientProvider.get()
|
||||||
|
try {
|
||||||
|
db.insert(BASE_URI, toContentValues(depictedItem))
|
||||||
|
} catch (e: RemoteException) {
|
||||||
|
throw RuntimeException(e)
|
||||||
|
} finally {
|
||||||
|
db.release()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a bookmark from database
|
||||||
|
* @param depictedItem : Bookmark to delete
|
||||||
|
*/
|
||||||
|
private fun deleteBookmarkItem(depictedItem: DepictedItem) {
|
||||||
|
val db = clientProvider.get()
|
||||||
|
try {
|
||||||
|
db.delete(uriForName(depictedItem.id), null, null)
|
||||||
|
} catch (e: RemoteException) {
|
||||||
|
throw RuntimeException(e)
|
||||||
|
} finally {
|
||||||
|
db.release()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a bookmark from database based on its name
|
||||||
|
* @param depictedItemID : Bookmark to find
|
||||||
|
* @return boolean : is bookmark in database ?
|
||||||
|
*/
|
||||||
|
fun findBookmarkItem(depictedItemID: String?): Boolean {
|
||||||
|
if (depictedItemID == null) { //Avoiding NPE's
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
val db = clientProvider.get()
|
||||||
|
try {
|
||||||
|
db.query(
|
||||||
|
BASE_URI,
|
||||||
|
BookmarkItemsTable.ALL_FIELDS,
|
||||||
|
COLUMN_ID + "=?",
|
||||||
|
arrayOf(depictedItemID),
|
||||||
|
null
|
||||||
|
).use { cursor ->
|
||||||
|
if (cursor != null && cursor.moveToFirst()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: RemoteException) {
|
||||||
|
throw RuntimeException(e)
|
||||||
|
} finally {
|
||||||
|
db.release()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recives real data from cursor
|
||||||
|
* @param cursor : Object for storing database data
|
||||||
|
* @return DepictedItem
|
||||||
|
*/
|
||||||
|
@SuppressLint("Range")
|
||||||
|
fun fromCursor(cursor: Cursor) = with(cursor) {
|
||||||
|
var name = getString(COLUMN_NAME)
|
||||||
|
if (name == null) {
|
||||||
|
name = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var id = getString(COLUMN_ID)
|
||||||
|
if (id == null) {
|
||||||
|
id = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
DepictedItem(
|
||||||
|
name,
|
||||||
|
getString(COLUMN_DESCRIPTION),
|
||||||
|
getString(COLUMN_IMAGE),
|
||||||
|
getStringArray(COLUMN_INSTANCE_LIST),
|
||||||
|
convertToCategoryItems(
|
||||||
|
getStringArray(COLUMN_CATEGORIES_NAME_LIST),
|
||||||
|
getStringArray(COLUMN_CATEGORIES_DESCRIPTION_LIST),
|
||||||
|
getStringArray(COLUMN_CATEGORIES_THUMBNAIL_LIST)
|
||||||
|
),
|
||||||
|
getString(COLUMN_IS_SELECTED).toBoolean(),
|
||||||
|
id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun convertToCategoryItems(
|
||||||
|
categoryNameList: List<String>,
|
||||||
|
categoryDescriptionList: List<String>,
|
||||||
|
categoryThumbnailList: List<String>
|
||||||
|
): List<CategoryItem> = categoryNameList.mapIndexed { index, name ->
|
||||||
|
CategoryItem(
|
||||||
|
name = name,
|
||||||
|
description = categoryDescriptionList.getOrNull(index),
|
||||||
|
thumbnail = categoryThumbnailList.getOrNull(index),
|
||||||
|
isSelected = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes data from DepictedItem and create a content value object
|
||||||
|
* @param depictedItem depicted item
|
||||||
|
* @return ContentValues
|
||||||
|
*/
|
||||||
|
private fun toContentValues(depictedItem: DepictedItem): ContentValues {
|
||||||
|
return contentValuesOf(
|
||||||
|
COLUMN_NAME to depictedItem.name,
|
||||||
|
COLUMN_DESCRIPTION to depictedItem.description,
|
||||||
|
COLUMN_IMAGE to depictedItem.imageUrl,
|
||||||
|
COLUMN_INSTANCE_LIST to arrayToString(depictedItem.instanceOfs),
|
||||||
|
COLUMN_CATEGORIES_NAME_LIST to arrayToString(depictedItem.commonsCategories.map { it.name }),
|
||||||
|
COLUMN_CATEGORIES_DESCRIPTION_LIST to arrayToString(depictedItem.commonsCategories.map { it.description }),
|
||||||
|
COLUMN_CATEGORIES_THUMBNAIL_LIST to arrayToString(depictedItem.commonsCategories.map { it.thumbnail }),
|
||||||
|
COLUMN_IS_SELECTED to depictedItem.isSelected,
|
||||||
|
COLUMN_ID to depictedItem.id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
package fr.free.nrw.commons.bookmarks.items;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.ProgressBar;
|
|
||||||
import android.widget.RelativeLayout;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import dagger.android.support.DaggerFragment;
|
|
||||||
import fr.free.nrw.commons.R;
|
|
||||||
import fr.free.nrw.commons.databinding.FragmentBookmarksItemsBinding;
|
|
||||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
|
|
||||||
import java.util.List;
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tab fragment to show list of bookmarked Wikidata Items
|
|
||||||
*/
|
|
||||||
public class BookmarkItemsFragment extends DaggerFragment {
|
|
||||||
|
|
||||||
private FragmentBookmarksItemsBinding binding;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
BookmarkItemsController controller;
|
|
||||||
|
|
||||||
public static BookmarkItemsFragment newInstance() {
|
|
||||||
return new BookmarkItemsFragment();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View onCreateView(
|
|
||||||
@NonNull final LayoutInflater inflater,
|
|
||||||
final ViewGroup container,
|
|
||||||
final Bundle savedInstanceState
|
|
||||||
) {
|
|
||||||
binding = FragmentBookmarksItemsBinding.inflate(inflater, container, false);
|
|
||||||
return binding.getRoot();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onViewCreated(final @NotNull View view, @Nullable final Bundle savedInstanceState) {
|
|
||||||
super.onViewCreated(view, savedInstanceState);
|
|
||||||
initList(requireContext());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
initList(requireContext());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get list of DepictedItem and sets to the adapter
|
|
||||||
* @param context context
|
|
||||||
*/
|
|
||||||
private void initList(final Context context) {
|
|
||||||
final List<DepictedItem> depictItems = controller.loadFavoritesItems();
|
|
||||||
final BookmarkItemsAdapter adapter = new BookmarkItemsAdapter(depictItems, context);
|
|
||||||
binding.listView.setAdapter(adapter);
|
|
||||||
binding.loadingImagesProgressBar.setVisibility(View.GONE);
|
|
||||||
if (depictItems.isEmpty()) {
|
|
||||||
binding.statusMessage.setText(R.string.bookmark_empty);
|
|
||||||
binding.statusMessage.setVisibility(View.VISIBLE);
|
|
||||||
} else {
|
|
||||||
binding.statusMessage.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
binding = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
package fr.free.nrw.commons.bookmarks.items
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import dagger.android.support.DaggerFragment
|
||||||
|
import fr.free.nrw.commons.R
|
||||||
|
import fr.free.nrw.commons.databinding.FragmentBookmarksItemsBinding
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tab fragment to show list of bookmarked Wikidata Items
|
||||||
|
*/
|
||||||
|
class BookmarkItemsFragment : DaggerFragment() {
|
||||||
|
private var binding: FragmentBookmarksItemsBinding? = null
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
@Inject
|
||||||
|
var controller: BookmarkItemsController? = null
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
binding = FragmentBookmarksItemsBinding.inflate(inflater, container, false)
|
||||||
|
return binding!!.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
initList(requireContext())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
initList(requireContext())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of DepictedItem and sets to the adapter
|
||||||
|
* @param context context
|
||||||
|
*/
|
||||||
|
private fun initList(context: Context) {
|
||||||
|
val depictItems = controller!!.loadFavoritesItems()
|
||||||
|
binding!!.listView.adapter = BookmarkItemsAdapter(depictItems, context)
|
||||||
|
binding!!.loadingImagesProgressBar.visibility = View.GONE
|
||||||
|
if (depictItems.isEmpty()) {
|
||||||
|
binding!!.statusMessage.setText(R.string.bookmark_empty)
|
||||||
|
binding!!.statusMessage.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
binding!!.statusMessage.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
binding = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
package fr.free.nrw.commons.bookmarks.items
|
||||||
|
|
||||||
|
import android.database.sqlite.SQLiteDatabase
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Table of bookmarksItems data
|
||||||
|
*/
|
||||||
|
object BookmarkItemsTable {
|
||||||
|
const val TABLE_NAME = "bookmarksItems"
|
||||||
|
const val COLUMN_NAME = "item_name"
|
||||||
|
const val COLUMN_DESCRIPTION = "item_description"
|
||||||
|
const val COLUMN_IMAGE = "item_image_url"
|
||||||
|
const val COLUMN_INSTANCE_LIST = "item_instance_of"
|
||||||
|
const val COLUMN_CATEGORIES_NAME_LIST = "item_name_categories"
|
||||||
|
const val COLUMN_CATEGORIES_DESCRIPTION_LIST = "item_description_categories"
|
||||||
|
const val COLUMN_CATEGORIES_THUMBNAIL_LIST = "item_thumbnail_categories"
|
||||||
|
const val COLUMN_IS_SELECTED = "item_is_selected"
|
||||||
|
const val COLUMN_ID = "item_id"
|
||||||
|
|
||||||
|
val ALL_FIELDS = arrayOf(
|
||||||
|
COLUMN_NAME,
|
||||||
|
COLUMN_DESCRIPTION,
|
||||||
|
COLUMN_IMAGE,
|
||||||
|
COLUMN_INSTANCE_LIST,
|
||||||
|
COLUMN_CATEGORIES_NAME_LIST,
|
||||||
|
COLUMN_CATEGORIES_DESCRIPTION_LIST,
|
||||||
|
COLUMN_CATEGORIES_THUMBNAIL_LIST,
|
||||||
|
COLUMN_IS_SELECTED,
|
||||||
|
COLUMN_ID
|
||||||
|
)
|
||||||
|
|
||||||
|
const val DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS $TABLE_NAME"
|
||||||
|
|
||||||
|
val CREATE_TABLE_STATEMENT =
|
||||||
|
"""CREATE TABLE $TABLE_NAME (
|
||||||
|
$COLUMN_NAME STRING,
|
||||||
|
$COLUMN_DESCRIPTION STRING,
|
||||||
|
$COLUMN_IMAGE STRING,
|
||||||
|
$COLUMN_INSTANCE_LIST STRING,
|
||||||
|
$COLUMN_CATEGORIES_NAME_LIST STRING,
|
||||||
|
$COLUMN_CATEGORIES_DESCRIPTION_LIST STRING,
|
||||||
|
$COLUMN_CATEGORIES_THUMBNAIL_LIST STRING,
|
||||||
|
$COLUMN_IS_SELECTED STRING,
|
||||||
|
$COLUMN_ID STRING PRIMARY KEY
|
||||||
|
);""".trimIndent()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates table
|
||||||
|
*
|
||||||
|
* @param db SQLiteDatabase
|
||||||
|
*/
|
||||||
|
fun onCreate(db: SQLiteDatabase) {
|
||||||
|
db.execSQL(CREATE_TABLE_STATEMENT)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes database
|
||||||
|
*
|
||||||
|
* @param db SQLiteDatabase
|
||||||
|
*/
|
||||||
|
fun onDelete(db: SQLiteDatabase) {
|
||||||
|
db.execSQL(DROP_TABLE_STATEMENT)
|
||||||
|
onCreate(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates database
|
||||||
|
*
|
||||||
|
* @param db SQLiteDatabase
|
||||||
|
* @param from starting
|
||||||
|
* @param to end
|
||||||
|
*/
|
||||||
|
fun onUpdate(db: SQLiteDatabase, from: Int, to: Int) {
|
||||||
|
if (from == to) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (from < 18) {
|
||||||
|
// doesn't exist yet
|
||||||
|
onUpdate(db, from + 1, to)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (from == 18) {
|
||||||
|
// table added in version 19
|
||||||
|
onCreate(db)
|
||||||
|
onUpdate(db, from + 1, to)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package fr.free.nrw.commons.bookmarks.locations
|
package fr.free.nrw.commons.bookmarks.locations
|
||||||
|
|
||||||
import android.Manifest.permission
|
import android.Manifest.permission
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
|
@ -9,15 +8,12 @@ import android.view.ViewGroup
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions
|
import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions
|
||||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
|
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import dagger.android.support.DaggerFragment
|
import dagger.android.support.DaggerFragment
|
||||||
import fr.free.nrw.commons.R
|
import fr.free.nrw.commons.R
|
||||||
import fr.free.nrw.commons.contributions.ContributionController
|
import fr.free.nrw.commons.contributions.ContributionController
|
||||||
import fr.free.nrw.commons.databinding.FragmentBookmarksLocationsBinding
|
import fr.free.nrw.commons.databinding.FragmentBookmarksLocationsBinding
|
||||||
import fr.free.nrw.commons.filepicker.FilePicker
|
|
||||||
import fr.free.nrw.commons.nearby.Place
|
import fr.free.nrw.commons.nearby.Place
|
||||||
import fr.free.nrw.commons.nearby.fragments.CommonPlaceClickActions
|
import fr.free.nrw.commons.nearby.fragments.CommonPlaceClickActions
|
||||||
import fr.free.nrw.commons.nearby.fragments.PlaceAdapter
|
import fr.free.nrw.commons.nearby.fragments.PlaceAdapter
|
||||||
|
|
@ -41,33 +37,27 @@ class BookmarkLocationsFragment : DaggerFragment() {
|
||||||
private val cameraPickLauncherForResult =
|
private val cameraPickLauncherForResult =
|
||||||
registerForActivityResult(StartActivityForResult()) { result ->
|
registerForActivityResult(StartActivityForResult()) { result ->
|
||||||
contributionController.handleActivityResultWithCallback(
|
contributionController.handleActivityResultWithCallback(
|
||||||
requireActivity(),
|
requireActivity()
|
||||||
object: FilePicker.HandleActivityResult {
|
) { callbacks ->
|
||||||
override fun onHandleActivityResult(callbacks: FilePicker.Callbacks) {
|
contributionController.onPictureReturnedFromCamera(
|
||||||
contributionController.onPictureReturnedFromCamera(
|
result,
|
||||||
result,
|
requireActivity(),
|
||||||
requireActivity(),
|
callbacks
|
||||||
callbacks
|
)
|
||||||
)
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val galleryPickLauncherForResult =
|
private val galleryPickLauncherForResult =
|
||||||
registerForActivityResult(StartActivityForResult()) { result ->
|
registerForActivityResult(StartActivityForResult()) { result ->
|
||||||
contributionController.handleActivityResultWithCallback(
|
contributionController.handleActivityResultWithCallback(
|
||||||
requireActivity(),
|
requireActivity()
|
||||||
object: FilePicker.HandleActivityResult {
|
) { callbacks ->
|
||||||
override fun onHandleActivityResult(callbacks: FilePicker.Callbacks) {
|
contributionController.onPictureReturnedFromGallery(
|
||||||
contributionController.onPictureReturnedFromGallery(
|
result,
|
||||||
result,
|
requireActivity(),
|
||||||
requireActivity(),
|
callbacks
|
||||||
callbacks
|
)
|
||||||
)
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ class Bookmark(
|
||||||
/**
|
/**
|
||||||
* Gets or Sets the content URI - marking this bookmark as already saved in the database
|
* Gets or Sets the content URI - marking this bookmark as already saved in the database
|
||||||
* @return content URI
|
* @return content URI
|
||||||
* @param contentUri the content URI
|
* contentUri the content URI
|
||||||
*/
|
*/
|
||||||
var contentUri: Uri?,
|
var contentUri: Uri?,
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -1,120 +0,0 @@
|
||||||
package fr.free.nrw.commons.bookmarks.pictures;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
import android.database.sqlite.SQLiteQueryBuilder;
|
|
||||||
// We can get uri using java.Net.Uri, but andoid implimentation is faster (but it's forgiving with handling exceptions though)
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
|
|
||||||
import fr.free.nrw.commons.BuildConfig;
|
|
||||||
import fr.free.nrw.commons.data.DBOpenHelper;
|
|
||||||
import fr.free.nrw.commons.di.CommonsDaggerContentProvider;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
import static fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao.Table.COLUMN_MEDIA_NAME;
|
|
||||||
import static fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao.Table.TABLE_NAME;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles private storage for Bookmark pictures
|
|
||||||
*/
|
|
||||||
public class BookmarkPicturesContentProvider extends CommonsDaggerContentProvider {
|
|
||||||
|
|
||||||
private static final String BASE_PATH = "bookmarks";
|
|
||||||
public static final Uri BASE_URI = Uri.parse("content://" + BuildConfig.BOOKMARK_AUTHORITY + "/" + BASE_PATH);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Append bookmark pictures name to the base uri
|
|
||||||
*/
|
|
||||||
public static Uri uriForName(String name) {
|
|
||||||
return Uri.parse(BASE_URI.toString() + "/" + name);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
DBOpenHelper dbOpenHelper;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getType(@NonNull Uri uri) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Queries the SQLite database for the bookmark pictures
|
|
||||||
* @param uri : contains the uri for bookmark pictures
|
|
||||||
* @param projection
|
|
||||||
* @param selection : handles Where
|
|
||||||
* @param selectionArgs : the condition of Where clause
|
|
||||||
* @param sortOrder : ascending or descending
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("ConstantConditions")
|
|
||||||
@Override
|
|
||||||
public Cursor query(@NonNull Uri uri, String[] projection, String selection,
|
|
||||||
String[] selectionArgs, String sortOrder) {
|
|
||||||
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
|
|
||||||
queryBuilder.setTables(TABLE_NAME);
|
|
||||||
|
|
||||||
SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
|
|
||||||
Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder);
|
|
||||||
cursor.setNotificationUri(getContext().getContentResolver(), uri);
|
|
||||||
|
|
||||||
return cursor;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the update query of local SQLite Database
|
|
||||||
* @param uri : contains the uri for bookmark pictures
|
|
||||||
* @param contentValues : new values to be entered to db
|
|
||||||
* @param selection : handles Where
|
|
||||||
* @param selectionArgs : the condition of Where clause
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("ConstantConditions")
|
|
||||||
@Override
|
|
||||||
public int update(@NonNull Uri uri, ContentValues contentValues, String selection,
|
|
||||||
String[] selectionArgs) {
|
|
||||||
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
|
||||||
int rowsUpdated;
|
|
||||||
if (TextUtils.isEmpty(selection)) {
|
|
||||||
int id = Integer.valueOf(uri.getLastPathSegment());
|
|
||||||
rowsUpdated = sqlDB.update(TABLE_NAME,
|
|
||||||
contentValues,
|
|
||||||
COLUMN_MEDIA_NAME + " = ?",
|
|
||||||
new String[]{String.valueOf(id)});
|
|
||||||
} else {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"Parameter `selection` should be empty when updating an ID");
|
|
||||||
}
|
|
||||||
getContext().getContentResolver().notifyChange(uri, null);
|
|
||||||
return rowsUpdated;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the insertion of new bookmark pictures record to local SQLite Database
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("ConstantConditions")
|
|
||||||
@Override
|
|
||||||
public Uri insert(@NonNull Uri uri, ContentValues contentValues) {
|
|
||||||
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
|
||||||
long id = sqlDB.insert(BookmarkPicturesDao.Table.TABLE_NAME, null, contentValues);
|
|
||||||
getContext().getContentResolver().notifyChange(uri, null);
|
|
||||||
return Uri.parse(BASE_URI + "/" + id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("ConstantConditions")
|
|
||||||
@Override
|
|
||||||
public int delete(@NonNull Uri uri, String s, String[] strings) {
|
|
||||||
int rows;
|
|
||||||
SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
|
|
||||||
Timber.d("Deleting bookmark name %s", uri.getLastPathSegment());
|
|
||||||
rows = db.delete(TABLE_NAME,
|
|
||||||
"media_name = ?",
|
|
||||||
new String[]{uri.getLastPathSegment()}
|
|
||||||
);
|
|
||||||
getContext().getContentResolver().notifyChange(uri, null);
|
|
||||||
return rows;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
package fr.free.nrw.commons.bookmarks.pictures
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.database.Cursor
|
||||||
|
import android.database.sqlite.SQLiteQueryBuilder
|
||||||
|
import android.net.Uri
|
||||||
|
import fr.free.nrw.commons.BuildConfig
|
||||||
|
import fr.free.nrw.commons.di.CommonsDaggerContentProvider
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable.COLUMN_MEDIA_NAME
|
||||||
|
import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable.TABLE_NAME
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles private storage for Bookmark pictures
|
||||||
|
*/
|
||||||
|
class BookmarkPicturesContentProvider : CommonsDaggerContentProvider() {
|
||||||
|
override fun getType(uri: Uri): String? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queries the SQLite database for the bookmark pictures
|
||||||
|
* @param uri : contains the uri for bookmark pictures
|
||||||
|
* @param projection
|
||||||
|
* @param selection : handles Where
|
||||||
|
* @param selectionArgs : the condition of Where clause
|
||||||
|
* @param sortOrder : ascending or descending
|
||||||
|
*/
|
||||||
|
override fun query(
|
||||||
|
uri: Uri, projection: Array<String>?, selection: String?,
|
||||||
|
selectionArgs: Array<String>?, sortOrder: String?
|
||||||
|
): Cursor {
|
||||||
|
val queryBuilder = SQLiteQueryBuilder().apply {
|
||||||
|
tables = TABLE_NAME
|
||||||
|
}
|
||||||
|
|
||||||
|
val cursor = queryBuilder.query(
|
||||||
|
requireDb(), projection, selection,
|
||||||
|
selectionArgs, null, null, sortOrder
|
||||||
|
)
|
||||||
|
cursor.setNotificationUri(context?.contentResolver, uri)
|
||||||
|
|
||||||
|
return cursor
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the update query of local SQLite Database
|
||||||
|
* @param uri : contains the uri for bookmark pictures
|
||||||
|
* @param contentValues : new values to be entered to db
|
||||||
|
* @param selection : handles Where
|
||||||
|
* @param selectionArgs : the condition of Where clause
|
||||||
|
*/
|
||||||
|
override fun update(
|
||||||
|
uri: Uri, contentValues: ContentValues?, selection: String?,
|
||||||
|
selectionArgs: Array<String>?
|
||||||
|
): Int {
|
||||||
|
val rowsUpdated: Int
|
||||||
|
if (selection.isNullOrEmpty()) {
|
||||||
|
val id = uri.lastPathSegment!!.toInt()
|
||||||
|
rowsUpdated = requireDb().update(
|
||||||
|
TABLE_NAME,
|
||||||
|
contentValues,
|
||||||
|
"$COLUMN_MEDIA_NAME = ?",
|
||||||
|
arrayOf(id.toString())
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
throw IllegalArgumentException(
|
||||||
|
"Parameter `selection` should be empty when updating an ID"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
context?.contentResolver?.notifyChange(uri, null)
|
||||||
|
return rowsUpdated
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the insertion of new bookmark pictures record to local SQLite Database
|
||||||
|
*/
|
||||||
|
override fun insert(uri: Uri, contentValues: ContentValues?): Uri {
|
||||||
|
val id = requireDb().insert(TABLE_NAME, null, contentValues)
|
||||||
|
context?.contentResolver?.notifyChange(uri, null)
|
||||||
|
return "$BASE_URI/$id".toUri()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun delete(uri: Uri, s: String?, strings: Array<String>?): Int {
|
||||||
|
val rows: Int = requireDb().delete(
|
||||||
|
TABLE_NAME,
|
||||||
|
"media_name = ?",
|
||||||
|
arrayOf(uri.lastPathSegment)
|
||||||
|
)
|
||||||
|
context?.contentResolver?.notifyChange(uri, null)
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val BASE_PATH = "bookmarks"
|
||||||
|
@JvmField
|
||||||
|
val BASE_URI: Uri = "content://${BuildConfig.BOOKMARK_AUTHORITY}/$BASE_PATH".toUri()
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun uriForName(name: String): Uri = "$BASE_URI/$name".toUri()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
package fr.free.nrw.commons.bookmarks.pictures;
|
|
||||||
|
|
||||||
import fr.free.nrw.commons.Media;
|
|
||||||
import fr.free.nrw.commons.bookmarks.models.Bookmark;
|
|
||||||
import fr.free.nrw.commons.media.MediaClient;
|
|
||||||
import io.reactivex.Observable;
|
|
||||||
import io.reactivex.ObservableSource;
|
|
||||||
import io.reactivex.Single;
|
|
||||||
import io.reactivex.functions.Function;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javax.inject.Singleton;
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
public class BookmarkPicturesController {
|
|
||||||
|
|
||||||
private final MediaClient mediaClient;
|
|
||||||
private final BookmarkPicturesDao bookmarkDao;
|
|
||||||
|
|
||||||
private List<Bookmark> currentBookmarks;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public BookmarkPicturesController(MediaClient mediaClient, BookmarkPicturesDao bookmarkDao) {
|
|
||||||
this.mediaClient = mediaClient;
|
|
||||||
this.bookmarkDao = bookmarkDao;
|
|
||||||
currentBookmarks = new ArrayList<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads the Media objects from the raw data stored in DB and the API.
|
|
||||||
* @return a list of bookmarked Media object
|
|
||||||
*/
|
|
||||||
Single<List<Media>> loadBookmarkedPictures() {
|
|
||||||
List<Bookmark> bookmarks = bookmarkDao.getAllBookmarks();
|
|
||||||
currentBookmarks = bookmarks;
|
|
||||||
return Observable.fromIterable(bookmarks)
|
|
||||||
.flatMap((Function<Bookmark, ObservableSource<Media>>) this::getMediaFromBookmark)
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Observable<Media> getMediaFromBookmark(Bookmark bookmark) {
|
|
||||||
return mediaClient.getMedia(bookmark.getMediaName())
|
|
||||||
.toObservable()
|
|
||||||
.onErrorResumeNext(Observable.empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads the Media objects from the raw data stored in DB and the API.
|
|
||||||
* @return a list of bookmarked Media object
|
|
||||||
*/
|
|
||||||
boolean needRefreshBookmarkedPictures() {
|
|
||||||
List<Bookmark> bookmarks = bookmarkDao.getAllBookmarks();
|
|
||||||
return bookmarks.size() != currentBookmarks.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cancels the requests to the API and the DB
|
|
||||||
*/
|
|
||||||
void stop() {
|
|
||||||
//noop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
package fr.free.nrw.commons.bookmarks.pictures
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.Media
|
||||||
|
import fr.free.nrw.commons.bookmarks.models.Bookmark
|
||||||
|
import fr.free.nrw.commons.media.MediaClient
|
||||||
|
import io.reactivex.Observable
|
||||||
|
import io.reactivex.Single
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class BookmarkPicturesController @Inject constructor(
|
||||||
|
private val mediaClient: MediaClient,
|
||||||
|
private val bookmarkDao: BookmarkPicturesDao
|
||||||
|
) {
|
||||||
|
private var currentBookmarks: List<Bookmark> = listOf()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the Media objects from the raw data stored in DB and the API.
|
||||||
|
* @return a list of bookmarked Media object
|
||||||
|
*/
|
||||||
|
fun loadBookmarkedPictures(): Single<List<Media>> {
|
||||||
|
val bookmarks = bookmarkDao.getAllBookmarks()
|
||||||
|
currentBookmarks = bookmarks
|
||||||
|
return Observable.fromIterable(bookmarks).flatMap {
|
||||||
|
mediaClient.getMedia(it.mediaName)
|
||||||
|
.toObservable()
|
||||||
|
.onErrorResumeNext(Observable.empty())
|
||||||
|
}.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun needRefreshBookmarkedPictures(): Boolean {
|
||||||
|
val bookmarks = bookmarkDao.getAllBookmarks()
|
||||||
|
return bookmarks.size != currentBookmarks.size
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stop() = Unit
|
||||||
|
}
|
||||||
|
|
@ -1,227 +0,0 @@
|
||||||
package fr.free.nrw.commons.bookmarks.pictures;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.content.ContentProviderClient;
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
import android.os.RemoteException;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javax.inject.Named;
|
|
||||||
import javax.inject.Provider;
|
|
||||||
import javax.inject.Singleton;
|
|
||||||
|
|
||||||
import fr.free.nrw.commons.bookmarks.models.Bookmark;
|
|
||||||
|
|
||||||
import static fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider.BASE_URI;
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
public class BookmarkPicturesDao {
|
|
||||||
|
|
||||||
private final Provider<ContentProviderClient> clientProvider;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public BookmarkPicturesDao(@Named("bookmarks") Provider<ContentProviderClient> clientProvider) {
|
|
||||||
this.clientProvider = clientProvider;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find all persisted pictures bookmarks on database
|
|
||||||
*
|
|
||||||
* @return list of bookmarks
|
|
||||||
*/
|
|
||||||
@NonNull
|
|
||||||
public List<Bookmark> getAllBookmarks() {
|
|
||||||
List<Bookmark> items = new ArrayList<>();
|
|
||||||
Cursor cursor = null;
|
|
||||||
ContentProviderClient db = clientProvider.get();
|
|
||||||
try {
|
|
||||||
cursor = db.query(
|
|
||||||
BookmarkPicturesContentProvider.BASE_URI,
|
|
||||||
Table.ALL_FIELDS,
|
|
||||||
null,
|
|
||||||
new String[]{},
|
|
||||||
null);
|
|
||||||
while (cursor != null && cursor.moveToNext()) {
|
|
||||||
items.add(fromCursor(cursor));
|
|
||||||
}
|
|
||||||
} catch (RemoteException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
} finally {
|
|
||||||
if (cursor != null) {
|
|
||||||
cursor.close();
|
|
||||||
}
|
|
||||||
db.release();
|
|
||||||
}
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Look for a bookmark in database and in order to insert or delete it
|
|
||||||
*
|
|
||||||
* @param bookmark : Bookmark object
|
|
||||||
* @return boolean : is bookmark now fav ?
|
|
||||||
*/
|
|
||||||
public boolean updateBookmark(Bookmark bookmark) {
|
|
||||||
boolean bookmarkExists = findBookmark(bookmark);
|
|
||||||
if (bookmarkExists) {
|
|
||||||
deleteBookmark(bookmark);
|
|
||||||
} else {
|
|
||||||
addBookmark(bookmark);
|
|
||||||
}
|
|
||||||
return !bookmarkExists;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a Bookmark to database
|
|
||||||
*
|
|
||||||
* @param bookmark : Bookmark to add
|
|
||||||
*/
|
|
||||||
private void addBookmark(Bookmark bookmark) {
|
|
||||||
ContentProviderClient db = clientProvider.get();
|
|
||||||
try {
|
|
||||||
db.insert(BASE_URI, toContentValues(bookmark));
|
|
||||||
} catch (RemoteException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
} finally {
|
|
||||||
db.release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a bookmark from database
|
|
||||||
*
|
|
||||||
* @param bookmark : Bookmark to delete
|
|
||||||
*/
|
|
||||||
private void deleteBookmark(Bookmark bookmark) {
|
|
||||||
ContentProviderClient db = clientProvider.get();
|
|
||||||
try {
|
|
||||||
if (bookmark.getContentUri() == null) {
|
|
||||||
throw new RuntimeException("tried to delete item with no content URI");
|
|
||||||
} else {
|
|
||||||
db.delete(bookmark.getContentUri(), null, null);
|
|
||||||
}
|
|
||||||
} catch (RemoteException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
} finally {
|
|
||||||
db.release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find a bookmark from database based on its name
|
|
||||||
*
|
|
||||||
* @param bookmark : Bookmark to find
|
|
||||||
* @return boolean : is bookmark in database ?
|
|
||||||
*/
|
|
||||||
public boolean findBookmark(Bookmark bookmark) {
|
|
||||||
if (bookmark == null) {//Avoiding NPE's
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Cursor cursor = null;
|
|
||||||
ContentProviderClient db = clientProvider.get();
|
|
||||||
try {
|
|
||||||
cursor = db.query(
|
|
||||||
BookmarkPicturesContentProvider.BASE_URI,
|
|
||||||
Table.ALL_FIELDS,
|
|
||||||
Table.COLUMN_MEDIA_NAME + "=?",
|
|
||||||
new String[]{bookmark.getMediaName()},
|
|
||||||
null);
|
|
||||||
if (cursor != null && cursor.moveToFirst()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch (RemoteException e) {
|
|
||||||
// This feels lazy, but to hell with checked exceptions. :)
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
} finally {
|
|
||||||
if (cursor != null) {
|
|
||||||
cursor.close();
|
|
||||||
}
|
|
||||||
db.release();
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("Range")
|
|
||||||
@NonNull
|
|
||||||
Bookmark fromCursor(Cursor cursor) {
|
|
||||||
String fileName = cursor.getString(cursor.getColumnIndex(Table.COLUMN_MEDIA_NAME));
|
|
||||||
return new Bookmark(
|
|
||||||
fileName,
|
|
||||||
cursor.getString(cursor.getColumnIndex(Table.COLUMN_CREATOR)),
|
|
||||||
BookmarkPicturesContentProvider.uriForName(fileName)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ContentValues toContentValues(Bookmark bookmark) {
|
|
||||||
ContentValues cv = new ContentValues();
|
|
||||||
cv.put(BookmarkPicturesDao.Table.COLUMN_MEDIA_NAME, bookmark.getMediaName());
|
|
||||||
cv.put(BookmarkPicturesDao.Table.COLUMN_CREATOR, bookmark.getMediaCreator());
|
|
||||||
return cv;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static class Table {
|
|
||||||
public static final String TABLE_NAME = "bookmarks";
|
|
||||||
|
|
||||||
public static final String COLUMN_MEDIA_NAME = "media_name";
|
|
||||||
public static final String COLUMN_CREATOR = "media_creator";
|
|
||||||
|
|
||||||
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
|
|
||||||
public static final String[] ALL_FIELDS = {
|
|
||||||
COLUMN_MEDIA_NAME,
|
|
||||||
COLUMN_CREATOR
|
|
||||||
};
|
|
||||||
|
|
||||||
public static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME;
|
|
||||||
|
|
||||||
public static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
|
|
||||||
+ COLUMN_MEDIA_NAME + " STRING PRIMARY KEY,"
|
|
||||||
+ COLUMN_CREATOR + " STRING"
|
|
||||||
+ ");";
|
|
||||||
|
|
||||||
public static void onCreate(SQLiteDatabase db) {
|
|
||||||
db.execSQL(CREATE_TABLE_STATEMENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void onDelete(SQLiteDatabase db) {
|
|
||||||
db.execSQL(DROP_TABLE_STATEMENT);
|
|
||||||
onCreate(db);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void onUpdate(SQLiteDatabase db, int from, int to) {
|
|
||||||
if (from == to) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (from < 7) {
|
|
||||||
// doesn't exist yet
|
|
||||||
from++;
|
|
||||||
onUpdate(db, from, to);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (from == 7) {
|
|
||||||
// table added in version 8
|
|
||||||
onCreate(db);
|
|
||||||
from++;
|
|
||||||
onUpdate(db, from, to);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (from == 8) {
|
|
||||||
from++;
|
|
||||||
onUpdate(db, from, to);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,144 @@
|
||||||
|
package fr.free.nrw.commons.bookmarks.pictures
|
||||||
|
|
||||||
|
import android.content.ContentProviderClient
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.database.Cursor
|
||||||
|
import android.os.RemoteException
|
||||||
|
import androidx.core.content.contentValuesOf
|
||||||
|
import fr.free.nrw.commons.bookmarks.models.Bookmark
|
||||||
|
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider.Companion.BASE_URI
|
||||||
|
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider.Companion.uriForName
|
||||||
|
import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable.ALL_FIELDS
|
||||||
|
import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable.COLUMN_CREATOR
|
||||||
|
import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable.COLUMN_MEDIA_NAME
|
||||||
|
import fr.free.nrw.commons.utils.getString
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Named
|
||||||
|
import javax.inject.Provider
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class BookmarkPicturesDao @Inject constructor(
|
||||||
|
@param:Named("bookmarks") private val clientProvider: Provider<ContentProviderClient>
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Find all persisted pictures bookmarks on database
|
||||||
|
*
|
||||||
|
* @return list of bookmarks
|
||||||
|
*/
|
||||||
|
fun getAllBookmarks(): List<Bookmark> {
|
||||||
|
val items: MutableList<Bookmark> = mutableListOf()
|
||||||
|
var cursor: Cursor? = null
|
||||||
|
val db = clientProvider.get()
|
||||||
|
try {
|
||||||
|
cursor = db.query(
|
||||||
|
BASE_URI, ALL_FIELDS, null, arrayOf(), null
|
||||||
|
)
|
||||||
|
while (cursor != null && cursor.moveToNext()) {
|
||||||
|
items.add(fromCursor(cursor))
|
||||||
|
}
|
||||||
|
} catch (e: RemoteException) {
|
||||||
|
throw RuntimeException(e)
|
||||||
|
} finally {
|
||||||
|
cursor?.close()
|
||||||
|
db.release()
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Look for a bookmark in database and in order to insert or delete it
|
||||||
|
*
|
||||||
|
* @param bookmark : Bookmark object
|
||||||
|
* @return boolean : is bookmark now fav ?
|
||||||
|
*/
|
||||||
|
fun updateBookmark(bookmark: Bookmark): Boolean {
|
||||||
|
val bookmarkExists = findBookmark(bookmark)
|
||||||
|
if (bookmarkExists) {
|
||||||
|
deleteBookmark(bookmark)
|
||||||
|
} else {
|
||||||
|
addBookmark(bookmark)
|
||||||
|
}
|
||||||
|
return !bookmarkExists
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a Bookmark to database
|
||||||
|
*
|
||||||
|
* @param bookmark : Bookmark to add
|
||||||
|
*/
|
||||||
|
private fun addBookmark(bookmark: Bookmark) {
|
||||||
|
val db = clientProvider.get()
|
||||||
|
try {
|
||||||
|
db.insert(BASE_URI, toContentValues(bookmark))
|
||||||
|
} catch (e: RemoteException) {
|
||||||
|
throw RuntimeException(e)
|
||||||
|
} finally {
|
||||||
|
db.release()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a bookmark from database
|
||||||
|
*
|
||||||
|
* @param bookmark : Bookmark to delete
|
||||||
|
*/
|
||||||
|
private fun deleteBookmark(bookmark: Bookmark) {
|
||||||
|
val db = clientProvider.get()
|
||||||
|
try {
|
||||||
|
if (bookmark.contentUri == null) {
|
||||||
|
throw RuntimeException("tried to delete item with no content URI")
|
||||||
|
} else {
|
||||||
|
db.delete(bookmark.contentUri!!, null, null)
|
||||||
|
}
|
||||||
|
} catch (e: RemoteException) {
|
||||||
|
throw RuntimeException(e)
|
||||||
|
} finally {
|
||||||
|
db.release()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a bookmark from database based on its name
|
||||||
|
*
|
||||||
|
* @param bookmark : Bookmark to find
|
||||||
|
* @return boolean : is bookmark in database ?
|
||||||
|
*/
|
||||||
|
fun findBookmark(bookmark: Bookmark?): Boolean {
|
||||||
|
if (bookmark == null) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var cursor: Cursor? = null
|
||||||
|
val db = clientProvider.get()
|
||||||
|
try {
|
||||||
|
cursor = db.query(
|
||||||
|
BASE_URI, ALL_FIELDS, "$COLUMN_MEDIA_NAME=?", arrayOf(bookmark.mediaName), null
|
||||||
|
)
|
||||||
|
if (cursor != null && cursor.moveToFirst()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} catch (e: RemoteException) {
|
||||||
|
throw RuntimeException(e)
|
||||||
|
} finally {
|
||||||
|
cursor?.close()
|
||||||
|
db.release()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fromCursor(cursor: Cursor): Bookmark {
|
||||||
|
var fileName = cursor.getString(COLUMN_MEDIA_NAME)
|
||||||
|
if (fileName == null) {
|
||||||
|
fileName = ""
|
||||||
|
}
|
||||||
|
return Bookmark(
|
||||||
|
fileName, cursor.getString(COLUMN_CREATOR), uriForName(fileName)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toContentValues(bookmark: Bookmark): ContentValues = contentValuesOf(
|
||||||
|
COLUMN_MEDIA_NAME to bookmark.mediaName,
|
||||||
|
COLUMN_CREATOR to bookmark.mediaCreator
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,218 +0,0 @@
|
||||||
package fr.free.nrw.commons.bookmarks.pictures;
|
|
||||||
|
|
||||||
import static android.view.View.GONE;
|
|
||||||
import static android.view.View.VISIBLE;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.AdapterView;
|
|
||||||
import android.widget.ListAdapter;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import dagger.android.support.DaggerFragment;
|
|
||||||
import fr.free.nrw.commons.Media;
|
|
||||||
import fr.free.nrw.commons.R;
|
|
||||||
import fr.free.nrw.commons.bookmarks.BookmarkListRootFragment;
|
|
||||||
import fr.free.nrw.commons.category.GridViewAdapter;
|
|
||||||
import fr.free.nrw.commons.databinding.FragmentBookmarksPicturesBinding;
|
|
||||||
import fr.free.nrw.commons.utils.NetworkUtils;
|
|
||||||
import fr.free.nrw.commons.utils.ViewUtil;
|
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
|
||||||
import io.reactivex.disposables.CompositeDisposable;
|
|
||||||
import io.reactivex.schedulers.Schedulers;
|
|
||||||
import java.util.List;
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
public class BookmarkPicturesFragment extends DaggerFragment {
|
|
||||||
|
|
||||||
private GridViewAdapter gridAdapter;
|
|
||||||
private CompositeDisposable compositeDisposable = new CompositeDisposable();
|
|
||||||
|
|
||||||
private FragmentBookmarksPicturesBinding binding;
|
|
||||||
@Inject
|
|
||||||
BookmarkPicturesController controller;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create an instance of the fragment with the right bundle parameters
|
|
||||||
* @return an instance of the fragment
|
|
||||||
*/
|
|
||||||
public static BookmarkPicturesFragment newInstance() {
|
|
||||||
return new BookmarkPicturesFragment();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View onCreateView(
|
|
||||||
@NonNull LayoutInflater inflater,
|
|
||||||
ViewGroup container,
|
|
||||||
Bundle savedInstanceState
|
|
||||||
) {
|
|
||||||
binding = FragmentBookmarksPicturesBinding.inflate(inflater, container, false);
|
|
||||||
return binding.getRoot();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
|
|
||||||
super.onViewCreated(view, savedInstanceState);
|
|
||||||
binding.bookmarkedPicturesList.setOnItemClickListener((AdapterView.OnItemClickListener) getParentFragment());
|
|
||||||
initList();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onStop() {
|
|
||||||
super.onStop();
|
|
||||||
controller.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
compositeDisposable.clear();
|
|
||||||
binding = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
if (controller.needRefreshBookmarkedPictures()) {
|
|
||||||
binding.bookmarkedPicturesList.setVisibility(GONE);
|
|
||||||
if (gridAdapter != null) {
|
|
||||||
gridAdapter.clear();
|
|
||||||
((BookmarkListRootFragment)getParentFragment()).viewPagerNotifyDataSetChanged();
|
|
||||||
}
|
|
||||||
initList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks for internet connection and then initializes
|
|
||||||
* the recycler view with bookmarked pictures
|
|
||||||
*/
|
|
||||||
@SuppressLint("CheckResult")
|
|
||||||
private void initList() {
|
|
||||||
if (!NetworkUtils.isInternetConnectionEstablished(getContext())) {
|
|
||||||
handleNoInternet();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.loadingImagesProgressBar.setVisibility(VISIBLE);
|
|
||||||
binding.statusMessage.setVisibility(GONE);
|
|
||||||
|
|
||||||
compositeDisposable.add(controller.loadBookmarkedPictures()
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(this::handleSuccess, this::handleError));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the UI updates for no internet scenario
|
|
||||||
*/
|
|
||||||
private void handleNoInternet() {
|
|
||||||
binding.loadingImagesProgressBar.setVisibility(GONE);
|
|
||||||
if (gridAdapter == null || gridAdapter.isEmpty()) {
|
|
||||||
binding.statusMessage.setVisibility(VISIBLE);
|
|
||||||
binding.statusMessage.setText(getString(R.string.no_internet));
|
|
||||||
} else {
|
|
||||||
ViewUtil.showShortSnackbar(binding.parentLayout, R.string.no_internet);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Logs and handles API error scenario
|
|
||||||
* @param throwable
|
|
||||||
*/
|
|
||||||
private void handleError(Throwable throwable) {
|
|
||||||
Timber.e(throwable, "Error occurred while loading images inside a category");
|
|
||||||
try{
|
|
||||||
ViewUtil.showShortSnackbar(binding.getRoot(), R.string.error_loading_images);
|
|
||||||
initErrorView();
|
|
||||||
}catch (Exception e){
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the UI updates for a error scenario
|
|
||||||
*/
|
|
||||||
private void initErrorView() {
|
|
||||||
binding.loadingImagesProgressBar.setVisibility(GONE);
|
|
||||||
if (gridAdapter == null || gridAdapter.isEmpty()) {
|
|
||||||
binding.statusMessage.setVisibility(VISIBLE);
|
|
||||||
binding.statusMessage.setText(getString(R.string.no_images_found));
|
|
||||||
} else {
|
|
||||||
binding.statusMessage.setVisibility(GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the UI updates when there is no bookmarks
|
|
||||||
*/
|
|
||||||
private void initEmptyBookmarkListView() {
|
|
||||||
binding.loadingImagesProgressBar.setVisibility(GONE);
|
|
||||||
if (gridAdapter == null || gridAdapter.isEmpty()) {
|
|
||||||
binding.statusMessage.setVisibility(VISIBLE);
|
|
||||||
binding.statusMessage.setText(getString(R.string.bookmark_empty));
|
|
||||||
} else {
|
|
||||||
binding.statusMessage.setVisibility(GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the success scenario
|
|
||||||
* On first load, it initializes the grid view. On subsequent loads, it adds items to the adapter
|
|
||||||
* @param collection List of new Media to be displayed
|
|
||||||
*/
|
|
||||||
private void handleSuccess(List<Media> collection) {
|
|
||||||
if (collection == null) {
|
|
||||||
initErrorView();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (collection.isEmpty()) {
|
|
||||||
initEmptyBookmarkListView();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (gridAdapter == null) {
|
|
||||||
setAdapter(collection);
|
|
||||||
} else {
|
|
||||||
if (gridAdapter.containsAll(collection)) {
|
|
||||||
binding.loadingImagesProgressBar.setVisibility(GONE);
|
|
||||||
binding.statusMessage.setVisibility(GONE);
|
|
||||||
binding.bookmarkedPicturesList.setVisibility(VISIBLE);
|
|
||||||
binding.bookmarkedPicturesList.setAdapter(gridAdapter);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
gridAdapter.addItems(collection);
|
|
||||||
((BookmarkListRootFragment) getParentFragment()).viewPagerNotifyDataSetChanged();
|
|
||||||
}
|
|
||||||
binding.loadingImagesProgressBar.setVisibility(GONE);
|
|
||||||
binding.statusMessage.setVisibility(GONE);
|
|
||||||
binding.bookmarkedPicturesList.setVisibility(VISIBLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the adapter with a list of Media objects
|
|
||||||
* @param mediaList List of new Media to be displayed
|
|
||||||
*/
|
|
||||||
private void setAdapter(List<Media> mediaList) {
|
|
||||||
gridAdapter = new GridViewAdapter(
|
|
||||||
this.getContext(),
|
|
||||||
R.layout.layout_category_images,
|
|
||||||
mediaList
|
|
||||||
);
|
|
||||||
binding.bookmarkedPicturesList.setAdapter(gridAdapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* It return an instance of gridView adapter which helps in extracting media details
|
|
||||||
* used by the gridView
|
|
||||||
* @return GridView Adapter
|
|
||||||
*/
|
|
||||||
public ListAdapter getAdapter() {
|
|
||||||
return binding.bookmarkedPicturesList.getAdapter();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,201 @@
|
||||||
|
package fr.free.nrw.commons.bookmarks.pictures
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.AdapterView.OnItemClickListener
|
||||||
|
import android.widget.ListAdapter
|
||||||
|
import dagger.android.support.DaggerFragment
|
||||||
|
import fr.free.nrw.commons.Media
|
||||||
|
import fr.free.nrw.commons.R
|
||||||
|
import fr.free.nrw.commons.bookmarks.BookmarkListRootFragment
|
||||||
|
import fr.free.nrw.commons.category.GridViewAdapter
|
||||||
|
import fr.free.nrw.commons.databinding.FragmentBookmarksPicturesBinding
|
||||||
|
import fr.free.nrw.commons.utils.NetworkUtils.isInternetConnectionEstablished
|
||||||
|
import fr.free.nrw.commons.utils.ViewUtil.showShortSnackbar
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.disposables.CompositeDisposable
|
||||||
|
import io.reactivex.functions.Consumer
|
||||||
|
import io.reactivex.schedulers.Schedulers
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class BookmarkPicturesFragment : DaggerFragment() {
|
||||||
|
private var gridAdapter: GridViewAdapter? = null
|
||||||
|
private val compositeDisposable = CompositeDisposable()
|
||||||
|
|
||||||
|
private var binding: FragmentBookmarksPicturesBinding? = null
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
@Inject
|
||||||
|
var controller: BookmarkPicturesController? = null
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
binding = FragmentBookmarksPicturesBinding.inflate(inflater, container, false)
|
||||||
|
return binding!!.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
binding!!.bookmarkedPicturesList.onItemClickListener =
|
||||||
|
parentFragment as OnItemClickListener?
|
||||||
|
initList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
super.onStop()
|
||||||
|
controller!!.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
compositeDisposable.clear()
|
||||||
|
binding = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
if (controller!!.needRefreshBookmarkedPictures()) {
|
||||||
|
binding!!.bookmarkedPicturesList.visibility = View.GONE
|
||||||
|
gridAdapter?.let {
|
||||||
|
it.clear()
|
||||||
|
(parentFragment as BookmarkListRootFragment).viewPagerNotifyDataSetChanged()
|
||||||
|
}
|
||||||
|
initList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks for internet connection and then initializes
|
||||||
|
* the recycler view with bookmarked pictures
|
||||||
|
*/
|
||||||
|
@SuppressLint("CheckResult")
|
||||||
|
private fun initList() {
|
||||||
|
if (!isInternetConnectionEstablished(context)) {
|
||||||
|
handleNoInternet()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
binding!!.loadingImagesProgressBar.visibility = View.VISIBLE
|
||||||
|
binding!!.statusMessage.visibility = View.GONE
|
||||||
|
|
||||||
|
compositeDisposable.add(
|
||||||
|
controller!!.loadBookmarkedPictures()
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(::handleSuccess, ::handleError)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the UI updates for no internet scenario
|
||||||
|
*/
|
||||||
|
private fun handleNoInternet() {
|
||||||
|
binding!!.loadingImagesProgressBar.visibility = View.GONE
|
||||||
|
if (gridAdapter == null || gridAdapter!!.isEmpty) {
|
||||||
|
binding!!.statusMessage.visibility = View.VISIBLE
|
||||||
|
binding!!.statusMessage.text = getString(R.string.no_internet)
|
||||||
|
} else {
|
||||||
|
showShortSnackbar(binding!!.parentLayout, R.string.no_internet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs and handles API error scenario
|
||||||
|
* @param throwable
|
||||||
|
*/
|
||||||
|
private fun handleError(throwable: Throwable) {
|
||||||
|
Timber.e(throwable, "Error occurred while loading images inside a category")
|
||||||
|
try {
|
||||||
|
showShortSnackbar(binding!!.root, R.string.error_loading_images)
|
||||||
|
initErrorView()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the UI updates for a error scenario
|
||||||
|
*/
|
||||||
|
private fun initErrorView() {
|
||||||
|
binding!!.loadingImagesProgressBar.visibility = View.GONE
|
||||||
|
if (gridAdapter == null || gridAdapter!!.isEmpty) {
|
||||||
|
binding!!.statusMessage.visibility = View.VISIBLE
|
||||||
|
binding!!.statusMessage.text = getString(R.string.no_images_found)
|
||||||
|
} else {
|
||||||
|
binding!!.statusMessage.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the UI updates when there is no bookmarks
|
||||||
|
*/
|
||||||
|
private fun initEmptyBookmarkListView() {
|
||||||
|
binding!!.loadingImagesProgressBar.visibility = View.GONE
|
||||||
|
if (gridAdapter == null || gridAdapter!!.isEmpty) {
|
||||||
|
binding!!.statusMessage.visibility = View.VISIBLE
|
||||||
|
binding!!.statusMessage.text = getString(R.string.bookmark_empty)
|
||||||
|
} else {
|
||||||
|
binding!!.statusMessage.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the success scenario
|
||||||
|
* On first load, it initializes the grid view. On subsequent loads, it adds items to the adapter
|
||||||
|
* @param collection List of new Media to be displayed
|
||||||
|
*/
|
||||||
|
private fun handleSuccess(collection: List<Media>?) {
|
||||||
|
if (collection == null) {
|
||||||
|
initErrorView()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (collection.isEmpty()) {
|
||||||
|
initEmptyBookmarkListView()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gridAdapter == null) {
|
||||||
|
setAdapter(collection)
|
||||||
|
} else {
|
||||||
|
if (gridAdapter!!.containsAll(collection)) {
|
||||||
|
binding!!.loadingImagesProgressBar.visibility = View.GONE
|
||||||
|
binding!!.statusMessage.visibility = View.GONE
|
||||||
|
binding!!.bookmarkedPicturesList.visibility = View.VISIBLE
|
||||||
|
binding!!.bookmarkedPicturesList.adapter = gridAdapter
|
||||||
|
return
|
||||||
|
}
|
||||||
|
gridAdapter!!.addItems(collection)
|
||||||
|
(parentFragment as BookmarkListRootFragment).viewPagerNotifyDataSetChanged()
|
||||||
|
}
|
||||||
|
binding!!.loadingImagesProgressBar.visibility = View.GONE
|
||||||
|
binding!!.statusMessage.visibility = View.GONE
|
||||||
|
binding!!.bookmarkedPicturesList.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the adapter with a list of Media objects
|
||||||
|
* @param mediaList List of new Media to be displayed
|
||||||
|
*/
|
||||||
|
private fun setAdapter(mediaList: List<Media>) {
|
||||||
|
gridAdapter = GridViewAdapter(
|
||||||
|
requireContext(),
|
||||||
|
R.layout.layout_category_images,
|
||||||
|
mediaList.toMutableList()
|
||||||
|
)
|
||||||
|
binding?.let { it.bookmarkedPicturesList.adapter = gridAdapter }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* It return an instance of gridView adapter which helps in extracting media details
|
||||||
|
* used by the gridView
|
||||||
|
* @return GridView Adapter
|
||||||
|
*/
|
||||||
|
fun getAdapter(): ListAdapter? = binding?.bookmarkedPicturesList?.adapter
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
package fr.free.nrw.commons.bookmarks.pictures
|
||||||
|
|
||||||
|
import android.database.sqlite.SQLiteDatabase
|
||||||
|
|
||||||
|
object BookmarksTable {
|
||||||
|
const val TABLE_NAME: String = "bookmarks"
|
||||||
|
const val COLUMN_MEDIA_NAME: String = "media_name"
|
||||||
|
const val COLUMN_CREATOR: String = "media_creator"
|
||||||
|
|
||||||
|
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
|
||||||
|
val ALL_FIELDS = arrayOf(
|
||||||
|
COLUMN_MEDIA_NAME,
|
||||||
|
COLUMN_CREATOR
|
||||||
|
)
|
||||||
|
|
||||||
|
const val DROP_TABLE_STATEMENT: String = "DROP TABLE IF EXISTS $TABLE_NAME"
|
||||||
|
|
||||||
|
const val CREATE_TABLE_STATEMENT: String = ("CREATE TABLE $TABLE_NAME (" +
|
||||||
|
"$COLUMN_MEDIA_NAME STRING PRIMARY KEY, " +
|
||||||
|
"$COLUMN_CREATOR STRING" +
|
||||||
|
");")
|
||||||
|
|
||||||
|
fun onCreate(db: SQLiteDatabase) =
|
||||||
|
db.execSQL(CREATE_TABLE_STATEMENT)
|
||||||
|
|
||||||
|
fun onDelete(db: SQLiteDatabase) {
|
||||||
|
db.execSQL(DROP_TABLE_STATEMENT)
|
||||||
|
onCreate(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onUpdate(db: SQLiteDatabase, from: Int, to: Int) {
|
||||||
|
if (from == to) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (from < 7) {
|
||||||
|
// doesn't exist yet
|
||||||
|
onUpdate(db, from+1, to)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (from == 7) {
|
||||||
|
// table added in version 8
|
||||||
|
onCreate(db)
|
||||||
|
onUpdate(db, from+1, to)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (from == 8) {
|
||||||
|
onUpdate(db, from+1, to)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,8 +7,8 @@ import com.google.gson.annotations.SerializedName
|
||||||
*/
|
*/
|
||||||
class CampaignConfig {
|
class CampaignConfig {
|
||||||
@SerializedName("showOnlyLiveCampaigns")
|
@SerializedName("showOnlyLiveCampaigns")
|
||||||
private val showOnlyLiveCampaigns = false
|
var showOnlyLiveCampaigns = false
|
||||||
|
|
||||||
@SerializedName("sortBy")
|
@SerializedName("sortBy")
|
||||||
private val sortBy: String? = null
|
var sortBy: String? = null
|
||||||
}
|
}
|
||||||
|
|
@ -8,8 +8,8 @@ import fr.free.nrw.commons.campaigns.models.Campaign
|
||||||
*/
|
*/
|
||||||
class CampaignResponseDTO {
|
class CampaignResponseDTO {
|
||||||
@SerializedName("config")
|
@SerializedName("config")
|
||||||
val campaignConfig: CampaignConfig? = null
|
var campaignConfig: CampaignConfig? = null
|
||||||
|
|
||||||
@SerializedName("campaigns")
|
@SerializedName("campaigns")
|
||||||
val campaigns: List<Campaign>? = null
|
var campaigns: List<Campaign>? = null
|
||||||
}
|
}
|
||||||
|
|
@ -7,7 +7,6 @@ import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import fr.free.nrw.commons.R
|
import fr.free.nrw.commons.R
|
||||||
import fr.free.nrw.commons.Utils
|
|
||||||
import fr.free.nrw.commons.campaigns.models.Campaign
|
import fr.free.nrw.commons.campaigns.models.Campaign
|
||||||
import fr.free.nrw.commons.contributions.MainActivity
|
import fr.free.nrw.commons.contributions.MainActivity
|
||||||
import fr.free.nrw.commons.databinding.LayoutCampaginBinding
|
import fr.free.nrw.commons.databinding.LayoutCampaginBinding
|
||||||
|
|
@ -16,6 +15,7 @@ import fr.free.nrw.commons.utils.CommonsDateUtil.getIso8601DateFormatShort
|
||||||
import fr.free.nrw.commons.utils.DateUtil.getExtraShortDateString
|
import fr.free.nrw.commons.utils.DateUtil.getExtraShortDateString
|
||||||
import fr.free.nrw.commons.utils.SwipableCardView
|
import fr.free.nrw.commons.utils.SwipableCardView
|
||||||
import fr.free.nrw.commons.utils.ViewUtil.showLongToast
|
import fr.free.nrw.commons.utils.ViewUtil.showLongToast
|
||||||
|
import fr.free.nrw.commons.utils.handleWebUrl
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.text.ParseException
|
import java.text.ParseException
|
||||||
|
|
||||||
|
|
@ -74,7 +74,7 @@ class CampaignView : SwipableCardView {
|
||||||
if (it.isWLMCampaign) {
|
if (it.isWLMCampaign) {
|
||||||
((context) as MainActivity).showNearby()
|
((context) as MainActivity).showNearby()
|
||||||
} else {
|
} else {
|
||||||
Utils.handleWebUrl(context, Uri.parse(it.link))
|
handleWebUrl(context, Uri.parse(it.link))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ class CampaignsPresenter @Inject constructor(
|
||||||
private val okHttpJsonApiClient: OkHttpJsonApiClient?,
|
private val okHttpJsonApiClient: OkHttpJsonApiClient?,
|
||||||
@param:Named(IO_THREAD) private val ioScheduler: Scheduler,
|
@param:Named(IO_THREAD) private val ioScheduler: Scheduler,
|
||||||
@param:Named(MAIN_THREAD) private val mainThreadScheduler: Scheduler
|
@param:Named(MAIN_THREAD) private val mainThreadScheduler: Scheduler
|
||||||
) : BasePresenter<ICampaignsView?> {
|
) : BasePresenter<ICampaignsView> {
|
||||||
private var view: ICampaignsView? = null
|
private var view: ICampaignsView? = null
|
||||||
private var disposable: Disposable? = null
|
private var disposable: Disposable? = null
|
||||||
private var campaign: Campaign? = null
|
private var campaign: Campaign? = null
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
package fr.free.nrw.commons.campaigns
|
package fr.free.nrw.commons.campaigns
|
||||||
|
|
||||||
import fr.free.nrw.commons.MvpView
|
|
||||||
import fr.free.nrw.commons.campaigns.models.Campaign
|
import fr.free.nrw.commons.campaigns.models.Campaign
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface which defines the view contracts of the campaign view
|
* Interface which defines the view contracts of the campaign view
|
||||||
*/
|
*/
|
||||||
interface ICampaignsView : MvpView {
|
interface ICampaignsView {
|
||||||
fun showCampaigns(campaign: Campaign?)
|
fun showCampaigns(campaign: Campaign?)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,9 @@ import android.database.sqlite.SQLiteDatabase
|
||||||
import android.database.sqlite.SQLiteQueryBuilder
|
import android.database.sqlite.SQLiteQueryBuilder
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import androidx.annotation.NonNull
|
|
||||||
import fr.free.nrw.commons.BuildConfig
|
import fr.free.nrw.commons.BuildConfig
|
||||||
import fr.free.nrw.commons.data.DBOpenHelper
|
|
||||||
import fr.free.nrw.commons.di.CommonsDaggerContentProvider
|
import fr.free.nrw.commons.di.CommonsDaggerContentProvider
|
||||||
import timber.log.Timber
|
import androidx.core.net.toUri
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
class CategoryContentProvider : CommonsDaggerContentProvider() {
|
class CategoryContentProvider : CommonsDaggerContentProvider() {
|
||||||
|
|
||||||
|
|
@ -23,9 +20,6 @@ class CategoryContentProvider : CommonsDaggerContentProvider() {
|
||||||
addURI(BuildConfig.CATEGORY_AUTHORITY, "${BASE_PATH}/#", CATEGORIES_ID)
|
addURI(BuildConfig.CATEGORY_AUTHORITY, "${BASE_PATH}/#", CATEGORIES_ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var dbOpenHelper: DBOpenHelper
|
|
||||||
|
|
||||||
@SuppressWarnings("ConstantConditions")
|
@SuppressWarnings("ConstantConditions")
|
||||||
override fun query(uri: Uri, projection: Array<String>?, selection: String?,
|
override fun query(uri: Uri, projection: Array<String>?, selection: String?,
|
||||||
selectionArgs: Array<String>?, sortOrder: String?): Cursor? {
|
selectionArgs: Array<String>?, sortOrder: String?): Cursor? {
|
||||||
|
|
@ -34,7 +28,7 @@ class CategoryContentProvider : CommonsDaggerContentProvider() {
|
||||||
}
|
}
|
||||||
|
|
||||||
val uriType = uriMatcher.match(uri)
|
val uriType = uriMatcher.match(uri)
|
||||||
val db = dbOpenHelper.readableDatabase
|
val db = requireDb()
|
||||||
|
|
||||||
val cursor: Cursor? = when (uriType) {
|
val cursor: Cursor? = when (uriType) {
|
||||||
CATEGORIES -> queryBuilder.query(
|
CATEGORIES -> queryBuilder.query(
|
||||||
|
|
@ -58,45 +52,37 @@ class CategoryContentProvider : CommonsDaggerContentProvider() {
|
||||||
else -> throw IllegalArgumentException("Unknown URI $uri")
|
else -> throw IllegalArgumentException("Unknown URI $uri")
|
||||||
}
|
}
|
||||||
|
|
||||||
cursor?.setNotificationUri(context?.contentResolver, uri)
|
cursor?.setNotificationUri(requireContext().contentResolver, uri)
|
||||||
return cursor
|
return cursor
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getType(uri: Uri): String? {
|
override fun getType(uri: Uri): String? = null
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("ConstantConditions")
|
@SuppressWarnings("ConstantConditions")
|
||||||
override fun insert(uri: Uri, contentValues: ContentValues?): Uri? {
|
override fun insert(uri: Uri, contentValues: ContentValues?): Uri {
|
||||||
val uriType = uriMatcher.match(uri)
|
val uriType = uriMatcher.match(uri)
|
||||||
val sqlDB = dbOpenHelper.writableDatabase
|
|
||||||
val id: Long
|
val id: Long
|
||||||
when (uriType) {
|
when (uriType) {
|
||||||
CATEGORIES -> {
|
CATEGORIES -> {
|
||||||
id = sqlDB.insert(TABLE_NAME, null, contentValues)
|
id = requireDb().insert(TABLE_NAME, null, contentValues)
|
||||||
}
|
}
|
||||||
else -> throw IllegalArgumentException("Unknown URI: $uri")
|
else -> throw IllegalArgumentException("Unknown URI: $uri")
|
||||||
}
|
}
|
||||||
context?.contentResolver?.notifyChange(uri, null)
|
requireContext().contentResolver?.notifyChange(uri, null)
|
||||||
return Uri.parse("${Companion.BASE_URI}/$id")
|
return "${BASE_URI}/$id".toUri()
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("ConstantConditions")
|
@SuppressWarnings("ConstantConditions")
|
||||||
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
|
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int = 0
|
||||||
// Not implemented
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("ConstantConditions")
|
@SuppressWarnings("ConstantConditions")
|
||||||
override fun bulkInsert(uri: Uri, values: Array<ContentValues>): Int {
|
override fun bulkInsert(uri: Uri, values: Array<ContentValues>): Int {
|
||||||
Timber.d("Hello, bulk insert! (CategoryContentProvider)")
|
|
||||||
val uriType = uriMatcher.match(uri)
|
val uriType = uriMatcher.match(uri)
|
||||||
val sqlDB = dbOpenHelper.writableDatabase
|
val sqlDB = requireDb()
|
||||||
sqlDB.beginTransaction()
|
sqlDB.beginTransaction()
|
||||||
when (uriType) {
|
when (uriType) {
|
||||||
CATEGORIES -> {
|
CATEGORIES -> {
|
||||||
for (value in values) {
|
for (value in values) {
|
||||||
Timber.d("Inserting! %s", value)
|
|
||||||
sqlDB.insert(TABLE_NAME, null, value)
|
sqlDB.insert(TABLE_NAME, null, value)
|
||||||
}
|
}
|
||||||
sqlDB.setTransactionSuccessful()
|
sqlDB.setTransactionSuccessful()
|
||||||
|
|
@ -104,7 +90,7 @@ class CategoryContentProvider : CommonsDaggerContentProvider() {
|
||||||
else -> throw IllegalArgumentException("Unknown URI: $uri")
|
else -> throw IllegalArgumentException("Unknown URI: $uri")
|
||||||
}
|
}
|
||||||
sqlDB.endTransaction()
|
sqlDB.endTransaction()
|
||||||
context?.contentResolver?.notifyChange(uri, null)
|
requireContext().contentResolver?.notifyChange(uri, null)
|
||||||
return values.size
|
return values.size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -112,17 +98,18 @@ class CategoryContentProvider : CommonsDaggerContentProvider() {
|
||||||
override fun update(uri: Uri, contentValues: ContentValues?, selection: String?,
|
override fun update(uri: Uri, contentValues: ContentValues?, selection: String?,
|
||||||
selectionArgs: Array<String>?): Int {
|
selectionArgs: Array<String>?): Int {
|
||||||
val uriType = uriMatcher.match(uri)
|
val uriType = uriMatcher.match(uri)
|
||||||
val sqlDB = dbOpenHelper.writableDatabase
|
|
||||||
val rowsUpdated: Int
|
val rowsUpdated: Int
|
||||||
when (uriType) {
|
when (uriType) {
|
||||||
CATEGORIES_ID -> {
|
CATEGORIES_ID -> {
|
||||||
if (TextUtils.isEmpty(selection)) {
|
if (TextUtils.isEmpty(selection)) {
|
||||||
val id = uri.lastPathSegment?.toInt()
|
val id = uri.lastPathSegment?.toInt()
|
||||||
?: throw IllegalArgumentException("Invalid ID")
|
?: throw IllegalArgumentException("Invalid ID")
|
||||||
rowsUpdated = sqlDB.update(TABLE_NAME,
|
rowsUpdated = requireDb().update(
|
||||||
|
TABLE_NAME,
|
||||||
contentValues,
|
contentValues,
|
||||||
"$COLUMN_ID = ?",
|
"$COLUMN_ID = ?",
|
||||||
arrayOf(id.toString()))
|
arrayOf(id.toString())
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
throw IllegalArgumentException(
|
throw IllegalArgumentException(
|
||||||
"Parameter `selection` should be empty when updating an ID")
|
"Parameter `selection` should be empty when updating an ID")
|
||||||
|
|
@ -130,7 +117,7 @@ class CategoryContentProvider : CommonsDaggerContentProvider() {
|
||||||
}
|
}
|
||||||
else -> throw IllegalArgumentException("Unknown URI: $uri with type $uriType")
|
else -> throw IllegalArgumentException("Unknown URI: $uri with type $uriType")
|
||||||
}
|
}
|
||||||
context?.contentResolver?.notifyChange(uri, null)
|
requireContext().contentResolver?.notifyChange(uri, null)
|
||||||
return rowsUpdated
|
return rowsUpdated
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -165,13 +152,9 @@ class CategoryContentProvider : CommonsDaggerContentProvider() {
|
||||||
"$COLUMN_TIMES_USED INTEGER" +
|
"$COLUMN_TIMES_USED INTEGER" +
|
||||||
");"
|
");"
|
||||||
|
|
||||||
fun uriForId(id: Int): Uri {
|
fun uriForId(id: Int): Uri = Uri.parse("${BASE_URI}/$id")
|
||||||
return Uri.parse("${BASE_URI}/$id")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onCreate(db: SQLiteDatabase) {
|
fun onCreate(db: SQLiteDatabase) = db.execSQL(CREATE_TABLE_STATEMENT)
|
||||||
db.execSQL(CREATE_TABLE_STATEMENT)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onDelete(db: SQLiteDatabase) {
|
fun onDelete(db: SQLiteDatabase) {
|
||||||
db.execSQL(DROP_TABLE_STATEMENT)
|
db.execSQL(DROP_TABLE_STATEMENT)
|
||||||
|
|
@ -200,6 +183,6 @@ class CategoryContentProvider : CommonsDaggerContentProvider() {
|
||||||
private const val CATEGORIES = 1
|
private const val CATEGORIES = 1
|
||||||
private const val CATEGORIES_ID = 2
|
private const val CATEGORIES_ID = 2
|
||||||
private const val BASE_PATH = "categories"
|
private const val BASE_PATH = "categories"
|
||||||
val BASE_URI: Uri = Uri.parse("content://${BuildConfig.CATEGORY_AUTHORITY}/${Companion.BASE_PATH}")
|
val BASE_URI: Uri = "content://${BuildConfig.CATEGORY_AUTHORITY}/${BASE_PATH}".toUri()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,21 +8,25 @@ import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import fr.free.nrw.commons.BuildConfig.COMMONS_URL
|
||||||
import fr.free.nrw.commons.Media
|
import fr.free.nrw.commons.Media
|
||||||
import fr.free.nrw.commons.R
|
import fr.free.nrw.commons.R
|
||||||
import fr.free.nrw.commons.Utils
|
|
||||||
import fr.free.nrw.commons.ViewPagerAdapter
|
import fr.free.nrw.commons.ViewPagerAdapter
|
||||||
import fr.free.nrw.commons.databinding.ActivityCategoryDetailsBinding
|
import fr.free.nrw.commons.databinding.ActivityCategoryDetailsBinding
|
||||||
import fr.free.nrw.commons.explore.categories.media.CategoriesMediaFragment
|
import fr.free.nrw.commons.explore.categories.media.CategoriesMediaFragment
|
||||||
import fr.free.nrw.commons.explore.categories.parent.ParentCategoriesFragment
|
import fr.free.nrw.commons.explore.categories.parent.ParentCategoriesFragment
|
||||||
import fr.free.nrw.commons.explore.categories.sub.SubCategoriesFragment
|
import fr.free.nrw.commons.explore.categories.sub.SubCategoriesFragment
|
||||||
import fr.free.nrw.commons.media.MediaDetailPagerFragment
|
import fr.free.nrw.commons.media.MediaDetailPagerFragment
|
||||||
|
import fr.free.nrw.commons.media.MediaDetailProvider
|
||||||
import fr.free.nrw.commons.theme.BaseActivity
|
import fr.free.nrw.commons.theme.BaseActivity
|
||||||
|
import fr.free.nrw.commons.utils.applyEdgeToEdgeAllInsets
|
||||||
|
import fr.free.nrw.commons.utils.handleWebUrl
|
||||||
|
import fr.free.nrw.commons.wikidata.model.WikiSite
|
||||||
|
import fr.free.nrw.commons.wikidata.model.page.PageTitle
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
@ -33,7 +37,7 @@ import javax.inject.Inject
|
||||||
* a particular category on wikimedia commons.
|
* a particular category on wikimedia commons.
|
||||||
*/
|
*/
|
||||||
class CategoryDetailsActivity : BaseActivity(),
|
class CategoryDetailsActivity : BaseActivity(),
|
||||||
MediaDetailPagerFragment.MediaDetailProvider,
|
MediaDetailProvider,
|
||||||
CategoryImagesCallback {
|
CategoryImagesCallback {
|
||||||
|
|
||||||
private lateinit var supportFragmentManager: FragmentManager
|
private lateinit var supportFragmentManager: FragmentManager
|
||||||
|
|
@ -54,9 +58,10 @@ class CategoryDetailsActivity : BaseActivity(),
|
||||||
|
|
||||||
binding = ActivityCategoryDetailsBinding.inflate(layoutInflater)
|
binding = ActivityCategoryDetailsBinding.inflate(layoutInflater)
|
||||||
val view = binding.root
|
val view = binding.root
|
||||||
|
applyEdgeToEdgeAllInsets(view)
|
||||||
setContentView(view)
|
setContentView(view)
|
||||||
supportFragmentManager = getSupportFragmentManager()
|
supportFragmentManager = getSupportFragmentManager()
|
||||||
viewPagerAdapter = ViewPagerAdapter(supportFragmentManager)
|
viewPagerAdapter = ViewPagerAdapter(this, supportFragmentManager)
|
||||||
binding.viewPager.adapter = viewPagerAdapter
|
binding.viewPager.adapter = viewPagerAdapter
|
||||||
binding.viewPager.offscreenPageLimit = 2
|
binding.viewPager.offscreenPageLimit = 2
|
||||||
binding.tabLayout.setupWithViewPager(binding.viewPager)
|
binding.tabLayout.setupWithViewPager(binding.viewPager)
|
||||||
|
|
@ -80,8 +85,6 @@ class CategoryDetailsActivity : BaseActivity(),
|
||||||
* Set the fragments according to the tab selected in the viewPager.
|
* Set the fragments according to the tab selected in the viewPager.
|
||||||
*/
|
*/
|
||||||
private fun setTabs() {
|
private fun setTabs() {
|
||||||
val fragmentList = mutableListOf<Fragment>()
|
|
||||||
val titleList = mutableListOf<String>()
|
|
||||||
categoriesMediaFragment = CategoriesMediaFragment()
|
categoriesMediaFragment = CategoriesMediaFragment()
|
||||||
val subCategoryListFragment = SubCategoriesFragment()
|
val subCategoryListFragment = SubCategoriesFragment()
|
||||||
val parentCategoriesFragment = ParentCategoriesFragment()
|
val parentCategoriesFragment = ParentCategoriesFragment()
|
||||||
|
|
@ -96,13 +99,12 @@ class CategoryDetailsActivity : BaseActivity(),
|
||||||
|
|
||||||
viewModel.onCheckIfBookmarked(categoryName!!)
|
viewModel.onCheckIfBookmarked(categoryName!!)
|
||||||
}
|
}
|
||||||
fragmentList.add(categoriesMediaFragment)
|
|
||||||
titleList.add("MEDIA")
|
viewPagerAdapter.setTabs(
|
||||||
fragmentList.add(subCategoryListFragment)
|
R.string.title_for_media to categoriesMediaFragment,
|
||||||
titleList.add("SUBCATEGORIES")
|
R.string.title_for_subcategories to subCategoryListFragment,
|
||||||
fragmentList.add(parentCategoriesFragment)
|
R.string.title_for_parent_categories to parentCategoriesFragment
|
||||||
titleList.add("PARENT CATEGORIES")
|
)
|
||||||
viewPagerAdapter.setTabData(fragmentList, titleList)
|
|
||||||
viewPagerAdapter.notifyDataSetChanged()
|
viewPagerAdapter.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -199,8 +201,9 @@ class CategoryDetailsActivity : BaseActivity(),
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
return when (item.itemId) {
|
return when (item.itemId) {
|
||||||
R.id.menu_browser_current_category -> {
|
R.id.menu_browser_current_category -> {
|
||||||
val title = Utils.getPageTitle(CATEGORY_PREFIX + categoryName)
|
val title = PageTitle(CATEGORY_PREFIX + categoryName, WikiSite(COMMONS_URL))
|
||||||
Utils.handleWebUrl(this, Uri.parse(title.canonicalUri))
|
|
||||||
|
handleWebUrl(this, Uri.parse(title.canonicalUri))
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,9 @@ class ExceptionAwareThreadPoolExecutor(
|
||||||
if (r.isDone) {
|
if (r.isDone) {
|
||||||
r.get()
|
r.get()
|
||||||
}
|
}
|
||||||
} catch (e: CancellationException) {
|
} catch (_: CancellationException) {
|
||||||
// ignore
|
// ignore
|
||||||
} catch (e: InterruptedException) {
|
} catch (_: InterruptedException) {
|
||||||
// ignore
|
// ignore
|
||||||
} catch (e: ExecutionException) {
|
} catch (e: ExecutionException) {
|
||||||
throwable = e.cause ?: e
|
throwable = e.cause ?: e
|
||||||
|
|
|
||||||
|
|
@ -180,8 +180,8 @@ class ContributionController @Inject constructor(@param:Named("default_preferenc
|
||||||
showAlertDialog(
|
showAlertDialog(
|
||||||
activity, activity.getString(R.string.location_permission_title),
|
activity, activity.getString(R.string.location_permission_title),
|
||||||
activity.getString(R.string.in_app_camera_location_permission_rationale),
|
activity.getString(R.string.in_app_camera_location_permission_rationale),
|
||||||
activity.getString(android.R.string.ok),
|
activity.getString(R.string.ok),
|
||||||
activity.getString(android.R.string.cancel),
|
activity.getString(R.string.cancel),
|
||||||
{
|
{
|
||||||
createDialogsAndHandleLocationPermissions(
|
createDialogsAndHandleLocationPermissions(
|
||||||
activity,
|
activity,
|
||||||
|
|
@ -253,13 +253,14 @@ class ContributionController @Inject constructor(@param:Named("default_preferenc
|
||||||
*/
|
*/
|
||||||
fun initiateCustomGalleryPickWithPermission(
|
fun initiateCustomGalleryPickWithPermission(
|
||||||
activity: Activity,
|
activity: Activity,
|
||||||
resultLauncher: ActivityResultLauncher<Intent>
|
resultLauncher: ActivityResultLauncher<Intent>,
|
||||||
|
singleSelection: Boolean = false
|
||||||
) {
|
) {
|
||||||
setPickerConfiguration(activity, true)
|
setPickerConfiguration(activity, true)
|
||||||
|
|
||||||
checkPermissionsAndPerformAction(
|
checkPermissionsAndPerformAction(
|
||||||
activity,
|
activity,
|
||||||
{ openCustomSelector(activity, resultLauncher, 0) },
|
{ FilePicker.openCustomSelector(activity, resultLauncher, 0, singleSelection) },
|
||||||
R.string.storage_permission_title,
|
R.string.storage_permission_title,
|
||||||
R.string.write_storage_permission_rationale,
|
R.string.write_storage_permission_rationale,
|
||||||
*PERMISSIONS_STORAGE
|
*PERMISSIONS_STORAGE
|
||||||
|
|
|
||||||
|
|
@ -8,23 +8,29 @@ import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.facebook.imagepipeline.request.ImageRequest
|
import com.facebook.imagepipeline.request.ImageRequest
|
||||||
import com.facebook.imagepipeline.request.ImageRequestBuilder
|
import com.facebook.imagepipeline.request.ImageRequestBuilder
|
||||||
|
import fr.free.nrw.commons.Media
|
||||||
|
import fr.free.nrw.commons.utils.MediaAttributionUtil
|
||||||
|
import fr.free.nrw.commons.MediaDataExtractor
|
||||||
import fr.free.nrw.commons.R
|
import fr.free.nrw.commons.R
|
||||||
import fr.free.nrw.commons.databinding.LayoutContributionBinding
|
import fr.free.nrw.commons.databinding.LayoutContributionBinding
|
||||||
import fr.free.nrw.commons.media.MediaClient
|
import fr.free.nrw.commons.media.MediaClient
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.disposables.CompositeDisposable
|
import io.reactivex.disposables.CompositeDisposable
|
||||||
import io.reactivex.schedulers.Schedulers
|
import io.reactivex.schedulers.Schedulers
|
||||||
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class ContributionViewHolder internal constructor(
|
class ContributionViewHolder internal constructor(
|
||||||
private val parent: View, private val callback: ContributionsListAdapter.Callback,
|
parent: View,
|
||||||
private val mediaClient: MediaClient
|
private val callback: ContributionsListAdapter.Callback,
|
||||||
|
private val compositeDisposable: CompositeDisposable,
|
||||||
|
private val mediaClient: MediaClient,
|
||||||
|
private val mediaDataExtractor: MediaDataExtractor
|
||||||
) : RecyclerView.ViewHolder(parent) {
|
) : RecyclerView.ViewHolder(parent) {
|
||||||
var binding: LayoutContributionBinding = LayoutContributionBinding.bind(parent)
|
var binding: LayoutContributionBinding = LayoutContributionBinding.bind(parent)
|
||||||
|
|
||||||
private var position = 0
|
private var position = 0
|
||||||
private var contribution: Contribution? = null
|
private var contribution: Contribution? = null
|
||||||
private val compositeDisposable = CompositeDisposable()
|
|
||||||
private var isWikipediaButtonDisplayed = false
|
private var isWikipediaButtonDisplayed = false
|
||||||
private val pausingPopUp: AlertDialog
|
private val pausingPopUp: AlertDialog
|
||||||
var imageRequest: ImageRequest? = null
|
var imageRequest: ImageRequest? = null
|
||||||
|
|
@ -54,7 +60,7 @@ an upload might take a dozen seconds. */
|
||||||
this.contribution = contribution
|
this.contribution = contribution
|
||||||
this.position = position
|
this.position = position
|
||||||
binding.contributionTitle.text = contribution.media.mostRelevantCaption
|
binding.contributionTitle.text = contribution.media.mostRelevantCaption
|
||||||
binding.authorView.text = contribution.media.getAuthorOrUser()
|
setAuthorText(contribution.media)
|
||||||
|
|
||||||
//Removes flicker of loading image.
|
//Removes flicker of loading image.
|
||||||
binding.contributionImage.hierarchy.fadeDuration = 0
|
binding.contributionImage.hierarchy.fadeDuration = 0
|
||||||
|
|
@ -93,6 +99,30 @@ an upload might take a dozen seconds. */
|
||||||
checkIfMediaExistsOnWikipediaPage(contribution)
|
checkIfMediaExistsOnWikipediaPage(contribution)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateAttribution() {
|
||||||
|
if (contribution != null) {
|
||||||
|
val media = contribution!!.media
|
||||||
|
if (!media.getAttributedAuthor().isNullOrEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
compositeDisposable.addAll(
|
||||||
|
mediaDataExtractor.fetchCreatorIdsAndLabels(media)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
{ idAndLabels ->
|
||||||
|
media.creatorName = MediaAttributionUtil.getCreatorName(idAndLabels)
|
||||||
|
setAuthorText(media)
|
||||||
|
},
|
||||||
|
{ t: Throwable? -> Timber.e(t) })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setAuthorText(media: Media) {
|
||||||
|
binding.authorView.text = MediaAttributionUtil.getTagLine(media, itemView.context)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a media exists on the corresponding Wikipedia article Currently the check is made
|
* Checks if a media exists on the corresponding Wikipedia article Currently the check is made
|
||||||
* for the device's current language Wikipedia
|
* for the device's current language Wikipedia
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import fr.free.nrw.commons.BasePresenter
|
||||||
interface ContributionsContract {
|
interface ContributionsContract {
|
||||||
|
|
||||||
interface View {
|
interface View {
|
||||||
fun showMessage(localizedMessage: String)
|
|
||||||
fun getContext(): Context?
|
fun getContext(): Context?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,6 @@ import androidx.work.WorkManager
|
||||||
import fr.free.nrw.commons.MapController.NearbyPlacesInfo
|
import fr.free.nrw.commons.MapController.NearbyPlacesInfo
|
||||||
import fr.free.nrw.commons.Media
|
import fr.free.nrw.commons.Media
|
||||||
import fr.free.nrw.commons.R
|
import fr.free.nrw.commons.R
|
||||||
import fr.free.nrw.commons.Utils
|
|
||||||
import fr.free.nrw.commons.auth.SessionManager
|
import fr.free.nrw.commons.auth.SessionManager
|
||||||
import fr.free.nrw.commons.campaigns.CampaignView
|
import fr.free.nrw.commons.campaigns.CampaignView
|
||||||
import fr.free.nrw.commons.campaigns.CampaignsPresenter
|
import fr.free.nrw.commons.campaigns.CampaignsPresenter
|
||||||
|
|
@ -44,7 +43,7 @@ import fr.free.nrw.commons.location.LatLng
|
||||||
import fr.free.nrw.commons.location.LocationServiceManager
|
import fr.free.nrw.commons.location.LocationServiceManager
|
||||||
import fr.free.nrw.commons.location.LocationUpdateListener
|
import fr.free.nrw.commons.location.LocationUpdateListener
|
||||||
import fr.free.nrw.commons.media.MediaDetailPagerFragment
|
import fr.free.nrw.commons.media.MediaDetailPagerFragment
|
||||||
import fr.free.nrw.commons.media.MediaDetailPagerFragment.MediaDetailProvider
|
import fr.free.nrw.commons.media.MediaDetailProvider
|
||||||
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
|
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
|
||||||
import fr.free.nrw.commons.nearby.NearbyController
|
import fr.free.nrw.commons.nearby.NearbyController
|
||||||
import fr.free.nrw.commons.nearby.NearbyNotificationCardView
|
import fr.free.nrw.commons.nearby.NearbyNotificationCardView
|
||||||
|
|
@ -64,13 +63,15 @@ import fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween
|
||||||
import fr.free.nrw.commons.utils.NetworkUtils.isInternetConnectionEstablished
|
import fr.free.nrw.commons.utils.NetworkUtils.isInternetConnectionEstablished
|
||||||
import fr.free.nrw.commons.utils.PermissionUtils.hasPermission
|
import fr.free.nrw.commons.utils.PermissionUtils.hasPermission
|
||||||
import fr.free.nrw.commons.utils.ViewUtil.showLongToast
|
import fr.free.nrw.commons.utils.ViewUtil.showLongToast
|
||||||
|
import fr.free.nrw.commons.utils.isMonumentsEnabled
|
||||||
|
import fr.free.nrw.commons.utils.wLMEndDate
|
||||||
|
import fr.free.nrw.commons.utils.wLMStartDate
|
||||||
import io.reactivex.Observable
|
import io.reactivex.Observable
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.disposables.CompositeDisposable
|
import io.reactivex.disposables.CompositeDisposable
|
||||||
import io.reactivex.schedulers.Schedulers
|
import io.reactivex.schedulers.Schedulers
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
import java.util.Date
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Named
|
import javax.inject.Named
|
||||||
|
|
||||||
|
|
@ -139,7 +140,7 @@ class ContributionsFragment : CommonsDaggerSupportFragment(), FragmentManager.On
|
||||||
|
|
||||||
private var wlmCampaign: Campaign? = null
|
private var wlmCampaign: Campaign? = null
|
||||||
|
|
||||||
var userName: String? = null
|
private var userName: String? = null
|
||||||
private var isUserProfile = false
|
private var isUserProfile = false
|
||||||
|
|
||||||
private var mSensorManager: SensorManager? = null
|
private var mSensorManager: SensorManager? = null
|
||||||
|
|
@ -242,8 +243,8 @@ class ContributionsFragment : CommonsDaggerSupportFragment(), FragmentManager.On
|
||||||
private fun initWLMCampaign() {
|
private fun initWLMCampaign() {
|
||||||
wlmCampaign = Campaign(
|
wlmCampaign = Campaign(
|
||||||
getString(R.string.wlm_campaign_title),
|
getString(R.string.wlm_campaign_title),
|
||||||
getString(R.string.wlm_campaign_description), Utils.getWLMStartDate().toString(),
|
getString(R.string.wlm_campaign_description), wLMStartDate,
|
||||||
Utils.getWLMEndDate().toString(), NearbyParentFragment.WLM_URL, true
|
wLMEndDate, NearbyParentFragment.WLM_URL, true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -729,7 +730,7 @@ class ContributionsFragment : CommonsDaggerSupportFragment(), FragmentManager.On
|
||||||
* of campaigns on the campaigns card
|
* of campaigns on the campaigns card
|
||||||
*/
|
*/
|
||||||
private fun fetchCampaigns() {
|
private fun fetchCampaigns() {
|
||||||
if (Utils.isMonumentsEnabled(Date())) {
|
if (isMonumentsEnabled) {
|
||||||
if (binding != null) {
|
if (binding != null) {
|
||||||
binding!!.campaignsView.setCampaign(wlmCampaign)
|
binding!!.campaignsView.setCampaign(wlmCampaign)
|
||||||
binding!!.campaignsView.visibility = View.VISIBLE
|
binding!!.campaignsView.visibility = View.VISIBLE
|
||||||
|
|
@ -743,10 +744,6 @@ class ContributionsFragment : CommonsDaggerSupportFragment(), FragmentManager.On
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun showMessage(message: String) {
|
|
||||||
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun showCampaigns(campaign: Campaign?) {
|
override fun showCampaigns(campaign: Campaign?) {
|
||||||
if (campaign != null && !isUserProfile) {
|
if (campaign != null && !isUserProfile) {
|
||||||
if (binding != null) {
|
if (binding != null) {
|
||||||
|
|
@ -808,10 +805,11 @@ class ContributionsFragment : CommonsDaggerSupportFragment(), FragmentManager.On
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// /**
|
||||||
* Temporarily disabled, see issue [https://github.com/commons-app/apps-android-commons/issues/5847]
|
// * Temporarily disabled. See issue [#5847](https://github.com/commons-app/apps-android-commons/issues/5847)
|
||||||
* @param count The number of pending uploads.
|
// * @param count The number of pending uploads.
|
||||||
*/
|
// */
|
||||||
|
// public void updateUploadIcon(int count) {
|
||||||
// public void updateUploadIcon(int count) {
|
// public void updateUploadIcon(int count) {
|
||||||
// if (pendingUploadsImageView != null) {
|
// if (pendingUploadsImageView != null) {
|
||||||
// if (count != 0) {
|
// if (count != 0) {
|
||||||
|
|
|
||||||
|
|
@ -4,21 +4,26 @@ import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.paging.PagedListAdapter
|
import androidx.paging.PagedListAdapter
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import fr.free.nrw.commons.MediaDataExtractor
|
||||||
import fr.free.nrw.commons.R
|
import fr.free.nrw.commons.R
|
||||||
import fr.free.nrw.commons.media.MediaClient
|
import fr.free.nrw.commons.media.MediaClient
|
||||||
|
import io.reactivex.disposables.CompositeDisposable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents The View Adapter for the List of Contributions
|
* Represents The View Adapter for the List of Contributions
|
||||||
*/
|
*/
|
||||||
class ContributionsListAdapter internal constructor(
|
class ContributionsListAdapter internal constructor(
|
||||||
private val callback: Callback,
|
private val callback: Callback,
|
||||||
private val mediaClient: MediaClient
|
private val mediaClient: MediaClient,
|
||||||
|
private val mediaDataExtractor: MediaDataExtractor,
|
||||||
|
private val compositeDisposable: CompositeDisposable
|
||||||
) : PagedListAdapter<Contribution, ContributionViewHolder>(DIFF_CALLBACK) {
|
) : PagedListAdapter<Contribution, ContributionViewHolder>(DIFF_CALLBACK) {
|
||||||
/**
|
/**
|
||||||
* Initializes the view holder with contribution data
|
* Initializes the view holder with contribution data
|
||||||
*/
|
*/
|
||||||
override fun onBindViewHolder(holder: ContributionViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: ContributionViewHolder, position: Int) {
|
||||||
holder.init(position, getItem(position))
|
holder.init(position, getItem(position))
|
||||||
|
holder.updateAttribution()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getContributionForPosition(position: Int): Contribution? {
|
fun getContributionForPosition(position: Int): Contribution? {
|
||||||
|
|
@ -36,7 +41,7 @@ class ContributionsListAdapter internal constructor(
|
||||||
val viewHolder = ContributionViewHolder(
|
val viewHolder = ContributionViewHolder(
|
||||||
LayoutInflater.from(parent.context)
|
LayoutInflater.from(parent.context)
|
||||||
.inflate(R.layout.layout_contribution, parent, false),
|
.inflate(R.layout.layout_contribution, parent, false),
|
||||||
callback, mediaClient
|
callback, compositeDisposable, mediaClient, mediaDataExtractor
|
||||||
)
|
)
|
||||||
return viewHolder
|
return viewHolder
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ class ContributionsListContract {
|
||||||
fun showNoContributionsUI(shouldShow: Boolean)
|
fun showNoContributionsUI(shouldShow: Boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserActionListener : BasePresenter<View?> {
|
interface UserActionListener : BasePresenter<View> {
|
||||||
fun refreshList(swipeRefreshLayout: SwipeRefreshLayout?)
|
fun refreshList(swipeRefreshLayout: SwipeRefreshLayout?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
|
@ -20,6 +19,8 @@ import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions
|
import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions
|
||||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
|
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
|
||||||
import androidx.annotation.VisibleForTesting
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import androidx.core.os.BundleCompat
|
||||||
import androidx.paging.PagedList
|
import androidx.paging.PagedList
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
|
@ -27,8 +28,8 @@ import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver
|
||||||
import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener
|
import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener
|
||||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
import fr.free.nrw.commons.Media
|
import fr.free.nrw.commons.Media
|
||||||
|
import fr.free.nrw.commons.MediaDataExtractor
|
||||||
import fr.free.nrw.commons.R
|
import fr.free.nrw.commons.R
|
||||||
import fr.free.nrw.commons.Utils
|
|
||||||
import fr.free.nrw.commons.auth.SessionManager
|
import fr.free.nrw.commons.auth.SessionManager
|
||||||
import fr.free.nrw.commons.contributions.WikipediaInstructionsDialogFragment.Companion.newInstance
|
import fr.free.nrw.commons.contributions.WikipediaInstructionsDialogFragment.Companion.newInstance
|
||||||
import fr.free.nrw.commons.databinding.FragmentContributionsListBinding
|
import fr.free.nrw.commons.databinding.FragmentContributionsListBinding
|
||||||
|
|
@ -38,10 +39,10 @@ import fr.free.nrw.commons.filepicker.FilePicker
|
||||||
import fr.free.nrw.commons.media.MediaClient
|
import fr.free.nrw.commons.media.MediaClient
|
||||||
import fr.free.nrw.commons.profile.ProfileActivity
|
import fr.free.nrw.commons.profile.ProfileActivity
|
||||||
import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog
|
import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog
|
||||||
import fr.free.nrw.commons.utils.SystemThemeUtils
|
|
||||||
import fr.free.nrw.commons.utils.ViewUtil.showShortToast
|
import fr.free.nrw.commons.utils.ViewUtil.showShortToast
|
||||||
|
import fr.free.nrw.commons.utils.copyToClipboard
|
||||||
|
import fr.free.nrw.commons.utils.handleWebUrl
|
||||||
import fr.free.nrw.commons.wikidata.model.WikiSite
|
import fr.free.nrw.commons.wikidata.model.WikiSite
|
||||||
import org.apache.commons.lang3.StringUtils
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Named
|
import javax.inject.Named
|
||||||
|
|
||||||
|
|
@ -51,10 +52,6 @@ import javax.inject.Named
|
||||||
*/
|
*/
|
||||||
class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsListContract.View,
|
class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsListContract.View,
|
||||||
ContributionsListAdapter.Callback, WikipediaInstructionsDialogFragment.Callback {
|
ContributionsListAdapter.Callback, WikipediaInstructionsDialogFragment.Callback {
|
||||||
@JvmField
|
|
||||||
@Inject
|
|
||||||
var systemThemeUtils: SystemThemeUtils? = null
|
|
||||||
|
|
||||||
@JvmField
|
@JvmField
|
||||||
@Inject
|
@Inject
|
||||||
var controller: ContributionController? = null
|
var controller: ContributionController? = null
|
||||||
|
|
@ -63,6 +60,10 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL
|
||||||
@Inject
|
@Inject
|
||||||
var mediaClient: MediaClient? = null
|
var mediaClient: MediaClient? = null
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
@Inject
|
||||||
|
var mediaDataExtractor: MediaDataExtractor? = null
|
||||||
|
|
||||||
@JvmField
|
@JvmField
|
||||||
@Named(NetworkingModule.NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE)
|
@Named(NetworkingModule.NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE)
|
||||||
@Inject
|
@Inject
|
||||||
|
|
@ -77,13 +78,14 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL
|
||||||
var sessionManager: SessionManager? = null
|
var sessionManager: SessionManager? = null
|
||||||
|
|
||||||
private var binding: FragmentContributionsListBinding? = null
|
private var binding: FragmentContributionsListBinding? = null
|
||||||
private var fab_close: Animation? = null
|
private var fabClose: Animation? = null
|
||||||
private var fab_open: Animation? = null
|
private var fabOpen: Animation? = null
|
||||||
private var rotate_forward: Animation? = null
|
private var rotateForward: Animation? = null
|
||||||
private var rotate_backward: Animation? = null
|
private var rotateBackward: Animation? = null
|
||||||
private var isFabOpen = false
|
private var isFabOpen = false
|
||||||
|
|
||||||
private lateinit var inAppCameraLocationPermissionLauncher: ActivityResultLauncher<Array<String>>
|
private lateinit var inAppCameraLocationPermissionLauncher:
|
||||||
|
ActivityResultLauncher<Array<String>>
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
var rvContributionsList: RecyclerView? = null
|
var rvContributionsList: RecyclerView? = null
|
||||||
|
|
@ -94,8 +96,8 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
var callback: Callback? = null
|
var callback: Callback? = null
|
||||||
|
|
||||||
private val SPAN_COUNT_LANDSCAPE = 3
|
private val spanCountLandscape = 3
|
||||||
private val SPAN_COUNT_PORTRAIT = 1
|
private val spanCountPortrait = 1
|
||||||
|
|
||||||
private var contributionsSize = 0
|
private var contributionsSize = 0
|
||||||
private var userName: String? = null
|
private var userName: String? = null
|
||||||
|
|
@ -144,7 +146,7 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL
|
||||||
userName = requireArguments().getString(ProfileActivity.KEY_USERNAME)
|
userName = requireArguments().getString(ProfileActivity.KEY_USERNAME)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (StringUtils.isEmpty(userName)) {
|
if (userName.isNullOrEmpty()) {
|
||||||
userName = sessionManager!!.userName
|
userName = sessionManager!!.userName
|
||||||
}
|
}
|
||||||
inAppCameraLocationPermissionLauncher =
|
inAppCameraLocationPermissionLauncher =
|
||||||
|
|
@ -155,7 +157,8 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL
|
||||||
controller?.locationPermissionCallback?.onLocationPermissionGranted()
|
controller?.locationPermissionCallback?.onLocationPermissionGranted()
|
||||||
} else {
|
} else {
|
||||||
activity?.let { currentActivity ->
|
activity?.let { currentActivity ->
|
||||||
if (currentActivity.shouldShowRequestPermissionRationale(permission.ACCESS_FINE_LOCATION)) {
|
if (currentActivity.shouldShowRequestPermissionRationale(
|
||||||
|
permission.ACCESS_FINE_LOCATION)) {
|
||||||
controller?.handleShowRationaleFlowCameraLocation(
|
controller?.handleShowRationaleFlowCameraLocation(
|
||||||
currentActivity,
|
currentActivity,
|
||||||
inAppCameraLocationPermissionLauncher, // Pass launcher
|
inAppCameraLocationPermissionLauncher, // Pass launcher
|
||||||
|
|
@ -163,7 +166,8 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
controller?.locationPermissionCallback?.onLocationPermissionDenied(
|
controller?.locationPermissionCallback?.onLocationPermissionDenied(
|
||||||
currentActivity.getString(R.string.in_app_camera_location_permission_denied)
|
currentActivity.getString(
|
||||||
|
R.string.in_app_camera_location_permission_denied)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -183,7 +187,7 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL
|
||||||
contributionsListPresenter!!.onAttachView(this)
|
contributionsListPresenter!!.onAttachView(this)
|
||||||
binding!!.fabCustomGallery.setOnClickListener { v: View? -> launchCustomSelector() }
|
binding!!.fabCustomGallery.setOnClickListener { v: View? -> launchCustomSelector() }
|
||||||
binding!!.fabCustomGallery.setOnLongClickListener { view: View? ->
|
binding!!.fabCustomGallery.setOnLongClickListener { view: View? ->
|
||||||
showShortToast(context, fr.free.nrw.commons.R.string.custom_selector_title)
|
showShortToast(context, R.string.custom_selector_title)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -193,7 +197,7 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL
|
||||||
} else {
|
} else {
|
||||||
binding!!.tvContributionsOfUser.visibility = View.VISIBLE
|
binding!!.tvContributionsOfUser.visibility = View.VISIBLE
|
||||||
binding!!.tvContributionsOfUser.text =
|
binding!!.tvContributionsOfUser.text =
|
||||||
getString(fr.free.nrw.commons.R.string.contributions_of_user, userName)
|
getString(R.string.contributions_of_user, userName)
|
||||||
binding!!.fabLayout.visibility = View.GONE
|
binding!!.fabLayout.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -231,7 +235,10 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initAdapter() {
|
private fun initAdapter() {
|
||||||
adapter = ContributionsListAdapter(this, mediaClient!!)
|
adapter = ContributionsListAdapter(this,
|
||||||
|
mediaClient!!,
|
||||||
|
mediaDataExtractor!!,
|
||||||
|
compositeDisposable)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
|
@ -306,7 +313,7 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL
|
||||||
override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean {
|
override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean {
|
||||||
if (e.action == MotionEvent.ACTION_DOWN) {
|
if (e.action == MotionEvent.ACTION_DOWN) {
|
||||||
if (isFabOpen) {
|
if (isFabOpen) {
|
||||||
animateFAB(isFabOpen)
|
animateFAB(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|
@ -338,14 +345,20 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getSpanCount(orientation: Int): Int {
|
private fun getSpanCount(orientation: Int): Int {
|
||||||
return if (orientation == Configuration.ORIENTATION_LANDSCAPE) SPAN_COUNT_LANDSCAPE else SPAN_COUNT_PORTRAIT
|
return if (orientation == Configuration.ORIENTATION_LANDSCAPE)
|
||||||
|
spanCountLandscape
|
||||||
|
else
|
||||||
|
spanCountPortrait
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
super.onConfigurationChanged(newConfig)
|
super.onConfigurationChanged(newConfig)
|
||||||
// check orientation
|
// check orientation
|
||||||
binding!!.fabLayout.orientation =
|
binding!!.fabLayout.orientation =
|
||||||
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) LinearLayout.HORIZONTAL else LinearLayout.VERTICAL
|
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE)
|
||||||
|
LinearLayout.HORIZONTAL
|
||||||
|
else
|
||||||
|
LinearLayout.VERTICAL
|
||||||
rvContributionsList
|
rvContributionsList
|
||||||
?.setLayoutManager(
|
?.setLayoutManager(
|
||||||
GridLayoutManager(context, getSpanCount(newConfig.orientation))
|
GridLayoutManager(context, getSpanCount(newConfig.orientation))
|
||||||
|
|
@ -353,10 +366,10 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initializeAnimations() {
|
private fun initializeAnimations() {
|
||||||
fab_open = AnimationUtils.loadAnimation(activity, fr.free.nrw.commons.R.anim.fab_open)
|
fabOpen = AnimationUtils.loadAnimation(activity, R.anim.fab_open)
|
||||||
fab_close = AnimationUtils.loadAnimation(activity, fr.free.nrw.commons.R.anim.fab_close)
|
fabClose = AnimationUtils.loadAnimation(activity, R.anim.fab_close)
|
||||||
rotate_forward = AnimationUtils.loadAnimation(activity, fr.free.nrw.commons.R.anim.rotate_forward)
|
rotateForward = AnimationUtils.loadAnimation(activity, R.anim.rotate_forward)
|
||||||
rotate_backward = AnimationUtils.loadAnimation(activity, fr.free.nrw.commons.R.anim.rotate_backward)
|
rotateBackward = AnimationUtils.loadAnimation(activity, R.anim.rotate_backward)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setListeners() {
|
private fun setListeners() {
|
||||||
|
|
@ -372,7 +385,7 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL
|
||||||
binding!!.fabCamera.setOnLongClickListener { view: View? ->
|
binding!!.fabCamera.setOnLongClickListener { view: View? ->
|
||||||
showShortToast(
|
showShortToast(
|
||||||
context,
|
context,
|
||||||
fr.free.nrw.commons.R.string.add_contribution_from_camera
|
R.string.add_contribution_from_camera
|
||||||
)
|
)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
@ -381,7 +394,7 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL
|
||||||
animateFAB(isFabOpen)
|
animateFAB(isFabOpen)
|
||||||
}
|
}
|
||||||
binding!!.fabGallery.setOnLongClickListener { view: View? ->
|
binding!!.fabGallery.setOnLongClickListener { view: View? ->
|
||||||
showShortToast(context, fr.free.nrw.commons.R.string.menu_from_gallery)
|
showShortToast(context, R.string.menu_from_gallery)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -389,7 +402,7 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL
|
||||||
/**
|
/**
|
||||||
* Launch Custom Selector.
|
* Launch Custom Selector.
|
||||||
*/
|
*/
|
||||||
protected fun launchCustomSelector() {
|
private fun launchCustomSelector() {
|
||||||
controller!!.initiateCustomGalleryPickWithPermission(
|
controller!!.initiateCustomGalleryPickWithPermission(
|
||||||
requireActivity(),
|
requireActivity(),
|
||||||
customSelectorLauncherForResult
|
customSelectorLauncherForResult
|
||||||
|
|
@ -405,18 +418,18 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL
|
||||||
this.isFabOpen = !isFabOpen
|
this.isFabOpen = !isFabOpen
|
||||||
if (binding!!.fabPlus.isShown) {
|
if (binding!!.fabPlus.isShown) {
|
||||||
if (isFabOpen) {
|
if (isFabOpen) {
|
||||||
binding!!.fabPlus.startAnimation(rotate_backward)
|
binding!!.fabPlus.startAnimation(rotateBackward)
|
||||||
binding!!.fabCamera.startAnimation(fab_close)
|
binding!!.fabCamera.startAnimation(fabClose)
|
||||||
binding!!.fabGallery.startAnimation(fab_close)
|
binding!!.fabGallery.startAnimation(fabClose)
|
||||||
binding!!.fabCustomGallery.startAnimation(fab_close)
|
binding!!.fabCustomGallery.startAnimation(fabClose)
|
||||||
binding!!.fabCamera.hide()
|
binding!!.fabCamera.hide()
|
||||||
binding!!.fabGallery.hide()
|
binding!!.fabGallery.hide()
|
||||||
binding!!.fabCustomGallery.hide()
|
binding!!.fabCustomGallery.hide()
|
||||||
} else {
|
} else {
|
||||||
binding!!.fabPlus.startAnimation(rotate_forward)
|
binding!!.fabPlus.startAnimation(rotateForward)
|
||||||
binding!!.fabCamera.startAnimation(fab_open)
|
binding!!.fabCamera.startAnimation(fabOpen)
|
||||||
binding!!.fabGallery.startAnimation(fab_open)
|
binding!!.fabGallery.startAnimation(fabOpen)
|
||||||
binding!!.fabCustomGallery.startAnimation(fab_open)
|
binding!!.fabCustomGallery.startAnimation(fabOpen)
|
||||||
binding!!.fabCamera.show()
|
binding!!.fabCamera.show()
|
||||||
binding!!.fabGallery.show()
|
binding!!.fabGallery.show()
|
||||||
binding!!.fabCustomGallery.show()
|
binding!!.fabCustomGallery.show()
|
||||||
|
|
@ -428,9 +441,9 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL
|
||||||
/**
|
/**
|
||||||
* Shows welcome message if user has no contributions yet i.e. new user.
|
* Shows welcome message if user has no contributions yet i.e. new user.
|
||||||
*/
|
*/
|
||||||
override fun showWelcomeTip(shouldShow: Boolean) {
|
override fun showWelcomeTip(numberOfUploads: Boolean) {
|
||||||
binding!!.noContributionsYet.visibility =
|
binding!!.noContributionsYet.visibility =
|
||||||
if (shouldShow) View.VISIBLE else View.GONE
|
if (numberOfUploads) View.VISIBLE else View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -450,22 +463,22 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
val layoutManager = rvContributionsList
|
val layoutManager = rvContributionsList?.layoutManager as GridLayoutManager?
|
||||||
?.getLayoutManager() as GridLayoutManager?
|
|
||||||
outState.putParcelable(RV_STATE, layoutManager!!.onSaveInstanceState())
|
outState.putParcelable(RV_STATE, layoutManager!!.onSaveInstanceState())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewStateRestored(savedInstanceState: Bundle?) {
|
override fun onViewStateRestored(savedInstanceState: Bundle?) {
|
||||||
super.onViewStateRestored(savedInstanceState)
|
super.onViewStateRestored(savedInstanceState)
|
||||||
if (null != savedInstanceState) {
|
if (null != savedInstanceState) {
|
||||||
val savedRecyclerLayoutState = savedInstanceState.getParcelable<Parcelable>(RV_STATE)
|
val savedRecyclerLayoutState =
|
||||||
|
BundleCompat.getParcelable(savedInstanceState, RV_STATE, Parcelable::class.java)
|
||||||
rvContributionsList!!.layoutManager!!.onRestoreInstanceState(savedRecyclerLayoutState)
|
rvContributionsList!!.layoutManager!!.onRestoreInstanceState(savedRecyclerLayoutState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun openMediaDetail(position: Int, isWikipediaButtonDisplayed: Boolean) {
|
override fun openMediaDetail(contribution: Int, isWikipediaPageExists: Boolean) {
|
||||||
if (null != callback) { //Just being safe, ideally they won't be called when detached
|
if (null != callback) { //Just being safe, ideally they won't be called when detached
|
||||||
callback!!.showDetail(position, isWikipediaButtonDisplayed)
|
callback!!.showDetail(contribution, isWikipediaPageExists)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -477,8 +490,8 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL
|
||||||
override fun addImageToWikipedia(contribution: Contribution?) {
|
override fun addImageToWikipedia(contribution: Contribution?) {
|
||||||
showAlertDialog(
|
showAlertDialog(
|
||||||
requireActivity(),
|
requireActivity(),
|
||||||
getString(fr.free.nrw.commons.R.string.add_picture_to_wikipedia_article_title),
|
getString(R.string.add_picture_to_wikipedia_article_title),
|
||||||
getString(fr.free.nrw.commons.R.string.add_picture_to_wikipedia_article_desc),
|
getString(R.string.add_picture_to_wikipedia_article_desc),
|
||||||
{
|
{
|
||||||
if (contribution != null) {
|
if (contribution != null) {
|
||||||
showAddImageToWikipediaInstructions(contribution)
|
showAddImageToWikipediaInstructions(contribution)
|
||||||
|
|
@ -492,16 +505,18 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL
|
||||||
* @param contribution
|
* @param contribution
|
||||||
*/
|
*/
|
||||||
private fun showAddImageToWikipediaInstructions(contribution: Contribution) {
|
private fun showAddImageToWikipediaInstructions(contribution: Contribution) {
|
||||||
val fragmentManager = fragmentManager
|
val fragmentManager = this.parentFragmentManager
|
||||||
val fragment = newInstance(contribution)
|
val fragment = newInstance(contribution)
|
||||||
fragment.callback =
|
fragment.callback =
|
||||||
WikipediaInstructionsDialogFragment.Callback { contribution: Contribution?, copyWikicode: Boolean ->
|
WikipediaInstructionsDialogFragment.Callback {
|
||||||
this.onConfirmClicked(
|
contribution: Contribution?,
|
||||||
|
copyWikicode: Boolean ->
|
||||||
|
onConfirmClicked(
|
||||||
contribution,
|
contribution,
|
||||||
copyWikicode
|
copyWikicode
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
fragment.show(fragmentManager!!, "WikimediaFragment")
|
fragment.show(fragmentManager, "WikimediaFragment")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -522,14 +537,13 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL
|
||||||
*/
|
*/
|
||||||
override fun onConfirmClicked(contribution: Contribution?, copyWikicode: Boolean) {
|
override fun onConfirmClicked(contribution: Contribution?, copyWikicode: Boolean) {
|
||||||
if (copyWikicode) {
|
if (copyWikicode) {
|
||||||
val wikicode = contribution!!.media.wikiCode
|
requireContext().copyToClipboard("wikicode", contribution!!.media.wikiCode)
|
||||||
Utils.copy("wikicode", wikicode, context)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val url =
|
val url =
|
||||||
languageWikipediaSite!!.mobileUrl() + "/wiki/" + (contribution!!.wikidataPlace
|
languageWikipediaSite!!.mobileUrl() + "/wiki/" + (contribution!!.wikidataPlace
|
||||||
?.getWikipediaPageTitle())
|
?.getWikipediaPageTitle())
|
||||||
Utils.handleWebUrl(context, Uri.parse(url))
|
handleWebUrl(requireContext(), url.toUri())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getContributionStateAt(position: Int): Int {
|
fun getContributionStateAt(position: Int): Int {
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ import androidx.fragment.app.FragmentManager
|
||||||
import androidx.work.ExistingWorkPolicy
|
import androidx.work.ExistingWorkPolicy
|
||||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||||
import fr.free.nrw.commons.R
|
import fr.free.nrw.commons.R
|
||||||
import fr.free.nrw.commons.WelcomeActivity
|
|
||||||
import fr.free.nrw.commons.auth.SessionManager
|
import fr.free.nrw.commons.auth.SessionManager
|
||||||
import fr.free.nrw.commons.bookmarks.BookmarkFragment
|
import fr.free.nrw.commons.bookmarks.BookmarkFragment
|
||||||
import fr.free.nrw.commons.contributions.ContributionsFragment.Companion.newInstance
|
import fr.free.nrw.commons.contributions.ContributionsFragment.Companion.newInstance
|
||||||
|
|
@ -33,7 +32,9 @@ import fr.free.nrw.commons.notification.NotificationActivity.Companion.startYour
|
||||||
import fr.free.nrw.commons.notification.NotificationController
|
import fr.free.nrw.commons.notification.NotificationController
|
||||||
import fr.free.nrw.commons.quiz.QuizChecker
|
import fr.free.nrw.commons.quiz.QuizChecker
|
||||||
import fr.free.nrw.commons.settings.SettingsFragment
|
import fr.free.nrw.commons.settings.SettingsFragment
|
||||||
|
import fr.free.nrw.commons.startWelcome
|
||||||
import fr.free.nrw.commons.theme.BaseActivity
|
import fr.free.nrw.commons.theme.BaseActivity
|
||||||
|
import fr.free.nrw.commons.utils.applyEdgeToEdgeAllInsets
|
||||||
import fr.free.nrw.commons.upload.UploadProgressActivity
|
import fr.free.nrw.commons.upload.UploadProgressActivity
|
||||||
import fr.free.nrw.commons.upload.worker.WorkRequestHelper.Companion.makeOneTimeWorkRequest
|
import fr.free.nrw.commons.upload.worker.WorkRequestHelper.Companion.makeOneTimeWorkRequest
|
||||||
import fr.free.nrw.commons.utils.ViewUtilWrapper
|
import fr.free.nrw.commons.utils.ViewUtilWrapper
|
||||||
|
|
@ -112,6 +113,7 @@ class MainActivity : BaseActivity(), FragmentManager.OnBackStackChangedListener
|
||||||
public override fun onCreate(savedInstanceState: Bundle?) {
|
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
binding = MainBinding.inflate(layoutInflater)
|
binding = MainBinding.inflate(layoutInflater)
|
||||||
|
applyEdgeToEdgeAllInsets(binding!!.root)
|
||||||
setContentView(binding!!.root)
|
setContentView(binding!!.root)
|
||||||
setSupportActionBar(binding!!.toolbarBinding.toolbar)
|
setSupportActionBar(binding!!.toolbarBinding.toolbar)
|
||||||
tabLayout = binding!!.fragmentMainNavTabLayout
|
tabLayout = binding!!.fragmentMainNavTabLayout
|
||||||
|
|
@ -151,21 +153,7 @@ after opening the app.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setUpPager()
|
setUpPager()
|
||||||
/**
|
|
||||||
* Ask the user for media location access just after login
|
|
||||||
* so that location in the EXIF metadata of the images shared by the user
|
|
||||||
* is retained on devices running Android 10 or above
|
|
||||||
*/
|
|
||||||
// if (VERSION.SDK_INT >= VERSION_CODES.Q) {
|
|
||||||
// ActivityCompat.requestPermissions(this,
|
|
||||||
// new String[]{Manifest.permission.ACCESS_MEDIA_LOCATION}, 0);
|
|
||||||
// PermissionUtils.checkPermissionsAndPerformAction(
|
|
||||||
// this,
|
|
||||||
// () -> {},
|
|
||||||
// R.string.media_location_permission_denied,
|
|
||||||
// R.string.add_location_manually,
|
|
||||||
// permission.ACCESS_MEDIA_LOCATION);
|
|
||||||
// }
|
|
||||||
checkAndResumeStuckUploads()
|
checkAndResumeStuckUploads()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -336,7 +324,7 @@ after opening the app.
|
||||||
)
|
)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.blockingGet()
|
.blockingGet()
|
||||||
Timber.d("Resuming " + stuckUploads.size + " uploads...")
|
Timber.d("Resuming %d uploads...", stuckUploads.size)
|
||||||
if (!stuckUploads.isEmpty()) {
|
if (!stuckUploads.isEmpty()) {
|
||||||
for (contribution in stuckUploads) {
|
for (contribution in stuckUploads) {
|
||||||
contribution.state = Contribution.STATE_QUEUED
|
contribution.state = Contribution.STATE_QUEUED
|
||||||
|
|
@ -517,7 +505,7 @@ after opening the app.
|
||||||
(!applicationKvStore!!.getBoolean("login_skipped"))
|
(!applicationKvStore!!.getBoolean("login_skipped"))
|
||||||
) {
|
) {
|
||||||
defaultKvStore.putBoolean("inAppCameraFirstRun", true)
|
defaultKvStore.putBoolean("inAppCameraFirstRun", true)
|
||||||
WelcomeActivity.startYourself(this)
|
startWelcome()
|
||||||
}
|
}
|
||||||
|
|
||||||
retryAllFailedUploads()
|
retryAllFailedUploads()
|
||||||
|
|
|
||||||
|
|
@ -45,10 +45,10 @@ class SetWallpaperWorker(context: Context, params: WorkerParameters) :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailureImpl(dataSource: DataSource<CloseableReference<CloseableImage>>?) {
|
override fun onFailureImpl(dataSource: DataSource<CloseableReference<CloseableImage?>?>) {
|
||||||
Timber.d("Error getting bitmap from image url %s", imageUrl.toString())
|
Timber.d("Error getting bitmap from image url %s", imageUrl.toString())
|
||||||
showNotification(context, "Setting Wallpaper Failed", "Failed to download image.")
|
showNotification(context, "Setting Wallpaper Failed", "Failed to download image.")
|
||||||
dataSource?.close()
|
dataSource.close()
|
||||||
}
|
}
|
||||||
}, CallerThreadExecutor.getInstance())
|
}, CallerThreadExecutor.getInstance())
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,11 @@ interface ImageSelectListener {
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* onLongPress
|
* Called when the user performs a long press on an image.
|
||||||
* @param imageUri : uri of image
|
*
|
||||||
|
* @param position The index of the pressed image in the list.
|
||||||
|
* @param images The list of all available images.
|
||||||
|
* @param selectedImages The currently selected images.
|
||||||
*/
|
*/
|
||||||
fun onLongPress(
|
fun onLongPress(
|
||||||
position: Int,
|
position: Int,
|
||||||
|
|
|
||||||
|
|
@ -8,15 +8,15 @@ sealed class CallbackStatus {
|
||||||
/**
|
/**
|
||||||
IDLE : The callback is idle , doing nothing.
|
IDLE : The callback is idle , doing nothing.
|
||||||
*/
|
*/
|
||||||
object IDLE : CallbackStatus()
|
data object IDLE : CallbackStatus()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
FETCHING : Fetching images.
|
FETCHING : Fetching images.
|
||||||
*/
|
*/
|
||||||
object FETCHING : CallbackStatus()
|
data object FETCHING : CallbackStatus()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
SUCCESS : Success fetching images.
|
SUCCESS : Success fetching images.
|
||||||
*/
|
*/
|
||||||
object SUCCESS : CallbackStatus()
|
data object SUCCESS : CallbackStatus()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,4 +39,11 @@ data class Folder(
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = bucketId.hashCode()
|
||||||
|
result = 31 * result + name.hashCode()
|
||||||
|
result = 31 * result + images.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package fr.free.nrw.commons.customselector.model
|
package fr.free.nrw.commons.customselector.model
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
|
||||||
|
|
@ -48,7 +49,12 @@ data class Image(
|
||||||
this(
|
this(
|
||||||
parcel.readLong(),
|
parcel.readLong(),
|
||||||
parcel.readString()!!,
|
parcel.readString()!!,
|
||||||
parcel.readParcelable(Uri::class.java.classLoader)!!,
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
parcel.readParcelable(Uri::class.java.classLoader, Uri::class.java)!!
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
parcel.readParcelable(Uri::class.java.classLoader)!!
|
||||||
|
},
|
||||||
parcel.readString()!!,
|
parcel.readString()!!,
|
||||||
parcel.readLong(),
|
parcel.readLong(),
|
||||||
parcel.readString()!!,
|
parcel.readString()!!,
|
||||||
|
|
@ -121,4 +127,16 @@ data class Image(
|
||||||
|
|
||||||
override fun newArray(size: Int): Array<Image?> = arrayOfNulls(size)
|
override fun newArray(size: Int): Array<Image?> = arrayOfNulls(size)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = id.hashCode()
|
||||||
|
result = 31 * result + bucketId.hashCode()
|
||||||
|
result = 31 * result + name.hashCode()
|
||||||
|
result = 31 * result + uri.hashCode()
|
||||||
|
result = 31 * result + path.hashCode()
|
||||||
|
result = 31 * result + bucketName.hashCode()
|
||||||
|
result = 31 * result + sha1.hashCode()
|
||||||
|
result = 31 * result + date.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import kotlinx.coroutines.MainScope
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import java.util.TreeMap
|
import java.util.TreeMap
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
|
|
@ -167,8 +168,7 @@ class ImageAdapter(
|
||||||
|
|
||||||
// Getting selected index when switch is off
|
// Getting selected index when switch is off
|
||||||
} else if (actionableImagesMap.size > position) {
|
} else if (actionableImagesMap.size > position) {
|
||||||
ImageHelper
|
ImageHelper.getIndex(selectedImages, ArrayList(actionableImagesMap.values)[position])
|
||||||
.getIndex(selectedImages, ArrayList(actionableImagesMap.values)[position])
|
|
||||||
|
|
||||||
// For any other case return -1
|
// For any other case return -1
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -326,12 +326,17 @@ class ImageAdapter(
|
||||||
|
|
||||||
// Getting clicked index from all images index when show_already_actioned_images
|
// Getting clicked index from all images index when show_already_actioned_images
|
||||||
// switch is on
|
// switch is on
|
||||||
|
if (singleSelection) {
|
||||||
|
// If single selection mode, clear previous selection and select only the new one
|
||||||
|
if (selectedImages.isNotEmpty() && (selectedImages[0] != images[position])) {
|
||||||
|
val prevIndex = images.indexOf(selectedImages[0])
|
||||||
|
selectedImages.clear()
|
||||||
|
notifyItemChanged(prevIndex, ImageUnselected())
|
||||||
|
}
|
||||||
|
}
|
||||||
val clickedIndex: Int =
|
val clickedIndex: Int =
|
||||||
if (showAlreadyActionedImages) {
|
if (showAlreadyActionedImages) {
|
||||||
ImageHelper.getIndex(selectedImages, images[position])
|
ImageHelper.getIndex(selectedImages, images[position])
|
||||||
|
|
||||||
// Getting clicked index from actionable images when show_already_actioned_images
|
|
||||||
// switch is off
|
|
||||||
} else {
|
} else {
|
||||||
ImageHelper.getIndex(selectedImages, ArrayList(actionableImagesMap.values)[position])
|
ImageHelper.getIndex(selectedImages, ArrayList(actionableImagesMap.values)[position])
|
||||||
}
|
}
|
||||||
|
|
@ -342,45 +347,41 @@ class ImageAdapter(
|
||||||
numberOfSelectedImagesMarkedAsNotForUpload--
|
numberOfSelectedImagesMarkedAsNotForUpload--
|
||||||
}
|
}
|
||||||
notifyItemChanged(position, ImageUnselected())
|
notifyItemChanged(position, ImageUnselected())
|
||||||
|
// Notify listener of deselection to update UI
|
||||||
// Getting index from all images index when switch is on
|
imageSelectListener.onSelectedImagesChanged(selectedImages, numberOfSelectedImagesMarkedAsNotForUpload)
|
||||||
val indexes =
|
|
||||||
if (showAlreadyActionedImages) {
|
|
||||||
ImageHelper.getIndexList(selectedImages, images)
|
|
||||||
|
|
||||||
// Getting index from actionable images when switch is off
|
|
||||||
} else {
|
|
||||||
ImageHelper.getIndexList(selectedImages, ArrayList(actionableImagesMap.values))
|
|
||||||
}
|
|
||||||
for (index in indexes) {
|
|
||||||
notifyItemChanged(index, ImageSelectedOrUpdated())
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (holder.isItemUploaded()) {
|
// Prevent adding the same image multiple times
|
||||||
Toast.makeText(context, R.string.custom_selector_already_uploaded_image_text, Toast.LENGTH_SHORT).show()
|
val image = if (showAlreadyActionedImages) images[position] else ArrayList(actionableImagesMap.values)[position]
|
||||||
} else {
|
if (selectedImages.contains(image)) {
|
||||||
if (holder.isItemNotForUpload()) {
|
return // Image already selected, ignore additional clicks
|
||||||
numberOfSelectedImagesMarkedAsNotForUpload++
|
}
|
||||||
}
|
scope.launch(ioDispatcher) {
|
||||||
|
val imageSHA1 = imageLoader.getSHA1(image, defaultDispatcher)
|
||||||
// Getting index from all images index when switch is on
|
withContext(Dispatchers.Main) {
|
||||||
val indexes: ArrayList<Int> =
|
if (holder.isItemUploaded()) {
|
||||||
if (showAlreadyActionedImages) {
|
Toast.makeText(context, R.string.custom_selector_already_uploaded_image_text, Toast.LENGTH_SHORT).show()
|
||||||
selectedImages.add(images[position])
|
return@withContext
|
||||||
ImageHelper.getIndexList(selectedImages, images)
|
|
||||||
|
|
||||||
// Getting index from actionable images when switch is off
|
|
||||||
} else {
|
|
||||||
selectedImages.add(ArrayList(actionableImagesMap.values)[position])
|
|
||||||
ImageHelper.getIndexList(selectedImages, ArrayList(actionableImagesMap.values))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (index in indexes) {
|
if (imageSHA1.isNotEmpty() && imageLoader.getFromUploaded(imageSHA1) != null) {
|
||||||
notifyItemChanged(index, ImageSelectedOrUpdated())
|
holder.itemUploaded()
|
||||||
|
Toast.makeText(context, R.string.custom_selector_already_uploaded_image_text, Toast.LENGTH_SHORT).show()
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!holder.isItemUploaded() && imageSHA1.isNotEmpty() && imageLoader.getFromUploaded(imageSHA1) != null) {
|
||||||
|
Toast.makeText(context, R.string.custom_selector_already_uploaded_image_text, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (holder.isItemNotForUpload()) {
|
||||||
|
numberOfSelectedImagesMarkedAsNotForUpload++
|
||||||
|
}
|
||||||
|
selectedImages.add(image)
|
||||||
|
notifyItemChanged(position, ImageSelectedOrUpdated())
|
||||||
|
imageSelectListener.onSelectedImagesChanged(selectedImages, numberOfSelectedImagesMarkedAsNotForUpload)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
imageSelectListener.onSelectedImagesChanged(selectedImages, numberOfSelectedImagesMarkedAsNotForUpload)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -626,4 +627,13 @@ class ImageAdapter(
|
||||||
* Returns the text for showing inside the bubble during bubble scroll.
|
* Returns the text for showing inside the bubble during bubble scroll.
|
||||||
*/
|
*/
|
||||||
override fun getSectionName(position: Int): String = images[position].date
|
override fun getSectionName(position: Int): String = images[position].date
|
||||||
}
|
|
||||||
|
private var singleSelection: Boolean = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set single selection mode
|
||||||
|
*/
|
||||||
|
fun setSingleSelection(single: Boolean) {
|
||||||
|
singleSelection = single
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -40,6 +40,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.view.ViewGroupCompat
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import fr.free.nrw.commons.R
|
import fr.free.nrw.commons.R
|
||||||
import fr.free.nrw.commons.customselector.database.NotForUploadStatus
|
import fr.free.nrw.commons.customselector.database.NotForUploadStatus
|
||||||
|
|
@ -56,6 +57,8 @@ import fr.free.nrw.commons.media.ZoomableActivity
|
||||||
import fr.free.nrw.commons.theme.BaseActivity
|
import fr.free.nrw.commons.theme.BaseActivity
|
||||||
import fr.free.nrw.commons.upload.FileUtilsWrapper
|
import fr.free.nrw.commons.upload.FileUtilsWrapper
|
||||||
import fr.free.nrw.commons.utils.CustomSelectorUtils
|
import fr.free.nrw.commons.utils.CustomSelectorUtils
|
||||||
|
import fr.free.nrw.commons.utils.applyEdgeToEdgeBottomPaddingInsets
|
||||||
|
import fr.free.nrw.commons.utils.applyEdgeToEdgeTopInsets
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
|
@ -104,7 +107,7 @@ class CustomSelectorActivity :
|
||||||
/**
|
/**
|
||||||
* Maximum number of images that can be selected.
|
* Maximum number of images that can be selected.
|
||||||
*/
|
*/
|
||||||
private val uploadLimit: Int = 20
|
private var uploadLimit: Int = 20
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flag that is marked true when the amount
|
* Flag that is marked true when the amount
|
||||||
|
|
@ -198,6 +201,9 @@ class CustomSelectorActivity :
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
ViewGroupCompat.installCompatInsetsDispatch(binding.root)
|
||||||
|
applyEdgeToEdgeTopInsets(toolbarBinding.toolbarLayout)
|
||||||
|
bottomSheetBinding.bottomLayout.applyEdgeToEdgeBottomPaddingInsets()
|
||||||
val view = binding.root
|
val view = binding.root
|
||||||
setContentView(view)
|
setContentView(view)
|
||||||
|
|
||||||
|
|
@ -207,6 +213,9 @@ class CustomSelectorActivity :
|
||||||
CustomSelectorViewModel::class.java,
|
CustomSelectorViewModel::class.java,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Check for single selection extra
|
||||||
|
uploadLimit = if (intent.getBooleanExtra(EXTRA_SINGLE_SELECTION, false)) 1 else 20
|
||||||
|
|
||||||
setupViews()
|
setupViews()
|
||||||
|
|
||||||
if (prefs.getBoolean("customSelectorFirstLaunch", true)) {
|
if (prefs.getBoolean("customSelectorFirstLaunch", true)) {
|
||||||
|
|
@ -610,8 +619,11 @@ class CustomSelectorActivity :
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* onLongPress
|
* Triggered when the user performs a long press on an image.
|
||||||
* @param imageUri : uri of image
|
*
|
||||||
|
* @param position The index of the selected image.
|
||||||
|
* @param images The list of all available images.
|
||||||
|
* @param selectedImages The list of images currently selected.
|
||||||
*/
|
*/
|
||||||
override fun onLongPress(
|
override fun onLongPress(
|
||||||
position: Int,
|
position: Int,
|
||||||
|
|
@ -638,17 +650,20 @@ class CustomSelectorActivity :
|
||||||
finishPickImages(arrayListOf())
|
finishPickImages(arrayListOf())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var i = 0
|
scope.launch(ioDispatcher) {
|
||||||
while (i < selectedImages.size) {
|
val uniqueImages = selectedImages.distinctBy { image ->
|
||||||
val path = selectedImages[i].path
|
CustomSelectorUtils.getImageSHA1(
|
||||||
val file = File(path)
|
image.uri,
|
||||||
if (!file.exists()) {
|
ioDispatcher,
|
||||||
selectedImages.removeAt(i)
|
fileUtilsWrapper,
|
||||||
i--
|
contentResolver
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
finishPickImages(ArrayList(uniqueImages))
|
||||||
}
|
}
|
||||||
i++
|
|
||||||
}
|
}
|
||||||
finishPickImages(selectedImages)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -722,6 +737,7 @@ class CustomSelectorActivity :
|
||||||
const val FOLDER_ID: String = "FolderId"
|
const val FOLDER_ID: String = "FolderId"
|
||||||
const val FOLDER_NAME: String = "FolderName"
|
const val FOLDER_NAME: String = "FolderName"
|
||||||
const val ITEM_ID: String = "ItemId"
|
const val ITEM_ID: String = "ItemId"
|
||||||
|
const val EXTRA_SINGLE_SELECTION: String = "EXTRA_SINGLE_SELECTION"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import fr.free.nrw.commons.databinding.FragmentCustomSelectorBinding
|
||||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
|
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
|
||||||
import fr.free.nrw.commons.media.MediaClient
|
import fr.free.nrw.commons.media.MediaClient
|
||||||
import fr.free.nrw.commons.upload.FileProcessor
|
import fr.free.nrw.commons.upload.FileProcessor
|
||||||
|
import fr.free.nrw.commons.utils.applyEdgeToEdgeBottomPaddingInsets
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -99,6 +100,7 @@ class FolderFragment : CommonsDaggerSupportFragment() {
|
||||||
selectorRV = binding?.selectorRv
|
selectorRV = binding?.selectorRv
|
||||||
loader = binding?.loader
|
loader = binding?.loader
|
||||||
with(binding?.selectorRv) {
|
with(binding?.selectorRv) {
|
||||||
|
this?.applyEdgeToEdgeBottomPaddingInsets()
|
||||||
this?.layoutManager = gridLayoutManager
|
this?.layoutManager = gridLayoutManager
|
||||||
this?.setHasFixedSize(true)
|
this?.setHasFixedSize(true)
|
||||||
this?.adapter = folderAdapter
|
this?.adapter = folderAdapter
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,8 @@ class ImageFileLoader(
|
||||||
if (file != null && file.exists() && name != null && path != null && bucketName != null) {
|
if (file != null && file.exists() && name != null && path != null && bucketName != null) {
|
||||||
val extension = path.substringAfterLast(".", "")
|
val extension = path.substringAfterLast(".", "")
|
||||||
// Check if the extension is one of the allowed types
|
// Check if the extension is one of the allowed types
|
||||||
if (extension.lowercase(Locale.ROOT) !in arrayOf("jpg", "jpeg", "png", "svg", "gif", "tiff", "webp", "xcf")) {
|
if (extension.lowercase(Locale.ROOT) !in arrayOf("jpg", "jpeg", "png", "svg",
|
||||||
|
"gif", "tiff", "webp", "xcf")) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ProgressBar
|
import android.widget.ProgressBar
|
||||||
import android.widget.Switch
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
|
@ -20,6 +19,7 @@ import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.google.android.material.switchmaterial.SwitchMaterial
|
||||||
import fr.free.nrw.commons.contributions.Contribution
|
import fr.free.nrw.commons.contributions.Contribution
|
||||||
import fr.free.nrw.commons.contributions.ContributionDao
|
import fr.free.nrw.commons.contributions.ContributionDao
|
||||||
import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao
|
import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao
|
||||||
|
|
@ -41,11 +41,13 @@ import fr.free.nrw.commons.media.MediaClient
|
||||||
import fr.free.nrw.commons.theme.BaseActivity
|
import fr.free.nrw.commons.theme.BaseActivity
|
||||||
import fr.free.nrw.commons.upload.FileProcessor
|
import fr.free.nrw.commons.upload.FileProcessor
|
||||||
import fr.free.nrw.commons.upload.FileUtilsWrapper
|
import fr.free.nrw.commons.upload.FileUtilsWrapper
|
||||||
|
import fr.free.nrw.commons.utils.applyEdgeToEdgeBottomPaddingInsets
|
||||||
import io.reactivex.schedulers.Schedulers
|
import io.reactivex.schedulers.Schedulers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
import java.util.TreeMap
|
import java.util.TreeMap
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
|
|
@ -80,7 +82,7 @@ class ImageFragment :
|
||||||
*/
|
*/
|
||||||
private var selectorRV: RecyclerView? = null
|
private var selectorRV: RecyclerView? = null
|
||||||
private var loader: ProgressBar? = null
|
private var loader: ProgressBar? = null
|
||||||
private var switch: Switch? = null
|
private var switch: SwitchMaterial? = null
|
||||||
lateinit var filteredImages: ArrayList<Image>
|
lateinit var filteredImages: ArrayList<Image>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -210,10 +212,18 @@ class ImageFragment :
|
||||||
savedInstanceState: Bundle?,
|
savedInstanceState: Bundle?,
|
||||||
): View? {
|
): View? {
|
||||||
_binding = FragmentCustomSelectorBinding.inflate(inflater, container, false)
|
_binding = FragmentCustomSelectorBinding.inflate(inflater, container, false)
|
||||||
imageAdapter =
|
|
||||||
ImageAdapter(requireActivity(), activity as ImageSelectListener, imageLoader!!)
|
// ensures imageAdapter is initialized
|
||||||
|
if (!::imageAdapter.isInitialized) {
|
||||||
|
imageAdapter = ImageAdapter(requireActivity(), activity as ImageSelectListener, imageLoader!!)
|
||||||
|
Timber.d("Initialized imageAdapter in onCreateView")
|
||||||
|
}
|
||||||
|
// Set single selection mode if needed
|
||||||
|
val singleSelection = (activity as? CustomSelectorActivity)?.intent?.getBooleanExtra(CustomSelectorActivity.EXTRA_SINGLE_SELECTION, false) == true
|
||||||
|
imageAdapter.setSingleSelection(singleSelection)
|
||||||
gridLayoutManager = GridLayoutManager(context, getSpanCount())
|
gridLayoutManager = GridLayoutManager(context, getSpanCount())
|
||||||
with(binding?.selectorRv) {
|
with(binding?.selectorRv) {
|
||||||
|
this?.applyEdgeToEdgeBottomPaddingInsets()
|
||||||
this?.layoutManager = gridLayoutManager
|
this?.layoutManager = gridLayoutManager
|
||||||
this?.setHasFixedSize(true)
|
this?.setHasFixedSize(true)
|
||||||
this?.adapter = imageAdapter
|
this?.adapter = imageAdapter
|
||||||
|
|
@ -365,7 +375,12 @@ class ImageFragment :
|
||||||
* notifyDataSetChanged, rebuild the holder views to account for deleted images.
|
* notifyDataSetChanged, rebuild the holder views to account for deleted images.
|
||||||
*/
|
*/
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
imageAdapter.notifyDataSetChanged()
|
if (::imageAdapter.isInitialized) {
|
||||||
|
imageAdapter.notifyDataSetChanged()
|
||||||
|
Timber.d("Notified imageAdapter in onResume")
|
||||||
|
} else {
|
||||||
|
Timber.w("imageAdapter not initialized in onResume")
|
||||||
|
}
|
||||||
super.onResume()
|
super.onResume()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -375,14 +390,19 @@ class ImageFragment :
|
||||||
* Save the Image Fragment state.
|
* Save the Image Fragment state.
|
||||||
*/
|
*/
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
imageAdapter.cleanUp()
|
if (::imageAdapter.isInitialized) {
|
||||||
|
imageAdapter.cleanUp()
|
||||||
|
Timber.d("Cleaned up imageAdapter in onDestroy")
|
||||||
|
} else {
|
||||||
|
Timber.w("imageAdapter not initialized in onDestroy, skipping cleanup")
|
||||||
|
}
|
||||||
|
|
||||||
val position =
|
val position =
|
||||||
(selectorRV?.layoutManager as GridLayoutManager)
|
(selectorRV?.layoutManager as? GridLayoutManager)
|
||||||
.findFirstVisibleItemPosition()
|
?.findFirstVisibleItemPosition() ?: -1
|
||||||
|
|
||||||
// Check for empty RecyclerView.
|
// check for valid position and non-empty image list
|
||||||
if (position != -1 && filteredImages.size > 0) {
|
if (position != -1 && filteredImages.isNotEmpty() && ::imageAdapter.isInitialized) {
|
||||||
context?.let { context ->
|
context?.let { context ->
|
||||||
context
|
context
|
||||||
.getSharedPreferences(
|
.getSharedPreferences(
|
||||||
|
|
@ -391,34 +411,57 @@ class ImageFragment :
|
||||||
)?.let { prefs ->
|
)?.let { prefs ->
|
||||||
prefs.edit()?.let { editor ->
|
prefs.edit()?.let { editor ->
|
||||||
editor.putLong("ItemId", imageAdapter.getImageIdAt(position))?.apply()
|
editor.putLong("ItemId", imageAdapter.getImageIdAt(position))?.apply()
|
||||||
|
Timber.d("Saved last visible item ID: %d", imageAdapter.getImageIdAt(position))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
Timber.d("Skipped saving item ID: position=%d, filteredImages.size=%d, imageAdapter initialized=%b",
|
||||||
|
position, filteredImages.size, ::imageAdapter.isInitialized)
|
||||||
}
|
}
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
_binding = null
|
_binding = null
|
||||||
|
selectorRV = null
|
||||||
|
loader = null
|
||||||
|
switch = null
|
||||||
|
progressLayout = null
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun refresh() {
|
override fun refresh() {
|
||||||
imageAdapter.refresh(filteredImages, allImages, getUploadingContributions())
|
if (::imageAdapter.isInitialized) {
|
||||||
|
imageAdapter.refresh(filteredImages, allImages, getUploadingContributions())
|
||||||
|
Timber.d("Refreshed imageAdapter")
|
||||||
|
} else {
|
||||||
|
Timber.w("imageAdapter not initialized in refresh")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes the image from the actionable image map
|
* Removes the image from the actionable image map
|
||||||
*/
|
*/
|
||||||
fun removeImage(image: Image) {
|
fun removeImage(image: Image) {
|
||||||
imageAdapter.removeImageFromActionableImageMap(image)
|
if (::imageAdapter.isInitialized) {
|
||||||
|
imageAdapter.removeImageFromActionableImageMap(image)
|
||||||
|
Timber.d("Removed image from actionable image map")
|
||||||
|
} else {
|
||||||
|
Timber.w("imageAdapter not initialized in removeImage")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clears the selected images
|
* Clears the selected images
|
||||||
*/
|
*/
|
||||||
fun clearSelectedImages() {
|
fun clearSelectedImages() {
|
||||||
imageAdapter.clearSelectedImages()
|
if (::imageAdapter.isInitialized) {
|
||||||
|
imageAdapter.clearSelectedImages()
|
||||||
|
Timber.d("Cleared selected images")
|
||||||
|
} else {
|
||||||
|
Timber.w("imageAdapter not initialized in clearSelectedImages")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -429,6 +472,15 @@ class ImageFragment :
|
||||||
selectedImages: ArrayList<Image>,
|
selectedImages: ArrayList<Image>,
|
||||||
shouldRefresh: Boolean,
|
shouldRefresh: Boolean,
|
||||||
) {
|
) {
|
||||||
|
if (::imageAdapter.isInitialized) {
|
||||||
|
imageAdapter.setSelectedImages(selectedImages)
|
||||||
|
if (shouldRefresh) {
|
||||||
|
imageAdapter.refresh(filteredImages, allImages, getUploadingContributions())
|
||||||
|
}
|
||||||
|
Timber.d("Passed %d selected images to imageAdapter, shouldRefresh=%b", selectedImages.size, shouldRefresh)
|
||||||
|
} else {
|
||||||
|
Timber.w("imageAdapter not initialized in passSelectedImages")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -438,6 +490,7 @@ class ImageFragment :
|
||||||
if (!progressDialog.isShowing) {
|
if (!progressDialog.isShowing) {
|
||||||
progressDialogLayout.progressDialogText.text = text
|
progressDialogLayout.progressDialogText.text = text
|
||||||
progressDialog.show()
|
progressDialog.show()
|
||||||
|
Timber.d("Showing mark/unmark progress dialog: %s", text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -447,6 +500,7 @@ class ImageFragment :
|
||||||
fun dismissMarkUnmarkProgressDialog() {
|
fun dismissMarkUnmarkProgressDialog() {
|
||||||
if (progressDialog.isShowing) {
|
if (progressDialog.isShowing) {
|
||||||
progressDialog.dismiss()
|
progressDialog.dismiss()
|
||||||
|
Timber.d("Dismissed mark/unmark progress dialog")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -456,4 +510,4 @@ class ImageFragment :
|
||||||
listOf(Contribution.STATE_IN_PROGRESS, Contribution.STATE_FAILED, Contribution.STATE_QUEUED, Contribution.STATE_PAUSED),
|
listOf(Contribution.STATE_IN_PROGRESS, Contribution.STATE_FAILED, Contribution.STATE_QUEUED, Contribution.STATE_PAUSED),
|
||||||
)?.subscribeOn(Schedulers.io())
|
)?.subscribeOn(Schedulers.io())
|
||||||
?.blockingGet() ?: emptyList()
|
?.blockingGet() ?: emptyList()
|
||||||
}
|
}
|
||||||
|
|
@ -4,11 +4,10 @@ import android.content.Context
|
||||||
import android.database.sqlite.SQLiteDatabase
|
import android.database.sqlite.SQLiteDatabase
|
||||||
import android.database.sqlite.SQLiteException
|
import android.database.sqlite.SQLiteException
|
||||||
import android.database.sqlite.SQLiteOpenHelper
|
import android.database.sqlite.SQLiteOpenHelper
|
||||||
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao
|
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable
|
||||||
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao
|
import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable
|
||||||
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao
|
|
||||||
import fr.free.nrw.commons.category.CategoryDao
|
import fr.free.nrw.commons.category.CategoryDao
|
||||||
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao
|
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable
|
||||||
import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao
|
import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -30,17 +29,17 @@ class DBOpenHelper(
|
||||||
*/
|
*/
|
||||||
override fun onCreate(db: SQLiteDatabase) {
|
override fun onCreate(db: SQLiteDatabase) {
|
||||||
CategoryDao.Table.onCreate(db)
|
CategoryDao.Table.onCreate(db)
|
||||||
BookmarkPicturesDao.Table.onCreate(db)
|
BookmarksTable.onCreate(db)
|
||||||
BookmarkItemsDao.Table.onCreate(db)
|
BookmarkItemsTable.onCreate(db)
|
||||||
RecentSearchesDao.Table.onCreate(db)
|
RecentSearchesTable.onCreate(db)
|
||||||
RecentLanguagesDao.Table.onCreate(db)
|
RecentLanguagesDao.Table.onCreate(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onUpgrade(db: SQLiteDatabase, from: Int, to: Int) {
|
override fun onUpgrade(db: SQLiteDatabase, from: Int, to: Int) {
|
||||||
CategoryDao.Table.onUpdate(db, from, to)
|
CategoryDao.Table.onUpdate(db, from, to)
|
||||||
BookmarkPicturesDao.Table.onUpdate(db, from, to)
|
BookmarksTable.onUpdate(db, from, to)
|
||||||
BookmarkItemsDao.Table.onUpdate(db, from, to)
|
BookmarkItemsTable.onUpdate(db, from, to)
|
||||||
RecentSearchesDao.Table.onUpdate(db, from, to)
|
RecentSearchesTable.onUpdate(db, from, to)
|
||||||
RecentLanguagesDao.Table.onUpdate(db, from, to)
|
RecentLanguagesDao.Table.onUpdate(db, from, to)
|
||||||
deleteTable(db, CONTRIBUTIONS_TABLE)
|
deleteTable(db, CONTRIBUTIONS_TABLE)
|
||||||
deleteTable(db, BOOKMARKS_LOCATIONS)
|
deleteTable(db, BOOKMARKS_LOCATIONS)
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ import fr.free.nrw.commons.upload.depicts.DepictsDao
|
||||||
*/
|
*/
|
||||||
@Database(
|
@Database(
|
||||||
entities = [Contribution::class, Depicts::class, UploadedStatus::class, NotForUploadStatus::class, ReviewEntity::class, Place::class, BookmarksCategoryModal::class, BookmarksLocations::class],
|
entities = [Contribution::class, Depicts::class, UploadedStatus::class, NotForUploadStatus::class, ReviewEntity::class, Place::class, BookmarksCategoryModal::class, BookmarksLocations::class],
|
||||||
version = 20,
|
version = 21,
|
||||||
exportSchema = false,
|
exportSchema = false,
|
||||||
)
|
)
|
||||||
@TypeConverters(Converters::class)
|
@TypeConverters(Converters::class)
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,6 @@ class DeleteHelper @Inject constructor(
|
||||||
media: Media?,
|
media: Media?,
|
||||||
reason: String?
|
reason: String?
|
||||||
): Single<Boolean>? {
|
): Single<Boolean>? {
|
||||||
|
|
||||||
if(context == null && media == null) {
|
if(context == null && media == null) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
@ -86,7 +85,6 @@ class DeleteHelper @Inject constructor(
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
private fun delete(media: Media, reason: String): Observable<Boolean> {
|
private fun delete(media: Media, reason: String): Observable<Boolean> {
|
||||||
Timber.d("thread is delete %s", Thread.currentThread().name)
|
|
||||||
val summary = "Nominating ${media.filename} for deletion."
|
val summary = "Nominating ${media.filename} for deletion."
|
||||||
val calendar = Calendar.getInstance()
|
val calendar = Calendar.getInstance()
|
||||||
val fileDeleteString = """
|
val fileDeleteString = """
|
||||||
|
|
|
||||||
|
|
@ -2,21 +2,19 @@ package fr.free.nrw.commons.delete
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
||||||
import fr.free.nrw.commons.utils.DateUtil
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
import javax.inject.Inject
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
import fr.free.nrw.commons.Media
|
import fr.free.nrw.commons.Media
|
||||||
import fr.free.nrw.commons.R
|
import fr.free.nrw.commons.R
|
||||||
import fr.free.nrw.commons.profile.achievements.FeedbackResponse
|
|
||||||
import fr.free.nrw.commons.auth.SessionManager
|
import fr.free.nrw.commons.auth.SessionManager
|
||||||
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
|
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
|
||||||
|
import fr.free.nrw.commons.utils.DateUtil
|
||||||
import fr.free.nrw.commons.utils.ViewUtilWrapper
|
import fr.free.nrw.commons.utils.ViewUtilWrapper
|
||||||
import io.reactivex.Single
|
import io.reactivex.Single
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.rx2.rxSingle
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.util.Locale
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class handles the reason for deleting a Media object
|
* This class handles the reason for deleting a Media object
|
||||||
|
|
@ -29,6 +27,8 @@ class ReasonBuilder @Inject constructor(
|
||||||
private val viewUtilWrapper: ViewUtilWrapper
|
private val viewUtilWrapper: ViewUtilWrapper
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
private val defaultFileUsagePageSize = 10
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* To process the reason and append the media's upload date and uploaded_by_me string
|
* To process the reason and append the media's upload date and uploaded_by_me string
|
||||||
* @param media
|
* @param media
|
||||||
|
|
@ -39,7 +39,7 @@ class ReasonBuilder @Inject constructor(
|
||||||
if (media == null || reason == null) {
|
if (media == null || reason == null) {
|
||||||
return Single.just("Not known")
|
return Single.just("Not known")
|
||||||
}
|
}
|
||||||
return fetchArticleNumber(media, reason)
|
return getAndAppendFileUsage(media, reason)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -54,27 +54,36 @@ class ReasonBuilder @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchArticleNumber(media: Media, reason: String): Single<String> {
|
private fun getAndAppendFileUsage(media: Media, reason: String): Single<String> {
|
||||||
return if (checkAccount()) {
|
return rxSingle(context = Dispatchers.IO) {
|
||||||
okHttpJsonApiClient
|
if (!checkAccount()) return@rxSingle ""
|
||||||
.getAchievements(sessionManager.userName)
|
|
||||||
.map { feedbackResponse -> appendArticlesUsed(feedbackResponse, media, reason) }
|
try {
|
||||||
} else {
|
val globalFileUsage = okHttpJsonApiClient.getGlobalFileUsages(
|
||||||
Single.just("")
|
fileName = media.filename,
|
||||||
|
pageSize = defaultFileUsagePageSize
|
||||||
|
)
|
||||||
|
val globalUsages = globalFileUsage?.query?.pages?.sumOf { it.fileUsage.size } ?: 0
|
||||||
|
|
||||||
|
appendArticlesUsed(globalUsages, media, reason)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "Error fetching file usage")
|
||||||
|
throw e
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes the uploaded_by_me string, the upload date, name of articles using images
|
* Takes the uploaded_by_me string, the upload date, no. of articles using images
|
||||||
* and appends it to the received reason
|
* and appends it to the received reason
|
||||||
* @param feedBack object
|
* @param fileUsages No. of files/articles using this image
|
||||||
* @param media whose upload data is to be fetched
|
* @param media whose upload data is to be fetched
|
||||||
* @param reason
|
* @param reason string to be appended
|
||||||
*/
|
*/
|
||||||
@SuppressLint("StringFormatInvalid")
|
@SuppressLint("StringFormatInvalid")
|
||||||
private fun appendArticlesUsed(feedBack: FeedbackResponse, media: Media, reason: String): String {
|
private fun appendArticlesUsed(fileUsages: Int, media: Media, reason: String): String {
|
||||||
val reason1Template = context.getString(R.string.uploaded_by_myself)
|
val reason1Template = context.getString(R.string.uploaded_by_myself)
|
||||||
return reason + String.format(Locale.getDefault(), reason1Template, prettyUploadedDate(media), feedBack.articlesUsingImages)
|
return reason + String.format(Locale.getDefault(), reason1Template, prettyUploadedDate(media), fileUsages)
|
||||||
.also { Timber.i("New Reason %s", it) }
|
.also { Timber.i("New Reason %s", it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import android.speech.RecognizerIntent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.activity.result.ActivityResult
|
import androidx.activity.result.ActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import fr.free.nrw.commons.CommonsApplication
|
import fr.free.nrw.commons.CommonsApplication
|
||||||
|
|
@ -20,9 +21,11 @@ import fr.free.nrw.commons.description.EditDescriptionConstants.WIKITEXT
|
||||||
import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao
|
import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao
|
||||||
import fr.free.nrw.commons.settings.Prefs
|
import fr.free.nrw.commons.settings.Prefs
|
||||||
import fr.free.nrw.commons.theme.BaseActivity
|
import fr.free.nrw.commons.theme.BaseActivity
|
||||||
|
import fr.free.nrw.commons.utils.applyEdgeToEdgeBottomInsets
|
||||||
import fr.free.nrw.commons.upload.UploadMediaDetail
|
import fr.free.nrw.commons.upload.UploadMediaDetail
|
||||||
import fr.free.nrw.commons.upload.UploadMediaDetailAdapter
|
import fr.free.nrw.commons.upload.UploadMediaDetailAdapter
|
||||||
import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog
|
import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog
|
||||||
|
import fr.free.nrw.commons.utils.applyEdgeToEdgeTopPaddingInsets
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.functions.Consumer
|
import io.reactivex.functions.Consumer
|
||||||
import io.reactivex.schedulers.Schedulers
|
import io.reactivex.schedulers.Schedulers
|
||||||
|
|
@ -87,6 +90,10 @@ class DescriptionEditActivity :
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
binding = ActivityDescriptionEditBinding.inflate(layoutInflater)
|
binding = ActivityDescriptionEditBinding.inflate(layoutInflater)
|
||||||
|
applyEdgeToEdgeBottomInsets(binding.btnEditSubmit)
|
||||||
|
WindowCompat.getInsetsController(window, window.decorView)
|
||||||
|
.isAppearanceLightStatusBars = false
|
||||||
|
binding.toolbar.applyEdgeToEdgeTopPaddingInsets()
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
val bundle = intent.extras
|
val bundle = intent.extras
|
||||||
|
|
@ -143,7 +150,7 @@ class DescriptionEditActivity :
|
||||||
this,
|
this,
|
||||||
getString(titleStringID),
|
getString(titleStringID),
|
||||||
getString(messageStringId),
|
getString(messageStringId),
|
||||||
getString(android.R.string.ok),
|
getString(R.string.ok),
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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