Compare commits

..

129 commits

Author SHA1 Message Date
Ritika Pahwa
63f621cb56
Update contributor list in README.md
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
2025-10-26 14:10:08 +05:30
Eric Pan
e81f916626
Part of issue #5996: Fix IDE warnings in ContributionsListFragment (#6542)
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
* Part of issue #5996: Fix IDE warnings in ContributionsListFragment (naming, null-safety, deprecations)

* Part of issue #5996: Clean final IDE warnings (parameter name alignment, remove redundant toggle)

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2025-10-26 08:30:17 +09:00
Ted
28fa7b1a20
Display specific, user-friendly error message when upload categories search API call returns an error (#6540)
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
* Make OkHttpConnectionFactory raise MwIOException when a non-suppressed API call returns an error

* Add AlertDialog displaying specific error message when categories search API call returns an error

* Add test for error alert dialog to UploadCategoriesFragment unit tests

* Add error handling when API call fails to CategoriesPresenter.onAttachViewWithMedia
2025-10-25 23:24:39 +09:00
translatewiki.net
aae9d4a387
Localisation updates from https://translatewiki.net.
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
2025-10-23 14:02:44 +02:00
Amir E. Aharoni
6873f63cf8
Remove an unused element from layout/fragment_media_detail.xml (#6536)
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
I noticed this issue years ago because it used a hard-to-understand
string that needed better documentation (see #688). I forgot it,
but recently, I started to explore the app much more deeply and
came back to it.

It looks like this string is only used in this layout element,
but the element itself is not used anywhere. It usage appears to
have been removed in #634.
2025-10-23 09:41:32 +09:00
Ritika Pahwa
2d0255e5fb
Disable hardware acceleration and keyboard animation (#6535)
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
* Disable hardware acceleration and keyboard animation

This is a temporary commit to see if it fixes issue #3364

* Remove unused import

* Bump up version code and modify version name

* Modify handleKeyboardInsets to handle insets correctly

* Refactor handleKeyboardInsets()

* Refactor handleKeyboardInsets()

* Fix inset in login activity
2025-10-22 16:44:06 +05:30
translatewiki.net
32ae406cca
Localisation updates from https://translatewiki.net.
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
2025-10-22 10:23:13 +02:00
translatewiki.net
3e04a1f036
Localisation updates from https://translatewiki.net.
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
2025-10-20 14:02:26 +02:00
translatewiki.net
6487191394
Localisation updates from https://translatewiki.net.
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
2025-10-20 06:29:31 +02:00
Amir E. Aharoni
beaf211f39
Fix three Java lint errors (#6531)
* Add braces to conditions.
* Remove an unnecessary semicolon.
* Remove an unnecessary constructor.

This fixes all the Java lint errors of these types.
2025-10-20 10:39:05 +09:00
Amir E. Aharoni
3549789cdf
Delete outdated localization files (#6533)
Strings files for he, id, yi were replaced with iw, in, and ji in 2016.
Those files cause build warnings, and they aren't used,
so it's OK to just remove them.
2025-10-20 10:18:13 +09:00
VoidRaven
def33552f9
Test/2819 add campaigns api tests (#6529)
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
* test:add mock JSON resource files for campaigns API responses

* feat:make campaign model fields mutable to allow for correct deserialization

* test:implement unit tests for fetching campaigns and fix DTO mocking logic

* test:implement unit tests for fetching campaigns and fix DTO mocking logic

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2025-10-19 22:58:14 +09:00
Amir E. Aharoni
3a55583460
Disable linting for icon hiding code in preferences (#6519)
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
2025-10-18 13:51:46 +09:00
Amir E. Aharoni
717a855149
Fix lint warning about Timber (#6521)
Change trivial string formatting and function calls
for Timber logging.

This resolves all the lint warnings in the
Android/Lint/Correctness/Messages group.
2025-10-18 13:45:35 +09:00
Amir E. Aharoni
29b6d0f8fe
Replace Switch with SwitchMaterial (#6522)
Lint recommended replacing Switch with SwitchMaterial.
This was a very simple replacement, and I tested it in
the custom selector, where it is used, and it works as
it worked previously.
2025-10-18 13:43:37 +09:00
Xinyu Yang
b5b5d8a8e4
I didn’t look at the code carefully before and directly modified the contents of strings.xml. After reviewing it, I found that the issue was actually in WikidataItemDetailsActivity.kt, where the wrong label was selected. After correcting this, there should no longer be any problems. (#6524)
Co-authored-by: frank <u7896083@anu.edu.au>
2025-10-18 11:31:49 +09:00
Aneesh Hebbar
714e5f8a4b
fix(i18n): Correct capitalization for 'Sending thanks' status messages (#6515) (#6518)
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
2025-10-17 11:40:48 +09:00
Dmitriy
7d96e94689
Fix crash for bookmarks without descriptions/thumbnails (#6488)
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2025-10-16 19:56:41 +09:00
Xinyu Yang
7a865df909
fix the bug of map reset (#6509)
Co-authored-by: Chengxu Yang <u7954427@anu.edu.au>
Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2025-10-16 19:34:17 +09:00
Amir E. Aharoni
864884e7b2
Update alternative texts for the welcome screen (#6512)
* Update alternative texts for the welcome screen

I've also updated their documentation for translators (qq)
in transltaewiki itself.

Resolves #689.

* Fixed typo

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2025-10-16 19:22:02 +09:00
Amir E. Aharoni
1ecaf09f21
Remove wikimedia_licenses.xml and files that use it (#6513)
This file doesn't seem to be used.

Resolves #6504.
2025-10-16 19:20:18 +09:00
Amir E. Aharoni
1ff2a28326
Replace tab with space in an XML layout file (#6514)
I was working on this file recently, and Android Studio
showed a warning that it has tabs instead of spaces,
so here's it's fixed.

A minor thing, but prevents distractions.
2025-10-16 19:19:30 +09:00
Amir E. Aharoni
b48905a153
Change all parameters to numbered parameters (#6516)
This will solve these errors:
"Format string is not a valid format string so it should not be passed to String.format"
2025-10-16 19:19:02 +09:00
Amir E. Aharoni
09c8d987e1
Simplify android:gravity in two layouts (#6506)
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
The "Inspect Code" linter complained that these two files
had Right-to-left text compatibility issues. I couldn't
really see any problems neither in English nor in Hebrew,
but the linter's suggestion still made sense, so I cleaned it up.

This fixes all the errors of the type
"Android Lint: Internationalization / Right-to-left text compatibility issues".
2025-10-15 13:52:05 +09:00
Amir E. Aharoni
2e52adbef8
Clean up empty tags in XML files (#6505)
This resolves all the "XML empty tags" lint errors.
2025-10-15 07:37:17 +09:00
Amir E. Aharoni
61c9de6fcc
Add a missing comma to a message (#6477)
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
There should be a comma before "etc." in a list,
and there already is a comma before "etc."
in the string depicts_search_text_hint, so it should be
in this string to for consistency.
2025-10-14 21:42:09 +09:00
Amir E. Aharoni
41d95814c9
Remove the string SingleWebViewActivity (#6494)
Resolves issue #6492.

Remove the title of a web activity and the accompanying
string resource.

This was not a real translatable message, but something that
looks more like an identifier that shouldn't be translated.
As far as I can tell, it's not seen anywhere in the interface
because the actual title is set in the code that calls it.
2025-10-14 21:41:31 +09:00
Amir E. Aharoni
c4cb65fc3c
Improve the grammar of messages about GPX and KML files (#6497)
Add articles, fix capitalization, add ellipses.
2025-10-14 21:40:38 +09:00
Amir E. Aharoni
a1c5974e93
Fix depicts and categories pickers for RTL languages (#6503)
This fixes the layouts to work in both left to right (LTR)
and right to left (RTL) languages.

Also replace two hard-coded strings in the depicts picker
with proper string resources.

Fixes #6502.
2025-10-14 17:54:54 +09:00
Amir E. Aharoni
0c244f369c
Replace android.R.string.* with R.string (#6499)
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
All these messages are not really necessary because
the app has its own localizations, and android.R.string
sometimes doesn't display the localized string.

Resolves #6470.
2025-10-13 21:54:32 +09:00
translatewiki.net
b6014b017c
Localisation updates from https://translatewiki.net. 2025-10-13 14:03:10 +02:00
Amir E. Aharoni
91ea4a6e7b
Rephrase images_featured_explanation (#6484)
Make the text of the panel consistent with its title.
The title is "Featured images", so the text should
use the same term.

Also move this resource next to the title, to make it easier
for the translators.
2025-10-13 18:43:59 +09:00
Amir E. Aharoni
1e51c4c5d0
Remove the arrow next to "Add location" (#6491)
This resolves #6489 using the "remove arrow" method.
2025-10-13 18:13:22 +09:00
Amir E. Aharoni
fbd28a0564
Change capitalization of "Add Location" (#6493)
This makes it consistent with "Edit Location" and
"Edit Image", which are used in the same screen.
2025-10-13 18:09:43 +09:00
Amir E. Aharoni
d0965206cd
Cleanup whitespace in the custom_selector_info_text2 string (#6496)
In the current state, it appears confusingly on translatewiki,
with a space in the beginning of a line.

This patch changes it to just two linebreaks.
2025-10-13 18:05:26 +09:00
Amir E. Aharoni
bb330c1771
Change "actioned" to "handled" in translatable strings (#6498)
"actioned" is not so standard in English as a verb.

"handled" sounds more appropriate.
2025-10-13 17:57:27 +09:00
Rohit Verma
14d6c80241
fix: remove location manager and update listener on pause (#6483)
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2025-10-11 23:33:44 +09:00
VoidRaven
4c621364c9
Fix/6404 app crashes theme change multi upload (#6429)
* Prevent IndexOutOfBoundsException in setUploadMediaDetails by validating index and list size (#6404)

* fixed UninitializedPropertyAccessException by safely initializing and accessing imageAdapter (#6404)

* fixed indexOutOfBoundsException by safely handling saved state and index in onViewCreated (#6404)

* resolve Unresolved reference by replacing setImageToBeUploaded with direct field assignments (#6404)

* Fix test compilation by removing obsolete testSetImageToBeUploaded and adding tha testInitializeFragmentWithUploadItem (#6404)

* Fix test compilation by removing testInitializeFragmentWithUploadItem with unresolved onImageProcessed (#6404)

* fix: test failures in UploadMediaDetailFragmentUnitTest by removing obsolete tests and initializing defaultKvStore (#6404)

* Fixed all the typos

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2025-10-11 21:38:07 +09:00
Amir E. Aharoni
2a9d5db51e
Consistent spelling of "screenshots" in the issue template (#6481)
"Screenshot" is written as one word without a hyphen everywhere
else in this app's code, and generally in the English language.
2025-10-11 21:33:54 +09:00
Amir E. Aharoni
b8d340fbe8
Rephrase the string copy_image_caption_description (#6472)
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
I was going over all the strings and documenting them (see #6457),
and I had a very hard time understand what this message does.
I read the code and finally figured it out. I added qq documentation
for it so now it's clearer, but I also think that the English
message can be clearer:
* "subsequent" changed to "the next" - shorter, easier word.
* "media" changed to "item" - "media" could mean a lot of things,
  and "item" is clearer in this context.
2025-10-11 14:54:40 +09:00
Amir E. Aharoni
dd1814c793
Change filename to username in toasts about sending thanks (#6467)
This fixes #6466.

Also fix the messages themselves a bit:
* Removed "successfully" from the success message. This word
  is usually redundant, because the message already says that it
  was done. (In MediaWiki, there's a specific convention about it:
  https://www.mediawiki.org/wiki/Help:System_message#Avoid_jargon_and_slanghttps://www.mediawiki.org/wiki/Help:System_message#Avoid_jargon_and_slang
)
* Added a missing preposition to the failure message.

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2025-10-11 14:53:57 +09:00
VoidRaven
adb6181e9f
fix: map crash (fixes #6432) (#6479)
* fix: map crash (fixes #6432)

* Fix typos in comments in ExploreMapFragment.kt

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2025-10-11 14:38:07 +09:00
Jason-Whitmore
0a4b179db5
Fixes Issue 6436: getString(...) must not be null (#6474)
* DatabaseUtils.kt: change getString() to allow null returns

Before this change, a call to getString() would assume that the specified column
name actually exists. A bad String input would cause a null value to be returned
to getString(), which would then throw a NPE because getString() can only return
non null Strings.

This change expands the getString() method to check if the column name exists.
If it does exist, the String is retrieved normally. Else, a null value is
returned. The method signature is changed to allow null return values.

* *Dao.kt: change some usages of getString()

Before this change, the getString() method in DatabaseUtils.kt was changed
to allow returning a null value upon method failure. All usages of getString()
were not changed.

This change updates all usages of getString() which require non null return
values. If null is returned, an empty string is used instead.

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2025-10-11 14:12:19 +09:00
Amir E. Aharoni
e78db7fa08
Remove the unused message "statistics" (#6478)
Its usage was removed from the file
app/src/main/res/layout/fragment_achievements.xml in a8387f0,
but the message remained in the strings file.

Resolves #6456.
2025-10-11 13:58:19 +09:00
Amir E. Aharoni
7be615bacb
Fix comma splice in a translatable string (#6465)
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
2025-10-10 23:46:17 +09:00
Amir E. Aharoni
95d58023c7
Fix punctuation in the string download_failed_we_cannot_download_the_file_without_storage_permission (#6473)
The double exclamation point is really unnecessary.
2025-10-10 23:26:30 +09:00
Amir E. Aharoni
7b8fbc239b
Remove commented out code and associated strings (#6475)
As I was documenting undocumented strings (see #6457), I noticed
that two messages are only used once in a few lines of code that
were commented out in 2023.

To clean up the messages, I am removing them from the strings
list and deleting the commented-out code.
2025-10-10 23:25:46 +09:00
Amir E. Aharoni
30d1107cef
Change "wikicode" to "wikitext" in a message (#6476)
The usual English term is "wikitext". "Wikicode" is used in French
and perhaps some other language, but English uses "wikitext".
2025-10-10 23:05:19 +09:00
Amir E. Aharoni
fe16c44caa
Change Android "OK" string to app's own localization (#6471)
Addressed one instance described in #6470.
2025-10-10 22:40:00 +10:00
translatewiki.net
4ed9ad5085
Localisation updates from https://translatewiki.net.
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
2025-10-09 14:02:46 +02:00
Amir E. Aharoni
755d8311dc
Make some hardcoded strings translatable (#6459)
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
2025-10-09 10:07:19 +09:00
Amir E. Aharoni
b6457cc6b9
Rename an identifier with a non-ASCII character (#6460)
Android Studio reported that there's an identifier
with a non-ASCII letter. It was nearbyFılterStateInstance,
with a Turkish dotless i. I renamed to ASCII dotted i.

This brings the number of Internationalization
issues in Inspect Code to zero.
2025-10-09 10:06:23 +09:00
Rohit Verma
2d51a7ce9a
chore: upgrade native libraries for 16KB page size compatibility (#6445)
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
* chore: bump maplibre-native for 16KB page size compatibility

Also, bump AGP

* chore: bump freso for 16KB page size compatibility and fix build issues

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
Co-authored-by: Ritika Pahwa <83745993+RitikaPahwa4444@users.noreply.github.com>
2025-10-08 23:25:20 +09:00
Amir E. Aharoni
0ade0705e2
Remove leading space from English messages (#6449)
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
2025-10-06 08:23:00 +09:00
Ben
6bc25ccd9b
Fix kotling warnings for Image.kt and Folder.kt (part of Issue #5996) (#6441)
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
* added hash code to folder.kt and image.kt to pair with equals

* fixed deprecation in readParcelable function

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2025-10-05 22:15:39 +09:00
Amir E. Aharoni
ed7007fc8c
Change a hardcoded string to a translatable message (#6444)
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
Follow-up to #6443. Noticed this one after that pull request
was already merged.
2025-10-04 14:10:00 +09:00
Amir E. Aharoni
71ad6a2ce5
Change hardcoded preferences strings to translatable messages (#6443) 2025-10-04 10:43:54 +09:00
Amir E. Aharoni
e9a1af0f52
Change hardcoded strings in the language search dialog to messages (#6440)
Another comment: While working on this, I also noticed that
"Recent Searches" is hardcoded in the XML file, and
I'm not sure where does it actually appear. I fixed it, too,
but perhaps it can be completely removed.

Fixes #6439.
2025-10-04 10:43:10 +09:00
translatewiki.net
10c384ffa7
Localisation updates from https://translatewiki.net.
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
2025-10-02 14:02:46 +02:00
VoidRaven
4e51977fb6
Fix Location Permission Prompt on "Uploaded via Mobile" Tab (#6425)
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
* Added location prompt at Map screen

* Update menu visibility logic to reflect Map tab selection

* Fix location prompt by deferring permission request to Map tab visibility

* Fix: Restrict location permission prompt to Map tab in Explore section
2025-10-02 09:44:37 +09:00
translatewiki.net
d632c268ae
Localisation updates from https://translatewiki.net.
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
2025-09-29 14:02:35 +02:00
translatewiki.net
be371e5236
Localisation updates from https://translatewiki.net.
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
2025-09-25 14:03:06 +02:00
Rickey H.
25d3068faf
added padding inset for mapview (#6427)
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
2025-09-24 23:18:02 +09:00
translatewiki.net
179c7c1855
Localisation updates from https://translatewiki.net.
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
2025-09-18 14:02:55 +02:00
translatewiki.net
8018000584
Localisation updates from https://translatewiki.net.
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
2025-09-15 14:02:29 +02:00
VoidRaven
657af4fe04
Fix #6409: Add listener call in ImageAdapter to update UI and upload (#6420)
* Fix #6409: Add listener call in ImageAdapter to update UI and upload button on deselection

* Fix image deselection issue in ImageAdapter to update UI correctly (#6409)

* Prevent duplicate image selections on multiple taps in ImageAdapter when showAlreadyActionedImages is off (#6409)

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2025-09-15 18:14:59 +09:00
translatewiki.net
219fcd3dd8
Localisation updates from https://translatewiki.net.
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
2025-09-11 14:02:52 +02:00
LeopoldoDelgadillo
2e9726b84f
Added VISIBLE flag to descriptionEdit inside onResume function at MediaDetailFragment.kt (#6421)
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2025-09-10 22:56:26 +09:00
translatewiki.net
64c6b0c8d0
Localisation updates from https://translatewiki.net.
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
2025-09-08 14:01:58 +02:00
Ritika Pahwa
fcc63b9f09 Add v6.0.2 to CHANGELOG.md
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
2025-09-07 13:13:46 +05:30
Ritika Pahwa
a283ffe2bc Bump up version code and name for the patch release (v6.0.2) 2025-09-07 13:05:55 +05:30
Rohit Verma
2811b181b7
Fix: enable H/W acceleration for UploadActivity to resolve keyboard not showing on Upload Screen (#6418)
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
* fix: enable h/w acceleration for UploadActivity to resolve ime issue

* fix(upload): handle keyboard insets for bottom buttons at Depicts step

* fix(upload): handle keyboard insets for buttons at select category step

* fix(upload): hide keyboard before navigating to Media License screen

This solves keyboard opened at the License screen issue, if we proceed by pressing next at the Upload Categories screen when the keyboard is opened
2025-09-07 00:35:47 +05:30
Jason-Whitmore
730f314200
Fixes Issue #6384: java.lang.NullPointerException in ReviewActivity (#6394)
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
* activity_review.xml: add new GUI elements to replace old ones

Before this commit, the info icon shared the same GUI element with the "Skip this image" text.
This made the Kotlin code to handle taps on the info icon difficult to write, and would crash
with a NPE when the user used a language that is read right to left and the info icon was pressed.

This commit creates new GUI elements. Notably, the info icon has it's own element. A LinearLayout
is used to place the skip button and the info icon button together. Kotlin code can now be
simplified and the NPE bug can be fixed.

* ReviewActivity.kt: simplify info icon code to work with some languages

Before this commit, if the language was set to a language that is read right to left,
pressing the info icon would crash the app with a NPE. This was because the Kotlin
code assumed that the icon would always be on the right of the skip button
(index 2 in the drawable array). When a right to left language was used, the icon
would be on the left and index 2 would be null.

This commit builds upon prior GUI changes. The info icon now has its own button.
Kotlin changes now remove the use of the drawable array to find the info icon and
instead directly references the new info icon button. The info icon button now works
properly for both left-to-right and right-to-left languages while maintaining correct
positioning.

* activity_review.xml: fix xml to be more readable

This commit moves around some lines in the XML to make it more readable.

* activity_review.xml: change button configuration

This change simplifies the button configuration XML and makes the info icon button slightly smaller

---------

Co-authored-by: Ritika Pahwa <83745993+RitikaPahwa4444@users.noreply.github.com>
Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2025-09-05 15:36:47 +09:00
translatewiki.net
81da5c9a1a
Localisation updates from https://translatewiki.net.
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
2025-09-04 14:02:19 +02:00
VoidRaven
a59bf64677
Added the wiki prefix to titles in GlobalFileUsage for issue #6416 (#6417) 2025-09-04 19:47:53 +09:00
VoidRaven
e2c8f85a5b
Make "File usages" items clickable with correct URLs #6307 (#6405)
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
* Fix: URL generation for GlobalFileUsage in FileUsagesUiModel.kt for issue #6307

* Add clickable functionality to 'Usages on Other Wikis' in FileUsagesContainer for issue #6307

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2025-09-02 17:03:28 +09:00
translatewiki.net
dd96c64182
Localisation updates from https://translatewiki.net.
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
2025-09-01 14:02:53 +02:00
Ritika Pahwa
9ba702eaa9
Add v6.0.1 to CHANGELOG.md
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
2025-08-30 12:41:52 +05:30
Ritika Pahwa
296b4c1f52 Bump up version code and name for the patch release (v6.0.1) 2025-08-30 12:34:47 +05:30
Chris Danis
48e7effd0a
fix: add User-Agent to NetworkingModule http client (#6415)
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
* fix: add User-Agent to NetworkingModule http client

* CommonHeaderRequestInterceptor: publicize

* fix import & style
2025-08-30 07:52:08 +09:00
translatewiki.net
b9f353bb5a
Localisation updates from https://translatewiki.net.
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
2025-08-28 14:02:26 +02:00
translatewiki.net
c22e8447b3
Localisation updates from https://translatewiki.net.
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
2025-08-25 14:03:00 +02:00
Ritika Pahwa
f810a2d49b Bump up version code to 1056 for v6.0.0 release
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
2025-08-23 12:37:19 +05:30
Rohit Verma
4f3f7b97fd
fix: use context instead of requireContext() for backward compatibility (#6403)
It fixes crash when opening certain screens like Contribution Details, Bookmark, etc. on lower Android versions

Co-authored-by: Ritika Pahwa <83745993+RitikaPahwa4444@users.noreply.github.com>
2025-08-23 12:33:30 +05:30
Rohit Verma
718c466505
Bump target sdk to API 35 and make the app UI compatible with edge to edge (#6393)
* chore: upgrade target SDK and refactor function signatures to resolve build issues

* chore: bump android gradle plugin version

* chore(ui): add extension functions for applying edge to edge insets

* fix: apply system bar top and bottom insets for edge to edge

* fix: force edge to edge for backward compatibility and consistent UI

* fix: apply top bar insets as padding and make the status bar color white

Since the toolbars have primary color as bg, we should make the status bar white

* chore: bump robolectric version for API 35 compatibility

* fix: preserve existing margins when adding new insets

* feat(customselector): improve RecyclerView edge-to-edge inset handling

It allows the last item to sits above the navigation bar while preserving edge-to-edge appearance.

* feat(notification): improve RecyclerView edge-to-edge insets handling

Also, refactor LocationPicker and DescriptionEdit activities to use extension functions and reduce duplication

* fix(quiz): enable and handle edge-to-edge insets and status icon colors

* fix: bottom insets not dispatched on all API versions consistently

Upgraded core-ktx version installCompatInsetsDispatch wasn't available on current version

* fix: return fallback value when versionName is null

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix: resolve compilation errors

* docs: add KDoc for edge-to-edge insets utility functions

* fix(SearchActivity): apply insets for system bars

* fix(util): add utility function to handle keyboard insets with animation

* fix(upload): handle keyboard insets for upload media detail card view

* fix(login): hadle IME insets and make edge-to-edge backward compatible

---------

Co-authored-by: Ritika Pahwa <83745993+RitikaPahwa4444@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-23 12:27:37 +05:30
translatewiki.net
b8a558303b
Localisation updates from https://translatewiki.net. 2025-08-21 14:02:13 +02:00
Ritika Pahwa
a892aa6dee
6357: Fix java.lang.SecurityException for multi-uploads (#6402)
* Fix java.lang.SecurityException for ACTION_OPEN_DOCUMENT

* Handle SecurityException in case of multi-upload

* Remove unused import

* Clean up code

* Clean up code

* Handle SecurityException for other upload methods

* Release persisted URI permissions for successful uploads

* Remove persistable permission for custom picker as it's not required

* Remove persistable permission for in-app camera as it's not required
2025-08-20 01:46:02 +10:00
translatewiki.net
5a6b3cbf09
Localisation updates from https://translatewiki.net. 2025-08-18 14:02:18 +02:00
Rohit Verma
5bdfbf5f6f
fix: NPE when changing theme while on profile screen (#6398) 2025-08-16 15:06:57 +05:30
translatewiki.net
1d7d2801e4
Localisation updates from https://translatewiki.net. 2025-08-14 14:01:59 +02:00
translatewiki.net
5201af70cd
Localisation updates from https://translatewiki.net. 2025-08-11 14:01:53 +02:00
translatewiki.net
d0e95bc3c2
Localisation updates from https://translatewiki.net. 2025-08-07 14:02:05 +02:00
Sonal Yadav
ffb9af1f1c
Support both "label" and "itemLabel" for NearbyResultItem mapping (#6386)
* Support both label and itemLabel for robust NearbyResultItem mapping.

* fix code style

* Add getOriginalLabel() for Wikidata edits to avoid fallback issues with itemLabel

* Fix Wikidata edit failure by resetting hasInvalidLocation flag on upload confirmation

---------

Co-authored-by: Sonal Yadav <sonalyadav@Sonals-MacBook-Air.local>
2025-08-05 13:13:40 +09:00
translatewiki.net
6dcce45c59
Localisation updates from https://translatewiki.net. 2025-08-04 14:02:18 +02:00
Paul Hawke
6f36cae767
Convert explore package to kotlin (#6389)
* Convert WikidataItemDetailsActivity to kotlin

* Convert RecentSearchesDao to kotlin

* Convert RecentSearchesFragment to kotlin

* Convert ExploreListRootFragment to kotlin

* Convert the ParentViewPager to kotlin

* Convert ExploreMapRootFragment to kotlin

* Convert SearchActivity to kotlin

* Convert ExploreFragment to kotlin

* Convert ExploreMapCalls and ExploreMapContract to kotlin

* Convert ExploreMapController to kotlin

* Convert the map presenter to kotlin

* Convert the ExploreMapFragment to kotlin

* Fix import issue
2025-08-04 11:44:00 +09:00
Ritika Pahwa
516039c91d
Add v5.6.1 to CHANGELOG.md 2025-08-02 12:34:34 +05:30
Paul Hawke
8de57304bf
Convert bookmarks package to kotlin (#6387)
* Convert BookmarkItemsController to kotlin

* Split BookmarkItemsDao apart and converted to Kotlin

* Convert and cleanup content providers

* Convert BookmarkItemsFragment to kotlin

* Convert BookmarkPicturesFragment to kotlin

* Convert BookmarkPicturesDao to kotlin and share some useful DB methods

* Convert BookmarkPicturesController to kotlin

* Convert BookmarkFragment to kotlin

* Convert BookmarksPagerAdapter to kotlin

* Convert BookmarkListRootFragment to kotlin
2025-08-01 08:26:16 +09:00
translatewiki.net
869371b485
Localisation updates from https://translatewiki.net. 2025-07-31 14:02:22 +02:00
translatewiki.net
929711da98
Localisation updates from https://translatewiki.net. 2025-07-28 14:01:44 +02:00
Ritika Pahwa
b2816e1459 Bump up version code to 1055 for v5.6.1 release
Revert SPARQL optimisation
2025-07-26 12:32:54 +05:30
Ritika Pahwa
532bd8baa6 Revert "Optimise SPARQL query for single entity metadata using wikibase:label (#6376)"
This reverts commit e5dbcfc2a1.
2025-07-26 12:32:48 +05:30
Sonal Yadav
90ab7a2766
Correct NearbyResultItem label mapping for place name display (#6382)
* Fix  Nearby place name missing in Nearby Place Found popup

* minor change

---------

Co-authored-by: Sonal Yadav <sonalyadav@Sonals-MacBook-Air.local>
2025-07-25 23:35:14 +09:00
translatewiki.net
ee33a9350f
Localisation updates from https://translatewiki.net. 2025-07-24 14:02:07 +02:00
translatewiki.net
f1e6f1ad31
Localisation updates from https://translatewiki.net. 2025-07-21 14:02:00 +02:00
Ritika Pahwa
11e3e37263 Bump up version code to 1054 for v5.6.0 release 2025-07-19 14:46:01 +05:30
translatewiki.net
da694022ac
Localisation updates from https://translatewiki.net.
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
2025-07-17 14:02:00 +02:00
VoidRaven
29ade1e5b7
Fix email verification input (#6367)
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
* Set imeOptions to actionDone and enforce singleLine for xlarge

* Set imeOptions to actionDone and enforce singleLine for landscape

* Set imeOptions to actionDone and enforce singleLine

* Update askUserForTwoFactorAuth for IME_ACTION_DONE to trigger performLogin

* Fix email verification: Set imeOptions to actionDone and replace singleLine with maxLines=1

* Fix email verification: Set imeOptions to actionDone and replace singleLine with maxLines=1

* Fix email verification: Set imeOptions to actionDone and replace singleLine with maxLines=1

* feat: Set imeOptions to actionNext for login_password to improve focus transition

* feat: Enhance keyboard visibility for email verification code input
2025-07-17 09:41:28 +09:00
Shravya K Suresh
88565b70c5
updated the strange wording (#6378)
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
2025-07-16 23:11:21 +09:00
Sonal Yadav
e5dbcfc2a1
Optimise SPARQL query for single entity metadata using wikibase:label (#6376)
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
* Optimize SPARQL query for single entity metadata using wikibase:label service

- Use SERVICE wikibase:label for efficient retrieval of labels and descriptions in preferred language
- Remove redundant label/description fetching logic
- Prevent Cartesian product and improve query performance for

* appeded all possible Wikidata languages

* Remove duplicate 'en'

* Update query_for_item.rq

* formatting, comments

---------

Co-authored-by: Sonal Yadav <sonalyadav@Sonals-MacBook-Air.local>
Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2025-07-16 17:22:10 +09:00
Copilot
0cda8e4d70
Show author/uploader names in Media Details for Commons licensing compliance (#6375)
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
* Initial plan

* Implement author/uploader display in Media Details

Co-authored-by: nicolas-raoul <99590+nicolas-raoul@users.noreply.github.com>

* Enhanced implementation to use getAttributedAuthor() for better attribution coverage

Co-authored-by: nicolas-raoul <99590+nicolas-raoul@users.noreply.github.com>

* Move Author/Uploader fields to be positioned after License field

Co-authored-by: nicolas-raoul <99590+nicolas-raoul@users.noreply.github.com>

* Remove unnecessary comment lines as requested

Co-authored-by: nicolas-raoul <99590+nicolas-raoul@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: nicolas-raoul <99590+nicolas-raoul@users.noreply.github.com>
2025-07-15 23:42:55 +09:00
translatewiki.net
7500b6d374
Localisation updates from https://translatewiki.net.
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
2025-07-14 14:01:46 +02:00
Ritika Pahwa
a4c7a9c4f7
Fix java.lang.SecurityException for ACTION_OPEN_DOCUMENT (#6370)
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
2025-07-14 08:56:35 +05:30
Paul Hawke
8fc7e1039b
Convert media package to kotlin (#6369)
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
* Convert Caption to kotlin

* Convert CaptionListViewAdapter to kotlin

* Convert CaptionListViewAdapter to kotlin

* Removed unused class

* Converted MwParseResult / MwParseResponse to kotlin

* Convert CustomOkHttpNetworkFetcher to kotlin

* Break up MediaDetailPagerFragment to make it easier to convert to kotlin

* Convert MediaDetailProvider to kotlin

* Convert the MediaDetailAdapter to kotlin

* Convert MediaDetailPagerFragment to kotlin
2025-07-12 11:11:20 +09:00
translatewiki.net
79f52db929
Localisation updates from https://translatewiki.net. 2025-07-10 14:02:16 +02:00
translatewiki.net
13048cc2fd
Localisation updates from https://translatewiki.net. 2025-07-07 14:01:56 +02:00
Paul Hawke
66395b9871
convert top level classes to kotlin (#6368)
* Converted welcome activity / pager to kotlin

* Removed unused interface

* Convert ViewPagerAdapter to kotlin and enforce that all tabs must have a title that comes from strings.xml

* Convert OkHttpConnectionFactory and remove an exception class nobody was using

* Convert MapController to kotlin along with fixing nullability in a few places
2025-07-07 09:50:16 +09:00
VoidRaven
65f41beed8
Update bug-report.yml to use Type:bug label as per issue #6356 (#6363)
* Update bug-report.yml to use Type:bug label as per issue #6356

* Update bug-report.yml to use type: bug and labels: ['Type: bug'] as per issue #6356

* Update bug-report.yml to use type: Bug and revert labels to ['bug'] as per mentor's feedback for issue #6356

* Remove labels field from bug-report.yml as per feedback for issue #6356

---------

Co-authored-by: Ritika Pahwa <83745993+RitikaPahwa4444@users.noreply.github.com>
2025-07-05 11:35:54 +05:30
Sonal Yadav
f98b49608e
fix popup from appearing in nearby (#6359)
Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2025-07-05 13:15:57 +09:00
Paul Hawke
3bd0ec4466
Convert top level "Utils" class to kotlin (#6364)
* Unused class removed

* Convert BasePresenter to kotlin

* Removed redundent class

* Move the Utils class into the utils package

* Inline the creation of a page title object

* Move license utilities into their own file

* Inline app rating since its only ever used in 1 place

* Moved GeoCoordinates utilities into their own class

* Moved Monuments related utils into their own class

* Moved screen capture into its own util class

* Moved handleWebUrl to its own utility class

* Moved fixExtension to its own class

* Moved clipboard copy into its own utility class

* Renames class to match remaining utility method

* Convert UnderlineUtils to kotlin

* Converted the copy-to-clipboard utility to kotlin

* Converted license name and url lookup to kotlin

* Converted fixExtension to kotlin

* Convert handleGeoCoordinates to kotlin

* Monument utils converted to kotlin

* Convert then inline screeen capture in kotlin

* Convert handleWebUrl to kotlin
2025-07-04 20:18:52 +09:00
translatewiki.net
4befff8f42
Localisation updates from https://translatewiki.net. 2025-07-03 14:02:08 +02:00
Ritika Pahwa
89436b0a75
Add v5.5.0 to CHANGELOG.md 2025-07-01 11:52:08 +05:30
translatewiki.net
6de5a07e0d
Localisation updates from https://translatewiki.net. 2025-06-30 14:01:39 +02:00
translatewiki.net
27b9d70333
Localisation updates from https://translatewiki.net. 2025-06-29 20:27:27 +02:00
Jason-Whitmore
9a94dc2548
Fixes Issue 6312: GPS has huge error and does not update (in Nearby) (#6352)
* NearbyParentFragment.kt: add helper methods for user location overlays and accuracy data.

Before this commit, the code used to create the user location overlays was in multiple places.
Additionally, there was no easy way to access the location accuracy.

This commit places the user location overlay creation code into helper methods, as well as adding
a new location accuracy getter method. These methods can now be used to refactor other parts of the file.

* NearbyParentFragment.kt: create method to update user location overlays

Before this commit, there was no easy way to update the user location overlays.

This commit adds the updateUserLocationOverlays() method, which will properly replace
the old user location overlays with new ones. It will also add the overlays if they
do not already exist.

* NearbyParentFragment.kt: replace old code with calls to updateUserLocationOverlays()

This commit completes the refactor and fixes the issue of the user overlays not
updating. The new method updateUserLocationOverlays is called to refactor and simplify
old code.

* Removal of file is not related to the issue, but is needed for project to compile and run.

* NearbyParentFragment.kt: fix bug where multiple user location overlays would appear

Before this commit, the user could see multiple user location overlays if they paused the app and reopened it when
there are no Places/pins on the map. This was caused by a linear search failing to identify the target overlay
because it compared Drawables between two Overlays, which was unreliable.

This commit contains a better solution for replacing existing user location overlays by adding 2 instance variables
to keep track of the overlays. The position of these overlays in the overlay list can then be found by using indexOf()
with these instance variables rather than the linear search that was implemented before. Some refactoring was also done.

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2025-06-29 18:30:10 +09:00
translatewiki.net
b1a8308aaf
Localisation updates from https://translatewiki.net. 2025-06-26 14:02:13 +02:00
Rohit Verma
ad7dddaac4
Fix infinite loading circular progress bar after nominating for deletion (#6324)
* fix: infinite loading progress bar after nominating for deletion

* add logs for testing

* refactor: use globalFileUsage instead of achievement to append in reason

Fetching achievements is a time consuming operation and globalFileUsage gives the similar result in optimal time

* test(ReasonBuilder): fix tests according to new behavior

* refactor: remove logs added for testing

* test: await for async getReason method call

---------

Co-authored-by: Neel Doshi <neeldoshi147@gmail.com>
Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2025-06-25 12:24:03 +09:00
Rohit Verma
5d7f42d127
Fix/file usage not working (#6354)
* chore: add R8 rules to prevent obfuscating file usage classes

* chore: upgrade lifecycle-runtime dependency to resolve lint errors

* remove invalid resource directory
2025-06-24 21:56:00 +09:00
translatewiki.net
d9e8917418
Localisation updates from https://translatewiki.net. 2025-06-23 14:01:45 +02:00
Sonal Yadav
09da7b8d68
Skip image upload to Wikidata (nearby -> green pins) (#6349)
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
* Skip image upload to Wikidata if item already has image

* Re-run CI

* no more Failed to update Wikidata for green pins
2025-06-22 22:40:15 +09:00
390 changed files with 14767 additions and 12034 deletions

View file

@ -1,7 +1,7 @@
name: "\U0001F41E Bug report"
description: Create a report to help us improve.
title: "[Bug]: "
labels: ["bug"]
type: Bug # Retained to categorize the issue as per organization-level type
body:
- type: markdown
attributes:
@ -70,7 +70,7 @@ body:
required: false
- type: textarea
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.
validations:
required: false

View file

@ -16,6 +16,7 @@
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="999" />
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="" withSubpackages="true" static="false" module="true" />
<package name="" withSubpackages="true" static="true" />
<emptyLine />
<package name="" withSubpackages="true" static="false" />

View file

@ -1,5 +1,41 @@
# 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

View file

@ -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/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/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/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/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/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/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/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/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/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/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).

View file

@ -18,14 +18,14 @@ if (isRunningOnTravisAndIsNotPRBuild) {
android {
namespace = "fr.free.nrw.commons"
compileSdk = 34
compileSdk = 35
defaultConfig {
applicationId = "fr.free.nrw.commons"
minSdk = 21
targetSdk = 34
versionCode = 1053
versionName = "5.5.0"
targetSdk = 35
versionCode = 1059
versionName = "6.1.0"
setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName())
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
@ -226,6 +226,7 @@ dependencies {
implementation(libs.rxbinding)
implementation(libs.rxbinding.appcompat)
implementation(libs.facebook.fresco)
implementation(libs.facebook.fresco.middleware)
implementation(libs.apache.commons.lang3)
// UI
@ -347,6 +348,7 @@ dependencies {
// Kotlin + coroutines
implementation(libs.androidx.work.runtime.ktx)
implementation(libs.androidx.work.runtime)
implementation(libs.kotlinx.coroutines.rx2)
testImplementation(libs.androidx.work.testing)
//Glide

View file

@ -66,6 +66,9 @@
# Application classes that will be serialized/deserialized over Gson
-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,
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
-keep class * implements com.google.gson.TypeAdapterFactory

View file

@ -57,8 +57,7 @@
tools:replace="android:appComponentFactory">
<activity
android:name=".activity.SingleWebViewActivity"
android:exported="false"
android:label="@string/title_activity_single_web_view" />
android:exported="false" />
<activity
android:name=".nearby.WikidataFeedback"
android:exported="false" />
@ -85,6 +84,7 @@
android:parentActivityName=".customselector.ui.selector.CustomSelectorActivity" />
<activity
android:name=".auth.LoginActivity"
android:windowSoftInputMode="adjustPan"
android:exported="true">
<intent-filter>
<category android:name="android.intent.category.LAUNCHER" />
@ -103,7 +103,7 @@
android:exported="true"
android:hardwareAccelerated="false"
android:icon="@mipmap/ic_launcher"
android:windowSoftInputMode="adjustResize">
android:windowSoftInputMode="adjustPan">
<intent-filter android:label="@string/intent_share_upload_label">
<action android:name="android.intent.action.SEND" />

View file

@ -1,7 +1,9 @@
package fr.free.nrw.commons
import android.annotation.SuppressLint
import android.content.ActivityNotFoundException
import android.content.Intent
import android.content.Intent.ACTION_VIEW
import android.net.Uri
import android.os.Bundle
import android.view.Menu
@ -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.DialogUtil.showAlertDialog
import java.util.Collections
import androidx.core.net.toUri
import fr.free.nrw.commons.utils.applyEdgeToEdgeTopInsets
import fr.free.nrw.commons.utils.handleWebUrl
import fr.free.nrw.commons.utils.setUnderlinedText
/**
* Represents about screen of this app
@ -42,6 +48,7 @@ class AboutActivity : BaseActivity() {
*/
binding = ActivityAboutBinding.inflate(layoutInflater)
val view: View = binding!!.root
applyEdgeToEdgeTopInsets(binding!!.toolbarLayout)
setContentView(view)
setSupportActionBar(binding!!.toolbarBinding.toolbar)
@ -59,30 +66,12 @@ class AboutActivity : BaseActivity() {
binding!!.aboutImprove.setHtmlText(improveText)
binding!!.aboutVersion.text = applicationContext.getVersionNameWithSha()
Utils.setUnderlinedText(
binding!!.aboutFaq, R.string.about_faq,
applicationContext
)
Utils.setUnderlinedText(
binding!!.aboutRateUs, R.string.about_rate_us,
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
)
binding!!.aboutFaq.setUnderlinedText(R.string.about_faq)
binding!!.aboutRateUs.setUnderlinedText(R.string.about_rate_us)
binding!!.aboutUserGuide.setUnderlinedText(R.string.user_guide)
binding!!.aboutPrivacyPolicy.setUnderlinedText(R.string.about_privacy_policy)
binding!!.aboutTranslate.setUnderlinedText(R.string.about_translate)
binding!!.aboutCredits.setUnderlinedText(R.string.about_credits)
/*
To set listeners, we can create a separate method and use lambda syntax.
@ -106,47 +95,56 @@ class AboutActivity : BaseActivity() {
fun launchFacebook(view: View?) {
val intent: Intent
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)
startActivity(intent)
} catch (e: Exception) {
Utils.handleWebUrl(this, Uri.parse(Urls.FACEBOOK_WEB_URL))
handleWebUrl(this, Urls.FACEBOOK_WEB_URL.toUri())
}
}
fun launchGithub(view: View?) {
val intent: Intent
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)
startActivity(intent)
} catch (e: Exception) {
Utils.handleWebUrl(this, Uri.parse(Urls.GITHUB_REPO_URL))
handleWebUrl(this, Urls.GITHUB_REPO_URL.toUri())
}
}
fun launchWebsite(view: View?) {
Utils.handleWebUrl(this, Uri.parse(Urls.WEBSITE_URL))
handleWebUrl(this, Urls.WEBSITE_URL.toUri())
}
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?) {
Utils.handleWebUrl(this, Uri.parse(Urls.CREDITS_URL))
handleWebUrl(this, Urls.CREDITS_URL.toUri())
}
fun launchUserGuide(view: View?) {
Utils.handleWebUrl(this, Uri.parse(Urls.USER_GUIDE_URL))
handleWebUrl(this, Urls.USER_GUIDE_URL.toUri())
}
fun launchPrivacyPolicy(view: View?) {
Utils.handleWebUrl(this, Uri.parse(BuildConfig.PRIVACY_POLICY_URL))
handleWebUrl(this, BuildConfig.PRIVACY_POLICY_URL.toUri())
}
fun launchFrequentlyAskedQuesions(view: View?) {
Utils.handleWebUrl(this, Uri.parse(Urls.FAQ_URL))
handleWebUrl(this, Urls.FAQ_URL.toUri())
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
@ -193,7 +191,7 @@ class AboutActivity : BaseActivity() {
val positiveButtonRunnable = Runnable {
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(
this,

View file

@ -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();
}

View 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()
}

View file

@ -15,9 +15,8 @@ import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.imagepipeline.core.ImagePipelineConfig
import fr.free.nrw.commons.auth.LoginActivity
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable
import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable
import fr.free.nrw.commons.category.CategoryDao
import fr.free.nrw.commons.concurrency.BackgroundPoolExceptionHandler
import fr.free.nrw.commons.concurrency.ThreadPoolService
@ -257,8 +256,8 @@ class CommonsApplication : MultiDexApplication() {
} catch (e: SQLiteException) {
Timber.e(e)
}
BookmarkPicturesDao.Table.onDelete(db)
BookmarkItemsDao.Table.onDelete(db)
BookmarksTable.onDelete(db)
BookmarkItemsTable.onDelete(db)
}

View file

@ -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);
}
}
}

View file

@ -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
}
}

View 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
}
}

View file

@ -1,7 +1,9 @@
package fr.free.nrw.commons
import android.os.Parcelable
import fr.free.nrw.commons.BuildConfig.COMMONS_URL
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 kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@ -173,7 +175,8 @@ class Media constructor(
* Gets file 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

View file

@ -1,8 +0,0 @@
package fr.free.nrw.commons;
/**
* Base interface for all the views
*/
public interface MvpView {
void showMessage(String message);
}

View file

@ -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;
}
}
}

View 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"
}
}

View file

@ -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;
}
}

View file

@ -1,7 +0,0 @@
package fr.free.nrw.commons;
import android.content.Context;
public interface ViewHolder<T> {
void bindModel(Context context, T model);
}

View file

@ -1,68 +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);
}
/**
* Constructs a ViewPagerAdapter with a specified Fragment Manager and Fragment resume behavior.
*
* @param manager The FragmentManager
* @param behavior An integer which represents the behavior of non visible fragments. See
* FragmentPagerAdapter.java for options.
*/
public ViewPagerAdapter(FragmentManager manager, int behavior) {
super(manager, behavior);
}
/**
* 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);
}
}

View 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
}
}

View file

@ -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();
}
}

View 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))
}

View file

@ -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);
}
}

View 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
)
}
}

View file

@ -22,10 +22,10 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.app.NavUtils
import androidx.core.content.ContextCompat
import androidx.core.view.WindowCompat
import fr.free.nrw.commons.BuildConfig
import fr.free.nrw.commons.CommonsApplication
import fr.free.nrw.commons.R
import fr.free.nrw.commons.Utils
import fr.free.nrw.commons.auth.login.LoginCallback
import fr.free.nrw.commons.auth.login.LoginClient
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.di.ApplicationlessInjection
import fr.free.nrw.commons.kvstore.JsonKvStore
import fr.free.nrw.commons.utils.applyEdgeToEdgeAllInsets
import fr.free.nrw.commons.utils.AbstractTextWatcher
import fr.free.nrw.commons.utils.ActivityUtils.startActivityWithFlags
import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour
import fr.free.nrw.commons.utils.SystemThemeUtils
import fr.free.nrw.commons.utils.ViewUtil.hideKeyboard
import fr.free.nrw.commons.utils.handleKeyboardInsets
import fr.free.nrw.commons.utils.handleWebUrl
import io.reactivex.disposables.CompositeDisposable
import timber.log.Timber
import java.util.Locale
@ -79,7 +82,14 @@ class LoginActivity : AccountAuthenticatorActivity() {
delegate.installViewFactory()
delegate.onCreate(savedInstanceState)
WindowCompat.getInsetsController(window, window.decorView)
.isAppearanceLightStatusBars = !isDarkTheme
WindowCompat.setDecorFitsSystemWindows(window, false)
binding = ActivityLoginBinding.inflate(layoutInflater)
applyEdgeToEdgeAllInsets(binding!!.root)
binding!!.root.handleKeyboardInsets()
with(binding!!) {
setContentView(root)
@ -92,7 +102,19 @@ class LoginActivity : AccountAuthenticatorActivity() {
aboutPrivacyPolicy.setOnClickListener { onPrivacyPolicyClicked() }
signUpButton.setOnClickListener { signUp() }
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 =
View.OnFocusChangeListener(::onPasswordFocusChanged)
@ -113,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?) {
super.onPostCreate(savedInstanceState)
delegate.onPostCreate(savedInstanceState)
@ -236,7 +291,7 @@ class LoginActivity : AccountAuthenticatorActivity() {
} else false
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() {
AlertDialog.Builder(this)
@ -254,10 +309,10 @@ class LoginActivity : AccountAuthenticatorActivity() {
}
private fun forgotPassword() =
Utils.handleWebUrl(this, Uri.parse(BuildConfig.FORGOT_PASSWORD_URL))
handleWebUrl(this, Uri.parse(BuildConfig.FORGOT_PASSWORD_URL))
private fun onPrivacyPolicyClicked() =
Utils.handleWebUrl(this, Uri.parse(BuildConfig.PRIVACY_POLICY_URL))
handleWebUrl(this, Uri.parse(BuildConfig.PRIVACY_POLICY_URL))
private fun signUp() =
startActivity(Intent(this, SignupActivity::class.java))
@ -286,14 +341,14 @@ class LoginActivity : AccountAuthenticatorActivity() {
Timber.d("Requesting 2FA prompt")
progressDialog!!.dismiss()
lastLoginResult = loginResult
askUserForTwoFactorAuth()
askUserForTwoFactorAuthWithKeyboard()
}
override fun emailAuthPrompt(loginResult: LoginResult, caught: Throwable, token: String?) {
override fun emailAuthPrompt(loginResult: LoginResult, caught: Throwable, token: String?) = runOnUiThread {
Timber.d("Requesting email auth prompt")
progressDialog!!.dismiss()
lastLoginResult = loginResult
askUserForTwoFactorAuth()
askUserForTwoFactorAuthWithKeyboard()
}
override fun passwordResetPrompt(token: String?) = runOnUiThread {
@ -348,12 +403,31 @@ class LoginActivity : AccountAuthenticatorActivity() {
@VisibleForTesting
fun askUserForTwoFactorAuth() {
if (binding == null) {
Timber.w("Binding is null, reinitializing in askUserForTwoFactorAuth")
binding = ActivityLoginBinding.inflate(layoutInflater)
setContentView(binding!!.root)
}
progressDialog!!.dismiss()
if (binding != null) {
with(binding!!) {
twoFactorContainer.visibility = View.VISIBLE
twoFactorContainer.hint = getString(if (lastLoginResult is LoginResult.EmailAuthResult) R.string.email_auth_code else R.string._2fa_code)
loginTwoFactor.visibility = View.VISIBLE
loginTwoFactor.requestFocus()
loginTwoFactor.setOnEditorActionListener { _, actionId, event ->
if (actionId == EditorInfo.IME_ACTION_DONE ||
(event != null && event.keyCode == KeyEvent.KEYCODE_ENTER && event.action == KeyEvent.ACTION_DOWN)) {
performLogin()
true
} else {
false
}
}
}
} else {
Timber.e("Binding is null in askUserForTwoFactorAuth after reinitialization attempt")
}
val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY)

View file

@ -10,6 +10,7 @@ import android.widget.Toast
import fr.free.nrw.commons.BuildConfig
import fr.free.nrw.commons.R
import fr.free.nrw.commons.theme.BaseActivity
import fr.free.nrw.commons.utils.applyEdgeToEdgeAllInsets
import timber.log.Timber
class SignupActivity : BaseActivity() {
@ -21,6 +22,7 @@ class SignupActivity : BaseActivity() {
Timber.d("Signup Activity started")
webView = WebView(this)
applyEdgeToEdgeAllInsets(webView!!)
with(webView!!) {
setContentView(this)
webViewClient = MyWebViewClient()

View file

@ -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;
}
}

View file

@ -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
}
}
}

View file

@ -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;
}
}

View file

@ -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
}
}

View file

@ -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();
}
}

View file

@ -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()
}

View file

@ -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;
}
}

View file

@ -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()
}
}

View file

@ -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();
}
}

View file

@ -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()
}
}

View file

@ -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);
}
}
}
}

View file

@ -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,
)
}
}

View file

@ -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;
}
}

View file

@ -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
}
}

View file

@ -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)
}
}
}

View file

@ -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;
}
}

View file

@ -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()
}
}

View file

@ -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
}
}

View file

@ -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
}

View file

@ -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;
}
}
}
}

View file

@ -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
)
}

View file

@ -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();
}
}

View file

@ -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
}

View file

@ -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
}
}
}

View file

@ -7,8 +7,8 @@ import com.google.gson.annotations.SerializedName
*/
class CampaignConfig {
@SerializedName("showOnlyLiveCampaigns")
private val showOnlyLiveCampaigns = false
var showOnlyLiveCampaigns = false
@SerializedName("sortBy")
private val sortBy: String? = null
var sortBy: String? = null
}

View file

@ -8,8 +8,8 @@ import fr.free.nrw.commons.campaigns.models.Campaign
*/
class CampaignResponseDTO {
@SerializedName("config")
val campaignConfig: CampaignConfig? = null
var campaignConfig: CampaignConfig? = null
@SerializedName("campaigns")
val campaigns: List<Campaign>? = null
var campaigns: List<Campaign>? = null
}

View file

@ -7,7 +7,6 @@ import android.view.LayoutInflater
import android.view.View
import androidx.core.content.ContextCompat
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.contributions.MainActivity
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.SwipableCardView
import fr.free.nrw.commons.utils.ViewUtil.showLongToast
import fr.free.nrw.commons.utils.handleWebUrl
import timber.log.Timber
import java.text.ParseException
@ -74,7 +74,7 @@ class CampaignView : SwipableCardView {
if (it.isWLMCampaign) {
((context) as MainActivity).showNearby()
} else {
Utils.handleWebUrl(context, Uri.parse(it.link))
handleWebUrl(context, Uri.parse(it.link))
}
}
}

View file

@ -26,7 +26,7 @@ class CampaignsPresenter @Inject constructor(
private val okHttpJsonApiClient: OkHttpJsonApiClient?,
@param:Named(IO_THREAD) private val ioScheduler: Scheduler,
@param:Named(MAIN_THREAD) private val mainThreadScheduler: Scheduler
) : BasePresenter<ICampaignsView?> {
) : BasePresenter<ICampaignsView> {
private var view: ICampaignsView? = null
private var disposable: Disposable? = null
private var campaign: Campaign? = null

View file

@ -1,11 +1,10 @@
package fr.free.nrw.commons.campaigns
import fr.free.nrw.commons.MvpView
import fr.free.nrw.commons.campaigns.models.Campaign
/**
* Interface which defines the view contracts of the campaign view
*/
interface ICampaignsView : MvpView {
interface ICampaignsView {
fun showCampaigns(campaign: Campaign?)
}

View file

@ -9,12 +9,9 @@ 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 timber.log.Timber
import javax.inject.Inject
import androidx.core.net.toUri
class CategoryContentProvider : CommonsDaggerContentProvider() {
@ -23,9 +20,6 @@ class CategoryContentProvider : CommonsDaggerContentProvider() {
addURI(BuildConfig.CATEGORY_AUTHORITY, "${BASE_PATH}/#", CATEGORIES_ID)
}
@Inject
lateinit var dbOpenHelper: DBOpenHelper
@SuppressWarnings("ConstantConditions")
override fun query(uri: Uri, projection: Array<String>?, selection: String?,
selectionArgs: Array<String>?, sortOrder: String?): Cursor? {
@ -34,7 +28,7 @@ class CategoryContentProvider : CommonsDaggerContentProvider() {
}
val uriType = uriMatcher.match(uri)
val db = dbOpenHelper.readableDatabase
val db = requireDb()
val cursor: Cursor? = when (uriType) {
CATEGORIES -> queryBuilder.query(
@ -58,45 +52,37 @@ class CategoryContentProvider : CommonsDaggerContentProvider() {
else -> throw IllegalArgumentException("Unknown URI $uri")
}
cursor?.setNotificationUri(context?.contentResolver, uri)
cursor?.setNotificationUri(requireContext().contentResolver, uri)
return cursor
}
override fun getType(uri: Uri): String? {
return null
}
override fun getType(uri: Uri): String? = null
@SuppressWarnings("ConstantConditions")
override fun insert(uri: Uri, contentValues: ContentValues?): Uri? {
override fun insert(uri: Uri, contentValues: ContentValues?): Uri {
val uriType = uriMatcher.match(uri)
val sqlDB = dbOpenHelper.writableDatabase
val id: Long
when (uriType) {
CATEGORIES -> {
id = sqlDB.insert(TABLE_NAME, null, contentValues)
id = requireDb().insert(TABLE_NAME, null, contentValues)
}
else -> throw IllegalArgumentException("Unknown URI: $uri")
}
context?.contentResolver?.notifyChange(uri, null)
return Uri.parse("${Companion.BASE_URI}/$id")
requireContext().contentResolver?.notifyChange(uri, null)
return "${BASE_URI}/$id".toUri()
}
@SuppressWarnings("ConstantConditions")
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
// Not implemented
return 0
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int = 0
@SuppressWarnings("ConstantConditions")
override fun bulkInsert(uri: Uri, values: Array<ContentValues>): Int {
Timber.d("Hello, bulk insert! (CategoryContentProvider)")
val uriType = uriMatcher.match(uri)
val sqlDB = dbOpenHelper.writableDatabase
val sqlDB = requireDb()
sqlDB.beginTransaction()
when (uriType) {
CATEGORIES -> {
for (value in values) {
Timber.d("Inserting! %s", value)
sqlDB.insert(TABLE_NAME, null, value)
}
sqlDB.setTransactionSuccessful()
@ -104,7 +90,7 @@ class CategoryContentProvider : CommonsDaggerContentProvider() {
else -> throw IllegalArgumentException("Unknown URI: $uri")
}
sqlDB.endTransaction()
context?.contentResolver?.notifyChange(uri, null)
requireContext().contentResolver?.notifyChange(uri, null)
return values.size
}
@ -112,17 +98,18 @@ class CategoryContentProvider : CommonsDaggerContentProvider() {
override fun update(uri: Uri, contentValues: ContentValues?, selection: String?,
selectionArgs: Array<String>?): Int {
val uriType = uriMatcher.match(uri)
val sqlDB = dbOpenHelper.writableDatabase
val rowsUpdated: Int
when (uriType) {
CATEGORIES_ID -> {
if (TextUtils.isEmpty(selection)) {
val id = uri.lastPathSegment?.toInt()
?: throw IllegalArgumentException("Invalid ID")
rowsUpdated = sqlDB.update(TABLE_NAME,
rowsUpdated = requireDb().update(
TABLE_NAME,
contentValues,
"$COLUMN_ID = ?",
arrayOf(id.toString()))
arrayOf(id.toString())
)
} else {
throw IllegalArgumentException(
"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")
}
context?.contentResolver?.notifyChange(uri, null)
requireContext().contentResolver?.notifyChange(uri, null)
return rowsUpdated
}
@ -165,13 +152,9 @@ class CategoryContentProvider : CommonsDaggerContentProvider() {
"$COLUMN_TIMES_USED INTEGER" +
");"
fun uriForId(id: Int): Uri {
return Uri.parse("${BASE_URI}/$id")
}
fun uriForId(id: Int): Uri = Uri.parse("${BASE_URI}/$id")
fun onCreate(db: SQLiteDatabase) {
db.execSQL(CREATE_TABLE_STATEMENT)
}
fun onCreate(db: SQLiteDatabase) = db.execSQL(CREATE_TABLE_STATEMENT)
fun onDelete(db: SQLiteDatabase) {
db.execSQL(DROP_TABLE_STATEMENT)
@ -200,6 +183,6 @@ class CategoryContentProvider : CommonsDaggerContentProvider() {
private const val CATEGORIES = 1
private const val CATEGORIES_ID = 2
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()
}
}

View file

@ -8,21 +8,25 @@ import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.activity.viewModels
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import fr.free.nrw.commons.BuildConfig.COMMONS_URL
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.R
import fr.free.nrw.commons.Utils
import fr.free.nrw.commons.ViewPagerAdapter
import fr.free.nrw.commons.databinding.ActivityCategoryDetailsBinding
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.sub.SubCategoriesFragment
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.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 javax.inject.Inject
@ -33,7 +37,7 @@ import javax.inject.Inject
* a particular category on wikimedia commons.
*/
class CategoryDetailsActivity : BaseActivity(),
MediaDetailPagerFragment.MediaDetailProvider,
MediaDetailProvider,
CategoryImagesCallback {
private lateinit var supportFragmentManager: FragmentManager
@ -54,9 +58,10 @@ class CategoryDetailsActivity : BaseActivity(),
binding = ActivityCategoryDetailsBinding.inflate(layoutInflater)
val view = binding.root
applyEdgeToEdgeAllInsets(view)
setContentView(view)
supportFragmentManager = getSupportFragmentManager()
viewPagerAdapter = ViewPagerAdapter(supportFragmentManager)
viewPagerAdapter = ViewPagerAdapter(this, supportFragmentManager)
binding.viewPager.adapter = viewPagerAdapter
binding.viewPager.offscreenPageLimit = 2
binding.tabLayout.setupWithViewPager(binding.viewPager)
@ -80,8 +85,6 @@ class CategoryDetailsActivity : BaseActivity(),
* Set the fragments according to the tab selected in the viewPager.
*/
private fun setTabs() {
val fragmentList = mutableListOf<Fragment>()
val titleList = mutableListOf<String>()
categoriesMediaFragment = CategoriesMediaFragment()
val subCategoryListFragment = SubCategoriesFragment()
val parentCategoriesFragment = ParentCategoriesFragment()
@ -96,13 +99,12 @@ class CategoryDetailsActivity : BaseActivity(),
viewModel.onCheckIfBookmarked(categoryName!!)
}
fragmentList.add(categoriesMediaFragment)
titleList.add("MEDIA")
fragmentList.add(subCategoryListFragment)
titleList.add("SUBCATEGORIES")
fragmentList.add(parentCategoriesFragment)
titleList.add("PARENT CATEGORIES")
viewPagerAdapter.setTabData(fragmentList, titleList)
viewPagerAdapter.setTabs(
R.string.title_for_media to categoriesMediaFragment,
R.string.title_for_subcategories to subCategoryListFragment,
R.string.title_for_parent_categories to parentCategoriesFragment
)
viewPagerAdapter.notifyDataSetChanged()
}
@ -199,8 +201,9 @@ class CategoryDetailsActivity : BaseActivity(),
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.menu_browser_current_category -> {
val title = Utils.getPageTitle(CATEGORY_PREFIX + categoryName)
Utils.handleWebUrl(this, Uri.parse(title.canonicalUri))
val title = PageTitle(CATEGORY_PREFIX + categoryName, WikiSite(COMMONS_URL))
handleWebUrl(this, Uri.parse(title.canonicalUri))
true
}

View file

@ -180,8 +180,8 @@ class ContributionController @Inject constructor(@param:Named("default_preferenc
showAlertDialog(
activity, activity.getString(R.string.location_permission_title),
activity.getString(R.string.in_app_camera_location_permission_rationale),
activity.getString(android.R.string.ok),
activity.getString(android.R.string.cancel),
activity.getString(R.string.ok),
activity.getString(R.string.cancel),
{
createDialogsAndHandleLocationPermissions(
activity,

View file

@ -9,7 +9,6 @@ import fr.free.nrw.commons.BasePresenter
interface ContributionsContract {
interface View {
fun showMessage(localizedMessage: String)
fun getContext(): Context?
}

View file

@ -30,7 +30,6 @@ import androidx.work.WorkManager
import fr.free.nrw.commons.MapController.NearbyPlacesInfo
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.R
import fr.free.nrw.commons.Utils
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.campaigns.CampaignView
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.LocationUpdateListener
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.nearby.NearbyController
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.PermissionUtils.hasPermission
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.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.schedulers.Schedulers
import timber.log.Timber
import java.util.Calendar
import java.util.Date
import javax.inject.Inject
import javax.inject.Named
@ -242,8 +243,8 @@ class ContributionsFragment : CommonsDaggerSupportFragment(), FragmentManager.On
private fun initWLMCampaign() {
wlmCampaign = Campaign(
getString(R.string.wlm_campaign_title),
getString(R.string.wlm_campaign_description), Utils.getWLMStartDate().toString(),
Utils.getWLMEndDate().toString(), NearbyParentFragment.WLM_URL, true
getString(R.string.wlm_campaign_description), wLMStartDate,
wLMEndDate, NearbyParentFragment.WLM_URL, true
)
}
@ -729,7 +730,7 @@ class ContributionsFragment : CommonsDaggerSupportFragment(), FragmentManager.On
* of campaigns on the campaigns card
*/
private fun fetchCampaigns() {
if (Utils.isMonumentsEnabled(Date())) {
if (isMonumentsEnabled) {
if (binding != null) {
binding!!.campaignsView.setCampaign(wlmCampaign)
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?) {
if (campaign != null && !isUserProfile) {
if (binding != null) {

View file

@ -15,7 +15,7 @@ class ContributionsListContract {
fun showNoContributionsUI(shouldShow: Boolean)
}
interface UserActionListener : BasePresenter<View?> {
interface UserActionListener : BasePresenter<View> {
fun refreshList(swipeRefreshLayout: SwipeRefreshLayout?)
}
}

View file

@ -5,7 +5,6 @@ import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.net.Uri
import android.os.Bundle
import android.os.Parcelable
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.StartActivityForResult
import androidx.annotation.VisibleForTesting
import androidx.core.net.toUri
import androidx.core.os.BundleCompat
import androidx.paging.PagedList
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
@ -29,7 +30,6 @@ import androidx.recyclerview.widget.SimpleItemAnimator
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.MediaDataExtractor
import fr.free.nrw.commons.R
import fr.free.nrw.commons.Utils
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.contributions.WikipediaInstructionsDialogFragment.Companion.newInstance
import fr.free.nrw.commons.databinding.FragmentContributionsListBinding
@ -39,10 +39,10 @@ import fr.free.nrw.commons.filepicker.FilePicker
import fr.free.nrw.commons.media.MediaClient
import fr.free.nrw.commons.profile.ProfileActivity
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.copyToClipboard
import fr.free.nrw.commons.utils.handleWebUrl
import fr.free.nrw.commons.wikidata.model.WikiSite
import org.apache.commons.lang3.StringUtils
import javax.inject.Inject
import javax.inject.Named
@ -52,10 +52,6 @@ import javax.inject.Named
*/
class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsListContract.View,
ContributionsListAdapter.Callback, WikipediaInstructionsDialogFragment.Callback {
@JvmField
@Inject
var systemThemeUtils: SystemThemeUtils? = null
@JvmField
@Inject
var controller: ContributionController? = null
@ -82,13 +78,14 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL
var sessionManager: SessionManager? = null
private var binding: FragmentContributionsListBinding? = null
private var fab_close: Animation? = null
private var fab_open: Animation? = null
private var rotate_forward: Animation? = null
private var rotate_backward: Animation? = null
private var fabClose: Animation? = null
private var fabOpen: Animation? = null
private var rotateForward: Animation? = null
private var rotateBackward: Animation? = null
private var isFabOpen = false
private lateinit var inAppCameraLocationPermissionLauncher: ActivityResultLauncher<Array<String>>
private lateinit var inAppCameraLocationPermissionLauncher:
ActivityResultLauncher<Array<String>>
@VisibleForTesting
var rvContributionsList: RecyclerView? = null
@ -99,8 +96,8 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL
@VisibleForTesting
var callback: Callback? = null
private val SPAN_COUNT_LANDSCAPE = 3
private val SPAN_COUNT_PORTRAIT = 1
private val spanCountLandscape = 3
private val spanCountPortrait = 1
private var contributionsSize = 0
private var userName: String? = null
@ -149,7 +146,7 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL
userName = requireArguments().getString(ProfileActivity.KEY_USERNAME)
}
if (StringUtils.isEmpty(userName)) {
if (userName.isNullOrEmpty()) {
userName = sessionManager!!.userName
}
inAppCameraLocationPermissionLauncher =
@ -160,7 +157,8 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL
controller?.locationPermissionCallback?.onLocationPermissionGranted()
} else {
activity?.let { currentActivity ->
if (currentActivity.shouldShowRequestPermissionRationale(permission.ACCESS_FINE_LOCATION)) {
if (currentActivity.shouldShowRequestPermissionRationale(
permission.ACCESS_FINE_LOCATION)) {
controller?.handleShowRationaleFlowCameraLocation(
currentActivity,
inAppCameraLocationPermissionLauncher, // Pass launcher
@ -168,7 +166,8 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL
)
} else {
controller?.locationPermissionCallback?.onLocationPermissionDenied(
currentActivity.getString(R.string.in_app_camera_location_permission_denied)
currentActivity.getString(
R.string.in_app_camera_location_permission_denied)
)
}
}
@ -188,7 +187,7 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL
contributionsListPresenter!!.onAttachView(this)
binding!!.fabCustomGallery.setOnClickListener { v: View? -> launchCustomSelector() }
binding!!.fabCustomGallery.setOnLongClickListener { view: View? ->
showShortToast(context, fr.free.nrw.commons.R.string.custom_selector_title)
showShortToast(context, R.string.custom_selector_title)
true
}
@ -198,7 +197,7 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL
} else {
binding!!.tvContributionsOfUser.visibility = View.VISIBLE
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
}
@ -236,7 +235,10 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL
}
private fun initAdapter() {
adapter = ContributionsListAdapter(this, mediaClient!!, mediaDataExtractor!!, compositeDisposable)
adapter = ContributionsListAdapter(this,
mediaClient!!,
mediaDataExtractor!!,
compositeDisposable)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -311,7 +313,7 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL
override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean {
if (e.action == MotionEvent.ACTION_DOWN) {
if (isFabOpen) {
animateFAB(isFabOpen)
animateFAB(true)
}
}
return false
@ -343,14 +345,20 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL
}
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) {
super.onConfigurationChanged(newConfig)
// check 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
?.setLayoutManager(
GridLayoutManager(context, getSpanCount(newConfig.orientation))
@ -358,10 +366,10 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL
}
private fun initializeAnimations() {
fab_open = AnimationUtils.loadAnimation(activity, fr.free.nrw.commons.R.anim.fab_open)
fab_close = AnimationUtils.loadAnimation(activity, fr.free.nrw.commons.R.anim.fab_close)
rotate_forward = AnimationUtils.loadAnimation(activity, fr.free.nrw.commons.R.anim.rotate_forward)
rotate_backward = AnimationUtils.loadAnimation(activity, fr.free.nrw.commons.R.anim.rotate_backward)
fabOpen = AnimationUtils.loadAnimation(activity, R.anim.fab_open)
fabClose = AnimationUtils.loadAnimation(activity, R.anim.fab_close)
rotateForward = AnimationUtils.loadAnimation(activity, R.anim.rotate_forward)
rotateBackward = AnimationUtils.loadAnimation(activity, R.anim.rotate_backward)
}
private fun setListeners() {
@ -377,7 +385,7 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL
binding!!.fabCamera.setOnLongClickListener { view: View? ->
showShortToast(
context,
fr.free.nrw.commons.R.string.add_contribution_from_camera
R.string.add_contribution_from_camera
)
true
}
@ -386,7 +394,7 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL
animateFAB(isFabOpen)
}
binding!!.fabGallery.setOnLongClickListener { view: View? ->
showShortToast(context, fr.free.nrw.commons.R.string.menu_from_gallery)
showShortToast(context, R.string.menu_from_gallery)
true
}
}
@ -394,7 +402,7 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL
/**
* Launch Custom Selector.
*/
protected fun launchCustomSelector() {
private fun launchCustomSelector() {
controller!!.initiateCustomGalleryPickWithPermission(
requireActivity(),
customSelectorLauncherForResult
@ -410,18 +418,18 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL
this.isFabOpen = !isFabOpen
if (binding!!.fabPlus.isShown) {
if (isFabOpen) {
binding!!.fabPlus.startAnimation(rotate_backward)
binding!!.fabCamera.startAnimation(fab_close)
binding!!.fabGallery.startAnimation(fab_close)
binding!!.fabCustomGallery.startAnimation(fab_close)
binding!!.fabPlus.startAnimation(rotateBackward)
binding!!.fabCamera.startAnimation(fabClose)
binding!!.fabGallery.startAnimation(fabClose)
binding!!.fabCustomGallery.startAnimation(fabClose)
binding!!.fabCamera.hide()
binding!!.fabGallery.hide()
binding!!.fabCustomGallery.hide()
} else {
binding!!.fabPlus.startAnimation(rotate_forward)
binding!!.fabCamera.startAnimation(fab_open)
binding!!.fabGallery.startAnimation(fab_open)
binding!!.fabCustomGallery.startAnimation(fab_open)
binding!!.fabPlus.startAnimation(rotateForward)
binding!!.fabCamera.startAnimation(fabOpen)
binding!!.fabGallery.startAnimation(fabOpen)
binding!!.fabCustomGallery.startAnimation(fabOpen)
binding!!.fabCamera.show()
binding!!.fabGallery.show()
binding!!.fabCustomGallery.show()
@ -433,9 +441,9 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL
/**
* 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 =
if (shouldShow) View.VISIBLE else View.GONE
if (numberOfUploads) View.VISIBLE else View.GONE
}
/**
@ -455,22 +463,22 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
val layoutManager = rvContributionsList
?.getLayoutManager() as GridLayoutManager?
val layoutManager = rvContributionsList?.layoutManager as GridLayoutManager?
outState.putParcelable(RV_STATE, layoutManager!!.onSaveInstanceState())
}
override fun onViewStateRestored(savedInstanceState: Bundle?) {
super.onViewStateRestored(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)
}
}
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
callback!!.showDetail(position, isWikipediaButtonDisplayed)
callback!!.showDetail(contribution, isWikipediaPageExists)
}
}
@ -482,8 +490,8 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL
override fun addImageToWikipedia(contribution: Contribution?) {
showAlertDialog(
requireActivity(),
getString(fr.free.nrw.commons.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_title),
getString(R.string.add_picture_to_wikipedia_article_desc),
{
if (contribution != null) {
showAddImageToWikipediaInstructions(contribution)
@ -497,16 +505,18 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL
* @param contribution
*/
private fun showAddImageToWikipediaInstructions(contribution: Contribution) {
val fragmentManager = fragmentManager
val fragmentManager = this.parentFragmentManager
val fragment = newInstance(contribution)
fragment.callback =
WikipediaInstructionsDialogFragment.Callback { contribution: Contribution?, copyWikicode: Boolean ->
this.onConfirmClicked(
WikipediaInstructionsDialogFragment.Callback {
contribution: Contribution?,
copyWikicode: Boolean ->
onConfirmClicked(
contribution,
copyWikicode
)
}
fragment.show(fragmentManager!!, "WikimediaFragment")
fragment.show(fragmentManager, "WikimediaFragment")
}
@ -527,14 +537,13 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL
*/
override fun onConfirmClicked(contribution: Contribution?, copyWikicode: Boolean) {
if (copyWikicode) {
val wikicode = contribution!!.media.wikiCode
Utils.copy("wikicode", wikicode, context)
requireContext().copyToClipboard("wikicode", contribution!!.media.wikiCode)
}
val url =
languageWikipediaSite!!.mobileUrl() + "/wiki/" + (contribution!!.wikidataPlace
?.getWikipediaPageTitle())
Utils.handleWebUrl(context, Uri.parse(url))
handleWebUrl(requireContext(), url.toUri())
}
fun getContributionStateAt(position: Int): Int {

View file

@ -12,7 +12,6 @@ import androidx.fragment.app.FragmentManager
import androidx.work.ExistingWorkPolicy
import com.google.android.material.bottomnavigation.BottomNavigationView
import fr.free.nrw.commons.R
import fr.free.nrw.commons.WelcomeActivity
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.bookmarks.BookmarkFragment
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.quiz.QuizChecker
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.utils.applyEdgeToEdgeAllInsets
import fr.free.nrw.commons.upload.UploadProgressActivity
import fr.free.nrw.commons.upload.worker.WorkRequestHelper.Companion.makeOneTimeWorkRequest
import fr.free.nrw.commons.utils.ViewUtilWrapper
@ -112,6 +113,7 @@ class MainActivity : BaseActivity(), FragmentManager.OnBackStackChangedListener
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = MainBinding.inflate(layoutInflater)
applyEdgeToEdgeAllInsets(binding!!.root)
setContentView(binding!!.root)
setSupportActionBar(binding!!.toolbarBinding.toolbar)
tabLayout = binding!!.fragmentMainNavTabLayout
@ -151,21 +153,7 @@ after opening the app.
}
}
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()
}
}
@ -336,7 +324,7 @@ after opening the app.
)
.subscribeOn(Schedulers.io())
.blockingGet()
Timber.d("Resuming " + stuckUploads.size + " uploads...")
Timber.d("Resuming %d uploads...", stuckUploads.size)
if (!stuckUploads.isEmpty()) {
for (contribution in stuckUploads) {
contribution.state = Contribution.STATE_QUEUED
@ -517,7 +505,7 @@ after opening the app.
(!applicationKvStore!!.getBoolean("login_skipped"))
) {
defaultKvStore.putBoolean("inAppCameraFirstRun", true)
WelcomeActivity.startYourself(this)
startWelcome()
}
retryAllFailedUploads()

View file

@ -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())
showNotification(context, "Setting Wallpaper Failed", "Failed to download image.")
dataSource?.close()
dataSource.close()
}
}, CallerThreadExecutor.getInstance())

View file

@ -39,4 +39,11 @@ data class Folder(
return true
}
override fun hashCode(): Int {
var result = bucketId.hashCode()
result = 31 * result + name.hashCode()
result = 31 * result + images.hashCode()
return result
}
}

View file

@ -1,6 +1,7 @@
package fr.free.nrw.commons.customselector.model
import android.net.Uri
import android.os.Build
import android.os.Parcel
import android.os.Parcelable
@ -48,7 +49,12 @@ data class Image(
this(
parcel.readLong(),
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.readLong(),
parcel.readString()!!,
@ -121,4 +127,16 @@ data class Image(
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
}
}

View file

@ -168,8 +168,7 @@ class ImageAdapter(
// Getting selected index when switch is off
} else if (actionableImagesMap.size > position) {
ImageHelper
.getIndex(selectedImages, ArrayList(actionableImagesMap.values)[position])
ImageHelper.getIndex(selectedImages, ArrayList(actionableImagesMap.values)[position])
// For any other case return -1
} else {
@ -348,8 +347,14 @@ class ImageAdapter(
numberOfSelectedImagesMarkedAsNotForUpload--
}
notifyItemChanged(position, ImageUnselected())
// Notify listener of deselection to update UI
imageSelectListener.onSelectedImagesChanged(selectedImages, numberOfSelectedImagesMarkedAsNotForUpload)
} else {
val image = images[position]
// Prevent adding the same image multiple times
val image = if (showAlreadyActionedImages) images[position] else ArrayList(actionableImagesMap.values)[position]
if (selectedImages.contains(image)) {
return // Image already selected, ignore additional clicks
}
scope.launch(ioDispatcher) {
val imageSHA1 = imageLoader.getSHA1(image, defaultDispatcher)
withContext(Dispatchers.Main) {
@ -373,7 +378,6 @@ class ImageAdapter(
}
selectedImages.add(image)
notifyItemChanged(position, ImageSelectedOrUpdated())
imageSelectListener.onSelectedImagesChanged(selectedImages, numberOfSelectedImagesMarkedAsNotForUpload)
}
}

View file

@ -40,6 +40,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.core.view.ViewGroupCompat
import androidx.lifecycle.ViewModelProvider
import fr.free.nrw.commons.R
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.upload.FileUtilsWrapper
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.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -198,6 +201,9 @@ class CustomSelectorActivity :
.fillMaxWidth(),
)
}
ViewGroupCompat.installCompatInsetsDispatch(binding.root)
applyEdgeToEdgeTopInsets(toolbarBinding.toolbarLayout)
bottomSheetBinding.bottomLayout.applyEdgeToEdgeBottomPaddingInsets()
val view = binding.root
setContentView(view)

View file

@ -18,6 +18,7 @@ import fr.free.nrw.commons.databinding.FragmentCustomSelectorBinding
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
import fr.free.nrw.commons.media.MediaClient
import fr.free.nrw.commons.upload.FileProcessor
import fr.free.nrw.commons.utils.applyEdgeToEdgeBottomPaddingInsets
import javax.inject.Inject
/**
@ -99,6 +100,7 @@ class FolderFragment : CommonsDaggerSupportFragment() {
selectorRV = binding?.selectorRv
loader = binding?.loader
with(binding?.selectorRv) {
this?.applyEdgeToEdgeBottomPaddingInsets()
this?.layoutManager = gridLayoutManager
this?.setHasFixedSize(true)
this?.adapter = folderAdapter

View file

@ -9,7 +9,6 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ProgressBar
import android.widget.Switch
import androidx.appcompat.app.AlertDialog
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible
@ -20,6 +19,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.GridLayoutManager
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.ContributionDao
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.upload.FileProcessor
import fr.free.nrw.commons.upload.FileUtilsWrapper
import fr.free.nrw.commons.utils.applyEdgeToEdgeBottomPaddingInsets
import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.TreeMap
import javax.inject.Inject
import kotlin.collections.ArrayList
@ -80,7 +82,7 @@ class ImageFragment :
*/
private var selectorRV: RecyclerView? = null
private var loader: ProgressBar? = null
private var switch: Switch? = null
private var switch: SwitchMaterial? = null
lateinit var filteredImages: ArrayList<Image>
/**
@ -210,13 +212,18 @@ class ImageFragment :
savedInstanceState: Bundle?,
): View? {
_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())
with(binding?.selectorRv) {
this?.applyEdgeToEdgeBottomPaddingInsets()
this?.layoutManager = gridLayoutManager
this?.setHasFixedSize(true)
this?.adapter = imageAdapter
@ -368,7 +375,12 @@ class ImageFragment :
* notifyDataSetChanged, rebuild the holder views to account for deleted images.
*/
override fun onResume() {
if (::imageAdapter.isInitialized) {
imageAdapter.notifyDataSetChanged()
Timber.d("Notified imageAdapter in onResume")
} else {
Timber.w("imageAdapter not initialized in onResume")
}
super.onResume()
}
@ -378,14 +390,19 @@ class ImageFragment :
* Save the Image Fragment state.
*/
override fun onDestroy() {
if (::imageAdapter.isInitialized) {
imageAdapter.cleanUp()
Timber.d("Cleaned up imageAdapter in onDestroy")
} else {
Timber.w("imageAdapter not initialized in onDestroy, skipping cleanup")
}
val position =
(selectorRV?.layoutManager as GridLayoutManager)
.findFirstVisibleItemPosition()
(selectorRV?.layoutManager as? GridLayoutManager)
?.findFirstVisibleItemPosition() ?: -1
// Check for empty RecyclerView.
if (position != -1 && filteredImages.size > 0) {
// check for valid position and non-empty image list
if (position != -1 && filteredImages.isNotEmpty() && ::imageAdapter.isInitialized) {
context?.let { context ->
context
.getSharedPreferences(
@ -394,34 +411,57 @@ class ImageFragment :
)?.let { prefs ->
prefs.edit()?.let { editor ->
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()
}
override fun onDestroyView() {
_binding = null
selectorRV = null
loader = null
switch = null
progressLayout = null
super.onDestroyView()
}
override fun refresh() {
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
*/
fun removeImage(image: 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
*/
fun clearSelectedImages() {
if (::imageAdapter.isInitialized) {
imageAdapter.clearSelectedImages()
Timber.d("Cleared selected images")
} else {
Timber.w("imageAdapter not initialized in clearSelectedImages")
}
}
/**
@ -432,6 +472,15 @@ class ImageFragment :
selectedImages: ArrayList<Image>,
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")
}
}
/**
@ -441,6 +490,7 @@ class ImageFragment :
if (!progressDialog.isShowing) {
progressDialogLayout.progressDialogText.text = text
progressDialog.show()
Timber.d("Showing mark/unmark progress dialog: %s", text)
}
}
@ -450,6 +500,7 @@ class ImageFragment :
fun dismissMarkUnmarkProgressDialog() {
if (progressDialog.isShowing) {
progressDialog.dismiss()
Timber.d("Dismissed mark/unmark progress dialog")
}
}

View file

@ -4,11 +4,10 @@ import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteException
import android.database.sqlite.SQLiteOpenHelper
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable
import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable
import fr.free.nrw.commons.category.CategoryDao
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable
import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao
@ -30,17 +29,17 @@ class DBOpenHelper(
*/
override fun onCreate(db: SQLiteDatabase) {
CategoryDao.Table.onCreate(db)
BookmarkPicturesDao.Table.onCreate(db)
BookmarkItemsDao.Table.onCreate(db)
RecentSearchesDao.Table.onCreate(db)
BookmarksTable.onCreate(db)
BookmarkItemsTable.onCreate(db)
RecentSearchesTable.onCreate(db)
RecentLanguagesDao.Table.onCreate(db)
}
override fun onUpgrade(db: SQLiteDatabase, from: Int, to: Int) {
CategoryDao.Table.onUpdate(db, from, to)
BookmarkPicturesDao.Table.onUpdate(db, from, to)
BookmarkItemsDao.Table.onUpdate(db, from, to)
RecentSearchesDao.Table.onUpdate(db, from, to)
BookmarksTable.onUpdate(db, from, to)
BookmarkItemsTable.onUpdate(db, from, to)
RecentSearchesTable.onUpdate(db, from, to)
RecentLanguagesDao.Table.onUpdate(db, from, to)
deleteTable(db, CONTRIBUTIONS_TABLE)
deleteTable(db, BOOKMARKS_LOCATIONS)

View file

@ -53,7 +53,6 @@ class DeleteHelper @Inject constructor(
media: Media?,
reason: String?
): Single<Boolean>? {
if(context == null && media == null) {
return null
}
@ -86,7 +85,6 @@ class DeleteHelper @Inject constructor(
* @return
*/
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 calendar = Calendar.getInstance()
val fileDeleteString = """

View file

@ -2,21 +2,19 @@ package fr.free.nrw.commons.delete
import android.annotation.SuppressLint
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.R
import fr.free.nrw.commons.profile.achievements.FeedbackResponse
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
import fr.free.nrw.commons.utils.DateUtil
import fr.free.nrw.commons.utils.ViewUtilWrapper
import io.reactivex.Single
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.rx2.rxSingle
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
@ -29,6 +27,8 @@ class ReasonBuilder @Inject constructor(
private val viewUtilWrapper: ViewUtilWrapper
) {
private val defaultFileUsagePageSize = 10
/**
* To process the reason and append the media's upload date and uploaded_by_me string
* @param media
@ -39,7 +39,7 @@ class ReasonBuilder @Inject constructor(
if (media == null || reason == null) {
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> {
return if (checkAccount()) {
okHttpJsonApiClient
.getAchievements(sessionManager.userName)
.map { feedbackResponse -> appendArticlesUsed(feedbackResponse, media, reason) }
} else {
Single.just("")
private fun getAndAppendFileUsage(media: Media, reason: String): Single<String> {
return rxSingle(context = Dispatchers.IO) {
if (!checkAccount()) return@rxSingle ""
try {
val globalFileUsage = okHttpJsonApiClient.getGlobalFileUsages(
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
* @param feedBack object
* @param fileUsages No. of files/articles using this image
* @param media whose upload data is to be fetched
* @param reason
* @param reason string to be appended
*/
@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)
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) }
}

View file

@ -7,6 +7,7 @@ import android.speech.RecognizerIntent
import android.view.View
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.view.WindowCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
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.settings.Prefs
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.UploadMediaDetailAdapter
import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog
import fr.free.nrw.commons.utils.applyEdgeToEdgeTopPaddingInsets
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.functions.Consumer
import io.reactivex.schedulers.Schedulers
@ -87,6 +90,10 @@ class DescriptionEditActivity :
super.onCreate(savedInstanceState)
binding = ActivityDescriptionEditBinding.inflate(layoutInflater)
applyEdgeToEdgeBottomInsets(binding.btnEditSubmit)
WindowCompat.getInsetsController(window, window.decorView)
.isAppearanceLightStatusBars = false
binding.toolbar.applyEdgeToEdgeTopPaddingInsets()
setContentView(binding.root)
val bundle = intent.extras
@ -143,7 +150,7 @@ class DescriptionEditActivity :
this,
getString(titleStringID),
getString(messageStringId),
getString(android.R.string.ok),
getString(R.string.ok),
null
)
}

View file

@ -1,14 +1,25 @@
package fr.free.nrw.commons.di
import android.content.ContentProvider
import android.database.sqlite.SQLiteDatabase
import fr.free.nrw.commons.data.DBOpenHelper
import fr.free.nrw.commons.di.ApplicationlessInjection.Companion.getInstance
import javax.inject.Inject
abstract class CommonsDaggerContentProvider : ContentProvider() {
@JvmField
@Inject
var dbOpenHelper: DBOpenHelper? = null
override fun onCreate(): Boolean {
inject()
return true
}
fun requireDbOpenHelper(): DBOpenHelper = dbOpenHelper!!
fun requireDb(): SQLiteDatabase = requireDbOpenHelper().writableDatabase!!
private fun inject() {
val injection = getInstance(context!!)

View file

@ -7,6 +7,7 @@ import dagger.Provides
import fr.free.nrw.commons.BetaConstants
import fr.free.nrw.commons.BuildConfig
import fr.free.nrw.commons.OkHttpConnectionFactory
import fr.free.nrw.commons.CommonHeaderRequestInterceptor
import fr.free.nrw.commons.actions.PageEditClient
import fr.free.nrw.commons.actions.PageEditInterface
import fr.free.nrw.commons.actions.ThanksInterface
@ -60,6 +61,7 @@ class NetworkingModule {
.connectTimeout(120, TimeUnit.SECONDS)
.writeTimeout(120, TimeUnit.SECONDS)
.addInterceptor(httpLoggingInterceptor)
.addInterceptor(CommonHeaderRequestInterceptor())
.readTimeout(120, TimeUnit.SECONDS)
.cache(Cache(File(context.cacheDir, "okHttpCache"), OK_HTTP_CACHE_SIZE))
.build()

View file

@ -1,264 +0,0 @@
package fr.free.nrw.commons.explore;
import static androidx.viewpager.widget.ViewPager.SCROLL_STATE_IDLE;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentPagerAdapter;
import androidx.viewpager.widget.ViewPager.OnPageChangeListener;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.ViewPagerAdapter;
import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.databinding.FragmentExploreBinding;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.theme.BaseActivity;
import fr.free.nrw.commons.utils.ActivityUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import javax.inject.Inject;
import javax.inject.Named;
public class ExploreFragment extends CommonsDaggerSupportFragment {
private static final String FEATURED_IMAGES_CATEGORY = "Featured_pictures_on_Wikimedia_Commons";
private static final String MOBILE_UPLOADS_CATEGORY = "Uploaded_with_Mobile/Android";
private static final String EXPLORE_MAP = "Map";
private static final String MEDIA_DETAILS_FRAGMENT_TAG = "MediaDetailsFragment";
public FragmentExploreBinding binding;
ViewPagerAdapter viewPagerAdapter;
private ExploreListRootFragment featuredRootFragment;
private ExploreListRootFragment mobileRootFragment;
private ExploreMapRootFragment mapRootFragment;
@Inject
@Named("default_preferences")
public JsonKvStore applicationKvStore;
// Nearby map state (for if we came from Nearby fragment)
private double prevZoom;
private double prevLatitude;
private double prevLongitude;
public void setScroll(boolean canScroll) {
if (binding != null) {
binding.viewPager.setCanScroll(canScroll);
}
}
@NonNull
public static ExploreFragment newInstance() {
ExploreFragment fragment = new ExploreFragment();
fragment.setRetainInstance(true);
return fragment;
}
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
loadNearbyMapData();
binding = FragmentExploreBinding.inflate(inflater, container, false);
viewPagerAdapter = new ViewPagerAdapter(getChildFragmentManager(),
FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
binding.viewPager.setAdapter(viewPagerAdapter);
binding.viewPager.setId(R.id.viewPager);
binding.tabLayout.setupWithViewPager(binding.viewPager);
binding.viewPager.addOnPageChangeListener(new OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset,
int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
if (position == 2) {
binding.viewPager.setCanScroll(false);
} else {
binding.viewPager.setCanScroll(true);
}
}
@Override
public void onPageScrollStateChanged(int state) {
}
});
setTabs();
setHasOptionsMenu(true);
// if we came from 'Show in Explore' in Nearby, jump to Map tab
if (isCameFromNearbyMap()) {
binding.viewPager.setCurrentItem(2);
}
return binding.getRoot();
}
/**
* Sets the titles in the tabLayout and fragments in the viewPager
*/
public void setTabs() {
List<Fragment> fragmentList = new ArrayList<>();
List<String> titleList = new ArrayList<>();
Bundle featuredArguments = new Bundle();
featuredArguments.putString("categoryName", FEATURED_IMAGES_CATEGORY);
Bundle mobileArguments = new Bundle();
mobileArguments.putString("categoryName", MOBILE_UPLOADS_CATEGORY);
Bundle mapArguments = new Bundle();
mapArguments.putString("categoryName", EXPLORE_MAP);
// if we came from 'Show in Explore' in Nearby, pass on zoom and center to Explore map root
if (isCameFromNearbyMap()) {
mapArguments.putDouble("prev_zoom", prevZoom);
mapArguments.putDouble("prev_latitude", prevLatitude);
mapArguments.putDouble("prev_longitude", prevLongitude);
}
featuredRootFragment = new ExploreListRootFragment(featuredArguments);
mobileRootFragment = new ExploreListRootFragment(mobileArguments);
mapRootFragment = new ExploreMapRootFragment(mapArguments);
fragmentList.add(featuredRootFragment);
titleList.add(getString(R.string.explore_tab_title_featured).toUpperCase(Locale.ROOT));
fragmentList.add(mobileRootFragment);
titleList.add(getString(R.string.explore_tab_title_mobile).toUpperCase(Locale.ROOT));
fragmentList.add(mapRootFragment);
titleList.add(getString(R.string.explore_tab_title_map).toUpperCase(Locale.ROOT));
((MainActivity) getActivity()).showTabs();
((BaseActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false);
viewPagerAdapter.setTabData(fragmentList, titleList);
viewPagerAdapter.notifyDataSetChanged();
}
/**
* Fetch Nearby map camera data from fragment arguments if any.
*/
public void loadNearbyMapData() {
// get fragment arguments
if (getArguments() != null) {
prevZoom = getArguments().getDouble("prev_zoom");
prevLatitude = getArguments().getDouble("prev_latitude");
prevLongitude = getArguments().getDouble("prev_longitude");
}
}
/**
* Checks if fragment arguments contain data from Nearby map. if present, then the user
* navigated from Nearby using 'Show in Explore'.
*
* @return true if user navigated from Nearby map
**/
public boolean isCameFromNearbyMap() {
return prevZoom != 0.0 || prevLatitude != 0.0 || prevLongitude != 0.0;
}
public boolean onBackPressed() {
if (binding.tabLayout.getSelectedTabPosition() == 0) {
if (featuredRootFragment.backPressed()) {
((BaseActivity) getActivity()).getSupportActionBar()
.setDisplayHomeAsUpEnabled(false);
return true;
}
} else if (binding.tabLayout.getSelectedTabPosition() == 1) { //Mobile root fragment
if (mobileRootFragment.backPressed()) {
((BaseActivity) getActivity()).getSupportActionBar()
.setDisplayHomeAsUpEnabled(false);
return true;
}
} else { //explore map fragment
if (mapRootFragment.backPressed()) {
((BaseActivity) getActivity()).getSupportActionBar()
.setDisplayHomeAsUpEnabled(false);
return true;
}
}
return false;
}
/**
* This method inflates the menu in the toolbar
*/
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
// if logged in 'Show in Nearby' menu item is visible
if (applicationKvStore.getBoolean("login_skipped") == false) {
inflater.inflate(R.menu.explore_fragment_menu, menu);
MenuItem others = menu.findItem(R.id.list_item_show_in_nearby);
if (binding.viewPager.getCurrentItem() == 2) {
others.setVisible(true);
}
// if on Map tab, show all menu options, else only show search
binding.viewPager.addOnPageChangeListener(new OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset,
int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
others.setVisible((position == 2));
}
@Override
public void onPageScrollStateChanged(int state) {
if (state == SCROLL_STATE_IDLE && binding.viewPager.getCurrentItem() == 2) {
onPageSelected(2);
}
}
});
} else {
inflater.inflate(R.menu.menu_search, menu);
}
super.onCreateOptionsMenu(menu, inflater);
}
/**
* This method handles the logic on ItemSelect in toolbar menu Currently only 1 choice is
* available to open search page of the app
*/
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle item selection
switch (item.getItemId()) {
case R.id.action_search:
ActivityUtils.startActivityWithFlags(getActivity(), SearchActivity.class);
return true;
case R.id.list_item_show_in_nearby:
mapRootFragment.loadNearbyMapFromExplore();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public void onDestroy() {
super.onDestroy();
binding = null;
}
}

View file

@ -0,0 +1,227 @@
package fr.free.nrw.commons.explore
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.FragmentPagerAdapter
import androidx.viewpager.widget.ViewPager
import androidx.viewpager.widget.ViewPager.OnPageChangeListener
import fr.free.nrw.commons.R
import fr.free.nrw.commons.ViewPagerAdapter
import fr.free.nrw.commons.contributions.MainActivity
import fr.free.nrw.commons.databinding.FragmentExploreBinding
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
import fr.free.nrw.commons.kvstore.JsonKvStore
import fr.free.nrw.commons.theme.BaseActivity
import fr.free.nrw.commons.utils.ActivityUtils.startActivityWithFlags
import javax.inject.Inject
import javax.inject.Named
class ExploreFragment : CommonsDaggerSupportFragment() {
@JvmField
@Inject
@Named("default_preferences")
var applicationKvStore: JsonKvStore? = null
private var featuredRootFragment: ExploreListRootFragment? = null
private var mobileRootFragment: ExploreListRootFragment? = null
private var mapRootFragment: ExploreMapRootFragment? = null
private var prevZoom = 0.0
private var prevLatitude = 0.0
private var prevLongitude = 0.0
private var viewPagerAdapter: ViewPagerAdapter? = null
var binding: FragmentExploreBinding? = null
fun setScroll(canScroll: Boolean) {
if (binding != null) {
binding!!.viewPager.canScroll = canScroll
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
super.onCreate(savedInstanceState)
loadNearbyMapData()
binding = FragmentExploreBinding.inflate(inflater, container, false)
viewPagerAdapter = ViewPagerAdapter(
requireContext(), childFragmentManager,
FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT
)
binding!!.viewPager.adapter = viewPagerAdapter
binding!!.viewPager.id = R.id.viewPager
binding!!.tabLayout.setupWithViewPager(binding!!.viewPager)
binding!!.viewPager.addOnPageChangeListener(object : OnPageChangeListener {
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) = Unit
override fun onPageScrollStateChanged(state: Int) = Unit
override fun onPageSelected(position: Int) {
binding!!.viewPager.canScroll = position != 2
if (position == 2) {
mapRootFragment?.requestLocationIfNeeded()
}
}
})
setTabs()
setHasOptionsMenu(true)
// if we came from 'Show in Explore' in Nearby, jump to Map tab
if (isCameFromNearbyMap) {
binding!!.viewPager.currentItem = 2
}
return binding!!.root
}
/**
* Sets the titles in the tabLayout and fragments in the viewPager
*/
fun setTabs() {
val featuredArguments = Bundle()
featuredArguments.putString("categoryName", FEATURED_IMAGES_CATEGORY)
val mobileArguments = Bundle()
mobileArguments.putString("categoryName", MOBILE_UPLOADS_CATEGORY)
val mapArguments = Bundle()
mapArguments.putString("categoryName", EXPLORE_MAP)
// if we came from 'Show in Explore' in Nearby, pass on zoom and center to Explore map root
if (isCameFromNearbyMap) {
mapArguments.putDouble("prev_zoom", prevZoom)
mapArguments.putDouble("prev_latitude", prevLatitude)
mapArguments.putDouble("prev_longitude", prevLongitude)
}
featuredRootFragment = ExploreListRootFragment(featuredArguments)
mobileRootFragment = ExploreListRootFragment(mobileArguments)
mapRootFragment = ExploreMapRootFragment(mapArguments)
(activity as MainActivity).showTabs()
(activity as BaseActivity).supportActionBar!!.setDisplayHomeAsUpEnabled(false)
viewPagerAdapter!!.setTabs(
R.string.explore_tab_title_featured to featuredRootFragment!!,
R.string.explore_tab_title_mobile to mobileRootFragment!!,
R.string.explore_tab_title_map to mapRootFragment!!
)
viewPagerAdapter!!.notifyDataSetChanged()
}
/**
* Fetch Nearby map camera data from fragment arguments if any.
*/
private fun loadNearbyMapData() {
// get fragment arguments
if (arguments != null) {
with (requireArguments()) {
prevZoom = getDouble("prev_zoom")
prevLatitude = getDouble("prev_latitude")
prevLongitude = getDouble("prev_longitude")
}
}
}
/**
* Checks if fragment arguments contain data from Nearby map. if present, then the user
* navigated from Nearby using 'Show in Explore'.
*
* @return true if user navigated from Nearby map
*/
private val isCameFromNearbyMap: Boolean
get() = prevZoom != 0.0 || prevLatitude != 0.0 || prevLongitude != 0.0
fun onBackPressed(): Boolean {
if (binding!!.tabLayout.selectedTabPosition == 0) {
if (featuredRootFragment!!.backPressed()) {
(activity as BaseActivity).supportActionBar!!.setDisplayHomeAsUpEnabled(false)
return true
}
} else if (binding!!.tabLayout.selectedTabPosition == 1) { //Mobile root fragment
if (mobileRootFragment!!.backPressed()) {
(activity as BaseActivity).supportActionBar!!.setDisplayHomeAsUpEnabled(false)
return true
}
} else { //explore map fragment
if (mapRootFragment!!.backPressed()) {
(activity as BaseActivity).supportActionBar!!.setDisplayHomeAsUpEnabled(false)
return true
}
}
return false
}
/**
* This method inflates the menu in the toolbar
*/
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
// if logged in 'Show in Nearby' menu item is visible
if (applicationKvStore!!.getBoolean("login_skipped") == false) {
inflater.inflate(R.menu.explore_fragment_menu, menu)
val others = menu.findItem(R.id.list_item_show_in_nearby)
if (binding!!.viewPager.currentItem == 2) {
others.setVisible(true)
}
// if on Map tab, show all menu options, else only show search
binding!!.viewPager.addOnPageChangeListener(object : OnPageChangeListener {
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) = Unit
override fun onPageScrollStateChanged(state: Int) = Unit
override fun onPageSelected(position: Int) {
binding!!.viewPager.canScroll = position != 2
others.setVisible(position == 2)
if (position == 2) {
mapRootFragment?.requestLocationIfNeeded()
}
}
})
} else {
inflater.inflate(R.menu.menu_search, menu)
}
super.onCreateOptionsMenu(menu, inflater)
}
/**
* This method handles the logic on ItemSelect in toolbar menu Currently only 1 choice is
* available to open search page of the app
*/
override fun onOptionsItemSelected(item: MenuItem): Boolean {
// Handle item selection
when (item.itemId) {
R.id.action_search -> {
startActivityWithFlags(requireActivity(), SearchActivity::class.java)
return true
}
R.id.list_item_show_in_nearby -> {
mapRootFragment!!.loadNearbyMapFromExplore()
return true
}
else -> return super.onOptionsItemSelected(item)
}
}
override fun onDestroy() {
super.onDestroy()
binding = null
}
companion object {
private const val FEATURED_IMAGES_CATEGORY = "Featured_pictures_on_Wikimedia_Commons"
private const val MOBILE_UPLOADS_CATEGORY = "Uploaded_with_Mobile/Android"
private const val EXPLORE_MAP = "Map"
fun newInstance(): ExploreFragment = ExploreFragment().apply {
retainInstance = true
}
}
}

View file

@ -1,215 +0,0 @@
package fr.free.nrw.commons.explore;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.category.CategoryImagesCallback;
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.explore.categories.media.CategoriesMediaFragment;
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.navtab.NavTab;
public class ExploreListRootFragment extends CommonsDaggerSupportFragment implements
MediaDetailPagerFragment.MediaDetailProvider, CategoryImagesCallback {
private MediaDetailPagerFragment mediaDetails;
private CategoriesMediaFragment listFragment;
private FragmentFeaturedRootBinding binding;
public ExploreListRootFragment() {
//empty constructor necessary otherwise crashes on recreate
}
public ExploreListRootFragment(Bundle bundle) {
String title = bundle.getString("categoryName");
listFragment = new CategoriesMediaFragment();
Bundle featuredArguments = new Bundle();
featuredArguments.putString("categoryName", title);
listFragment.setArguments(featuredArguments);
}
@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) {
if (binding!=null) {
binding.exploreContainer.setVisibility(View.VISIBLE);
}
if (((ExploreFragment) getParentFragment()).binding!=null) {
((ExploreFragment) getParentFragment()).binding.tabLayout.setVisibility(View.GONE);
}
mediaDetails = MediaDetailPagerFragment.newInstance(false, true);
((ExploreFragment) getParentFragment()).setScroll(false);
setFragment(mediaDetails, listFragment);
mediaDetails.showImage(position);
}
/**
* 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 (listFragment != null) {
return listFragment.getMediaAtPosition(i);
} else {
return null;
}
}
/**
* 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 (listFragment != null) {
return listFragment.getTotalMediaCount();
} else {
return 0;
}
}
@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);
onMediaClicked(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();
}
}
/**
* Performs back pressed action on the fragment. Return true if the event was handled by the
* mediaDetails otherwise returns false.
*
* @return
*/
public boolean backPressed() {
if (null != mediaDetails && mediaDetails.isVisible()) {
if (((ExploreFragment) getParentFragment()).binding != null) {
((ExploreFragment) getParentFragment()).binding.tabLayout.setVisibility(View.VISIBLE);
}
removeFragment(mediaDetails);
((ExploreFragment) getParentFragment()).setScroll(true);
setFragment(listFragment, mediaDetails);
((MainActivity) getActivity()).showTabs();
return true;
} else {
if (((MainActivity) getActivity()) != null) {
((MainActivity) getActivity()).setSelectedItemId(NavTab.CONTRIBUTIONS.code());
}
}
if (((MainActivity) getActivity()) != null) {
((MainActivity) getActivity()).showTabs();
}
return false;
}
@Override
public void onDestroy() {
super.onDestroy();
binding = null;
}
}

View file

@ -0,0 +1,182 @@
package fr.free.nrw.commons.explore
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.R
import fr.free.nrw.commons.category.CategoryImagesCallback
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.explore.categories.media.CategoriesMediaFragment
import fr.free.nrw.commons.media.MediaDetailPagerFragment
import fr.free.nrw.commons.media.MediaDetailProvider
import fr.free.nrw.commons.navtab.NavTab
class ExploreListRootFragment : CommonsDaggerSupportFragment, MediaDetailProvider,
CategoryImagesCallback {
private var mediaDetails: MediaDetailPagerFragment? = null
private var listFragment: CategoriesMediaFragment? = null
private var binding: FragmentFeaturedRootBinding? = null
constructor()
constructor(bundle: Bundle) {
listFragment = CategoriesMediaFragment().apply {
arguments = bundleOf(
"categoryName" to bundle.getString("categoryName")
)
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
super.onCreate(savedInstanceState)
binding = FragmentFeaturedRootBinding.inflate(inflater, container, false)
return binding!!.root
}
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) {
childFragmentManager
.beginTransaction()
.hide(otherFragment)
.show(fragment)
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit()
childFragmentManager.executePendingTransactions()
} else if (fragment.isAdded && otherFragment == null) {
childFragmentManager
.beginTransaction()
.show(fragment)
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit()
childFragmentManager.executePendingTransactions()
} else if (!fragment.isAdded && otherFragment != null) {
childFragmentManager
.beginTransaction()
.hide(otherFragment)
.add(R.id.explore_container, fragment)
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit()
childFragmentManager.executePendingTransactions()
} else if (!fragment.isAdded) {
childFragmentManager
.beginTransaction()
.replace(R.id.explore_container, fragment)
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit()
childFragmentManager.executePendingTransactions()
}
}
private fun removeFragment(fragment: Fragment) {
childFragmentManager
.beginTransaction()
.remove(fragment)
.commit()
childFragmentManager.executePendingTransactions()
}
override fun onMediaClicked(position: Int) {
if (binding != null) {
binding!!.exploreContainer.visibility = View.VISIBLE
}
if ((parentFragment as ExploreFragment).binding != null) {
(parentFragment as ExploreFragment).binding!!.tabLayout.visibility =
View.GONE
}
mediaDetails = MediaDetailPagerFragment.newInstance(false, true)
(parentFragment as ExploreFragment).setScroll(false)
setFragment(mediaDetails!!, listFragment)
mediaDetails!!.showImage(position)
}
/**
* 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? = listFragment?.getMediaAtPosition(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 fun getTotalMediaCount(): Int = listFragment?.getTotalMediaCount() ?: 0
override fun getContributionStateAt(position: Int): Int? = 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!!)
onMediaClicked(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() {
mediaDetails?.notifyDataSetChanged()
}
/**
* Performs back pressed action on the fragment. Return true if the event was handled by the
* mediaDetails otherwise returns false.
*
* @return
*/
fun backPressed(): Boolean {
if (null != mediaDetails && mediaDetails!!.isVisible) {
if ((parentFragment as ExploreFragment).binding != null) {
(parentFragment as ExploreFragment).binding!!.tabLayout.visibility =
View.VISIBLE
}
removeFragment(mediaDetails!!)
(parentFragment as ExploreFragment).setScroll(true)
setFragment(listFragment!!, mediaDetails)
(activity as MainActivity).showTabs()
return true
} else {
if ((activity as MainActivity?) != null) {
(activity as MainActivity).setSelectedItemId(NavTab.CONTRIBUTIONS.code())
}
}
if ((activity as MainActivity?) != null) {
(activity as MainActivity).showTabs()
}
return false
}
override fun onDestroy() {
super.onDestroy()
binding = null
}
}

View file

@ -1,239 +0,0 @@
package fr.free.nrw.commons.explore;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.category.CategoryImagesCallback;
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.explore.map.ExploreMapFragment;
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.navtab.NavTab;
public class ExploreMapRootFragment extends CommonsDaggerSupportFragment implements
MediaDetailPagerFragment.MediaDetailProvider, CategoryImagesCallback {
private MediaDetailPagerFragment mediaDetails;
private ExploreMapFragment mapFragment;
private FragmentFeaturedRootBinding binding;
public ExploreMapRootFragment() {
//empty constructor necessary otherwise crashes on recreate
}
@NonNull
public static ExploreMapRootFragment newInstance() {
ExploreMapRootFragment fragment = new ExploreMapRootFragment();
fragment.setRetainInstance(true);
return fragment;
}
public ExploreMapRootFragment(Bundle bundle) {
// get fragment arguments
String title = bundle.getString("categoryName");
double zoom = bundle.getDouble("prev_zoom");
double latitude = bundle.getDouble("prev_latitude");
double longitude = bundle.getDouble("prev_longitude");
mapFragment = new ExploreMapFragment();
Bundle featuredArguments = new Bundle();
featuredArguments.putString("categoryName", title);
// if we came from 'Show in Explore' in Nearby, pass on zoom and center
if (zoom != 0.0 || latitude != 0.0 || longitude != 0.0) {
featuredArguments.putDouble("prev_zoom", zoom);
featuredArguments.putDouble("prev_latitude", latitude);
featuredArguments.putDouble("prev_longitude", longitude);
}
mapFragment.setArguments(featuredArguments);
}
@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(mapFragment, 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) {
binding.exploreContainer.setVisibility(View.VISIBLE);
((ExploreFragment) getParentFragment()).binding.tabLayout.setVisibility(View.GONE);
mediaDetails = MediaDetailPagerFragment.newInstance(false, true);
((ExploreFragment) getParentFragment()).setScroll(false);
setFragment(mediaDetails, mapFragment);
mediaDetails.showImage(position);
}
/**
* 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 (mapFragment != null && mapFragment.mediaList != null) {
return mapFragment.mediaList.get(i);
} else {
return null;
}
}
/**
* 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 (mapFragment != null && mapFragment.mediaList != null) {
return mapFragment.mediaList.size();
} else {
return 0;
}
}
@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 && !mapFragment.isVisible()) {
removeFragment(mediaDetails);
onMediaClicked(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();
}
}
/**
* Performs back pressed action on the fragment. Return true if the event was handled by the
* mediaDetails otherwise returns false.
*
* @return
*/
public boolean backPressed() {
if (null != mediaDetails && mediaDetails.isVisible()) {
((ExploreFragment) getParentFragment()).binding.tabLayout.setVisibility(View.VISIBLE);
removeFragment(mediaDetails);
((ExploreFragment) getParentFragment()).setScroll(true);
setFragment(mapFragment, mediaDetails);
((MainActivity) getActivity()).showTabs();
return true;
}
if (mapFragment != null && mapFragment.isVisible()) {
if (mapFragment.backButtonClicked()) {
// Explore map fragment handled the event no further action required.
return true;
} else {
((MainActivity) getActivity()).showTabs();
return false;
}
} else {
((MainActivity) getActivity()).setSelectedItemId(NavTab.CONTRIBUTIONS.code());
}
((MainActivity) getActivity()).showTabs();
return false;
}
public void loadNearbyMapFromExplore() {
mapFragment.loadNearbyMapFromExplore();
}
@Override
public void onDestroy() {
super.onDestroy();
binding = null;
}
}

View file

@ -0,0 +1,212 @@
package fr.free.nrw.commons.explore
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.R
import fr.free.nrw.commons.category.CategoryImagesCallback
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.explore.map.ExploreMapFragment
import fr.free.nrw.commons.media.MediaDetailPagerFragment
import fr.free.nrw.commons.media.MediaDetailProvider
import fr.free.nrw.commons.navtab.NavTab
class ExploreMapRootFragment : CommonsDaggerSupportFragment, MediaDetailProvider,
CategoryImagesCallback {
private var mediaDetails: MediaDetailPagerFragment? = null
private var mapFragment: ExploreMapFragment? = null
private var binding: FragmentFeaturedRootBinding? = null
constructor()
constructor(bundle: Bundle) {
// get fragment arguments
val title = bundle.getString("categoryName")
val zoom = bundle.getDouble("prev_zoom")
val latitude = bundle.getDouble("prev_latitude")
val longitude = bundle.getDouble("prev_longitude")
mapFragment = ExploreMapFragment()
val featuredArguments = bundleOf(
"categoryName" to title
)
// if we came from 'Show in Explore' in Nearby, pass on zoom and center
if (zoom != 0.0 || latitude != 0.0 || longitude != 0.0) {
featuredArguments.putDouble("prev_zoom", zoom)
featuredArguments.putDouble("prev_latitude", latitude)
featuredArguments.putDouble("prev_longitude", longitude)
}
mapFragment!!.arguments = featuredArguments
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
super.onCreate(savedInstanceState)
binding = FragmentFeaturedRootBinding.inflate(inflater, container, false)
return binding!!.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (savedInstanceState == null) {
setFragment(mapFragment!!, mediaDetails)
}
}
fun setFragment(fragment: Fragment, otherFragment: Fragment?) {
if (fragment.isAdded && otherFragment != null) {
childFragmentManager
.beginTransaction()
.hide(otherFragment)
.show(fragment)
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit()
childFragmentManager.executePendingTransactions()
} else if (fragment.isAdded && otherFragment == null) {
childFragmentManager
.beginTransaction()
.show(fragment)
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit()
childFragmentManager.executePendingTransactions()
} else if (!fragment.isAdded && otherFragment != null) {
childFragmentManager
.beginTransaction()
.hide(otherFragment)
.add(R.id.explore_container, fragment)
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit()
childFragmentManager.executePendingTransactions()
} else if (!fragment.isAdded) {
childFragmentManager
.beginTransaction()
.replace(R.id.explore_container, fragment)
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit()
childFragmentManager.executePendingTransactions()
}
}
private fun removeFragment(fragment: Fragment) {
childFragmentManager
.beginTransaction()
.remove(fragment)
.commit()
childFragmentManager.executePendingTransactions()
}
override fun onMediaClicked(position: Int) {
binding!!.exploreContainer.visibility = View.VISIBLE
(parentFragment as ExploreFragment).binding!!.tabLayout.visibility = View.GONE
mediaDetails = MediaDetailPagerFragment.newInstance(false, true)
(parentFragment as ExploreFragment).setScroll(false)
setFragment(mediaDetails!!, mapFragment)
mediaDetails!!.showImage(position)
}
/**
* 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? = mapFragment?.mediaList?.get(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 fun getTotalMediaCount(): Int = mapFragment?.mediaList?.size ?: 0
override fun getContributionStateAt(position: Int): Int? = 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 && !mapFragment!!.isVisible) {
removeFragment(mediaDetails!!)
onMediaClicked(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() {
mediaDetails?.notifyDataSetChanged()
}
/**
* Performs back pressed action on the fragment. Return true if the event was handled by the
* mediaDetails otherwise returns false.
*
* @return
*/
fun backPressed(): Boolean {
if (null != mediaDetails && mediaDetails!!.isVisible) {
(parentFragment as ExploreFragment).binding!!.tabLayout.visibility = View.VISIBLE
removeFragment(mediaDetails!!)
(parentFragment as ExploreFragment).setScroll(true)
setFragment(mapFragment!!, mediaDetails)
(activity as MainActivity).showTabs()
return true
}
if (mapFragment != null && mapFragment!!.isVisible) {
if (mapFragment!!.backButtonClicked()) {
// Explore map fragment handled the event no further action required.
return true
} else {
(activity as MainActivity).showTabs()
return false
}
} else {
(activity as MainActivity).setSelectedItemId(NavTab.CONTRIBUTIONS.code())
}
(activity as MainActivity).showTabs()
return false
}
fun loadNearbyMapFromExplore() = mapFragment?.loadNearbyMapFromExplore()
override fun onDestroy() {
super.onDestroy()
binding = null
}
fun requestLocationIfNeeded() {
mapFragment?.requestLocationIfNeeded()
}
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
super.setUserVisibleHint(isVisibleToUser)
if (isVisibleToUser) {
requestLocationIfNeeded()
}
}
companion object {
fun newInstance(): ExploreMapRootFragment = ExploreMapRootFragment().apply {
retainInstance = true
}
}
}

View file

@ -1,66 +0,0 @@
package fr.free.nrw.commons.explore;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import androidx.viewpager.widget.ViewPager;
/**
* ParentViewPager A custom viewPager whose scrolling can be enabled and disabled.
*/
public class ParentViewPager extends ViewPager {
/**
* Boolean variable that stores the current state of pager scroll i.e(enabled or disabled)
*/
private boolean canScroll = true;
/**
* Default constructors
*/
public ParentViewPager(Context context) {
super(context);
}
public ParentViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* Setter method for canScroll.
*/
public void setCanScroll(boolean canScroll) {
this.canScroll = canScroll;
}
/**
* Getter method for canScroll.
*/
public boolean isCanScroll() {
return canScroll;
}
/**
* Method that prevents scrolling if canScroll is set to false.
*/
@Override
public boolean onTouchEvent(MotionEvent ev) {
return canScroll && super.onTouchEvent(ev);
}
/**
* A facilitator method that allows parent to intercept touch events before its children. thus
* making it possible to prevent swiping parent on child end.
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return canScroll && super.onInterceptTouchEvent(ev);
}
}

View file

@ -0,0 +1,25 @@
package fr.free.nrw.commons.explore
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import androidx.viewpager.widget.ViewPager
/**
* ParentViewPager A custom viewPager whose scrolling can be enabled and disabled.
*/
class ParentViewPager : ViewPager {
var canScroll: Boolean = true
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
override fun onTouchEvent(ev: MotionEvent): Boolean {
return canScroll && super.onTouchEvent(ev)
}
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
return canScroll && super.onInterceptTouchEvent(ev)
}
}

View file

@ -1,290 +0,0 @@
package fr.free.nrw.commons.explore;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import com.jakewharton.rxbinding2.view.RxView;
import com.jakewharton.rxbinding2.widget.RxSearchView;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.ViewPagerAdapter;
import fr.free.nrw.commons.category.CategoryImagesCallback;
import fr.free.nrw.commons.databinding.ActivitySearchBinding;
import fr.free.nrw.commons.explore.categories.search.SearchCategoryFragment;
import fr.free.nrw.commons.explore.depictions.search.SearchDepictionsFragment;
import fr.free.nrw.commons.explore.media.SearchMediaFragment;
import fr.free.nrw.commons.explore.models.RecentSearch;
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao;
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesFragment;
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.theme.BaseActivity;
import fr.free.nrw.commons.utils.FragmentUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.android.schedulers.AndroidSchedulers;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import timber.log.Timber;
/**
* Represents search screen of this app
*/
public class SearchActivity extends BaseActivity
implements MediaDetailPagerFragment.MediaDetailProvider, CategoryImagesCallback {
@Inject
RecentSearchesDao recentSearchesDao;
private SearchMediaFragment searchMediaFragment;
private SearchCategoryFragment searchCategoryFragment;
private SearchDepictionsFragment searchDepictionsFragment;
private RecentSearchesFragment recentSearchesFragment;
private FragmentManager supportFragmentManager;
private MediaDetailPagerFragment mediaDetails;
ViewPagerAdapter viewPagerAdapter;
private ActivitySearchBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivitySearchBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
setTitle(getString(R.string.title_activity_search));
setSupportActionBar(binding.toolbarSearch);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
binding.toolbarSearch.setNavigationOnClickListener(v->onBackPressed());
supportFragmentManager = getSupportFragmentManager();
setSearchHistoryFragment();
viewPagerAdapter = new ViewPagerAdapter(getSupportFragmentManager());
binding.viewPager.setAdapter(viewPagerAdapter);
binding.viewPager.setOffscreenPageLimit(2); // Because we want all the fragments to be alive
binding.tabLayout.setupWithViewPager(binding.viewPager);
setTabs();
binding.searchBox.setQueryHint(getString(R.string.search_commons));
binding.searchBox.onActionViewExpanded();
binding.searchBox.clearFocus();
}
/**
* This method sets the search history fragment.
* Search history fragment is displayed when query is empty.
*/
private void setSearchHistoryFragment() {
recentSearchesFragment = new RecentSearchesFragment();
FragmentTransaction transaction = supportFragmentManager.beginTransaction();
transaction.add(R.id.searchHistoryContainer, recentSearchesFragment).commit();
}
/**
* Sets the titles in the tabLayout and fragments in the viewPager
*/
public void setTabs() {
List<Fragment> fragmentList = new ArrayList<>();
List<String> titleList = new ArrayList<>();
searchMediaFragment = new SearchMediaFragment();
searchDepictionsFragment = new SearchDepictionsFragment();
searchCategoryFragment= new SearchCategoryFragment();
fragmentList.add(searchMediaFragment);
titleList.add(getResources().getString(R.string.search_tab_title_media).toUpperCase(Locale.ROOT));
fragmentList.add(searchCategoryFragment);
titleList.add(getResources().getString(R.string.search_tab_title_categories).toUpperCase(Locale.ROOT));
fragmentList.add(searchDepictionsFragment);
titleList.add(getResources().getString(R.string.search_tab_title_depictions).toUpperCase(Locale.ROOT));
viewPagerAdapter.setTabData(fragmentList, titleList);
viewPagerAdapter.notifyDataSetChanged();
getCompositeDisposable().add(RxSearchView.queryTextChanges(binding.searchBox)
.takeUntil(RxView.detaches(binding.searchBox))
.debounce(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::handleSearch, Timber::e
));
}
private void handleSearch(final CharSequence query) {
if (!TextUtils.isEmpty(query)) {
saveRecentSearch(query.toString());
binding.viewPager.setVisibility(View.VISIBLE);
binding.tabLayout.setVisibility(View.VISIBLE);
binding.searchHistoryContainer.setVisibility(View.GONE);
if (FragmentUtils.isFragmentUIActive(searchDepictionsFragment)) {
searchDepictionsFragment.onQueryUpdated(query.toString());
}
if (FragmentUtils.isFragmentUIActive(searchMediaFragment)) {
searchMediaFragment.onQueryUpdated(query.toString());
}
if (FragmentUtils.isFragmentUIActive(searchCategoryFragment)) {
searchCategoryFragment.onQueryUpdated(query.toString());
}
}
else {
//Open RecentSearchesFragment
recentSearchesFragment.updateRecentSearches();
binding.viewPager.setVisibility(View.GONE);
binding.tabLayout.setVisibility(View.GONE);
setSearchHistoryFragment();
binding.searchHistoryContainer.setVisibility(View.VISIBLE);
}
}
private void saveRecentSearch(@NonNull final String query) {
final RecentSearch recentSearch = recentSearchesDao.find(query);
// Newly searched query...
if (recentSearch == null) {
recentSearchesDao.save(new RecentSearch(null, query, new Date()));
} else {
recentSearch.setLastSearched(new Date());
recentSearchesDao.save(recentSearch);
}
}
/**
* returns Media Object at position
* @param i position of Media in the imagesRecyclerView adapter.
*/
@Override
public Media getMediaAtPosition(int i) {
return searchMediaFragment.getMediaAtPosition(i);
}
/**
* returns total number of images present in the imagesRecyclerView adapter.
*/
@Override
public int getTotalMediaCount() {
return searchMediaFragment.getTotalMediaCount();
}
@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 (getSupportFragmentManager().getBackStackEntryCount() == 1) {
onBackPressed();
onMediaClicked(index);
}
}
/**
* This method is called on success of API call for image Search.
* The viewpager will notified that number of items have changed.
*/
@Override
public void viewPagerNotifyDataSetChanged() {
if (mediaDetails!=null){
mediaDetails.notifyDataSetChanged();
}
}
/**
* Open media detail pager fragment on click of image in search results
* @param index item index that should be opened
*/
@Override
public void onMediaClicked(int index) {
ViewUtil.hideKeyboard(this.findViewById(R.id.searchBox));
binding.tabLayout.setVisibility(View.GONE);
binding.viewPager.setVisibility(View.GONE);
binding.mediaContainer.setVisibility(View.VISIBLE);
binding.searchBox.setVisibility(View.GONE);// to remove searchview when mediaDetails fragment open
if (mediaDetails == null || !mediaDetails.isVisible()) {
// set isFeaturedImage true for featured images, to include author field on media detail
mediaDetails = MediaDetailPagerFragment.newInstance(false, true);
supportFragmentManager
.beginTransaction()
.hide(supportFragmentManager.getFragments().get(supportFragmentManager.getBackStackEntryCount()))
.add(R.id.mediaContainer, mediaDetails)
.addToBackStack(null)
.commit();
// Reason for using hide, add instead of replace is to maintain scroll position after
// coming back to the search activity. See https://github.com/commons-app/apps-android-commons/issues/1631
// https://stackoverflow.com/questions/11353075/how-can-i-maintain-fragment-state-when-added-to-the-back-stack/19022550#19022550
supportFragmentManager.executePendingTransactions();
}
mediaDetails.showImage(index);
}
/**
* This method is called on Screen Rotation
*/
@Override
protected void onResume() {
if (supportFragmentManager.getBackStackEntryCount()==1){
//FIXME: Temporary fix for screen rotation inside media details. If we don't call onBackPressed then fragment stack is increasing every time.
//FIXME: Similar issue like this https://github.com/commons-app/apps-android-commons/issues/894
// This is called on screen rotation when user is inside media details. Ideally it should show Media Details but since we are not saving the state now. We are throwing the user to search screen otherwise the app was crashing.
//
onBackPressed();
}
super.onResume();
}
/**
* This method is called on backPressed of anyFragment in the activity.
* If condition is called when mediaDetailFragment is opened.
*/
@Override
public void onBackPressed() {
//Remove the backstack entry that gets added when share button is clicked
//fixing:https://github.com/commons-app/apps-android-commons/issues/2296
if (getSupportFragmentManager().getBackStackEntryCount() == 2) {
supportFragmentManager
.beginTransaction()
.remove(mediaDetails)
.commit();
supportFragmentManager.popBackStack();
supportFragmentManager.executePendingTransactions();
}
if (getSupportFragmentManager().getBackStackEntryCount() == 1) {
// back to search so show search toolbar and hide navigation toolbar
binding.searchBox.setVisibility(View.VISIBLE);//set the searchview
binding.tabLayout.setVisibility(View.VISIBLE);
binding.viewPager.setVisibility(View.VISIBLE);
binding.mediaContainer.setVisibility(View.GONE);
} else {
binding.toolbarSearch.setVisibility(View.GONE);
}
super.onBackPressed();
}
/**
* This method is called on click of a recent search to update query in SearchView.
* @param query Recent Search Query
*/
public void updateText(String query) {
binding.searchBox.setQuery(query, true);
// Clear focus of searchView now. searchView.clearFocus(); does not seem to work Check the below link for more details.
// https://stackoverflow.com/questions/6117967/how-to-remove-focus-without-setting-focus-to-another-control/15481511
binding.viewPager.requestFocus();
}
@Override protected void onDestroy() {
super.onDestroy();
//Dispose the disposables when the activity is destroyed
getCompositeDisposable().dispose();
binding = null;
}
}

View file

@ -0,0 +1,254 @@
package fr.free.nrw.commons.explore
import android.os.Bundle
import android.text.TextUtils
import android.view.View
import androidx.fragment.app.FragmentManager
import com.jakewharton.rxbinding2.view.RxView
import com.jakewharton.rxbinding2.widget.RxSearchView
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.R
import fr.free.nrw.commons.ViewPagerAdapter
import fr.free.nrw.commons.category.CategoryImagesCallback
import fr.free.nrw.commons.databinding.ActivitySearchBinding
import fr.free.nrw.commons.explore.categories.search.SearchCategoryFragment
import fr.free.nrw.commons.explore.depictions.search.SearchDepictionsFragment
import fr.free.nrw.commons.explore.media.SearchMediaFragment
import fr.free.nrw.commons.explore.models.RecentSearch
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesFragment
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.utils.FragmentUtils.isFragmentUIActive
import fr.free.nrw.commons.utils.ViewUtil.hideKeyboard
import fr.free.nrw.commons.utils.applyEdgeToEdgeAllInsets
import io.reactivex.android.schedulers.AndroidSchedulers
import timber.log.Timber
import java.util.Date
import java.util.concurrent.TimeUnit
import javax.inject.Inject
/**
* Represents search screen of this app
*/
class SearchActivity : BaseActivity(), MediaDetailProvider, CategoryImagesCallback {
@JvmField
@Inject
var recentSearchesDao: RecentSearchesDao? = null
private var searchMediaFragment: SearchMediaFragment? = null
private var searchCategoryFragment: SearchCategoryFragment? = null
private var searchDepictionsFragment: SearchDepictionsFragment? = null
private var recentSearchesFragment: RecentSearchesFragment? = null
private var supportFragmentManager: FragmentManager? = null
private var mediaDetails: MediaDetailPagerFragment? = null
private var viewPagerAdapter: ViewPagerAdapter? = null
private var binding: ActivitySearchBinding? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySearchBinding.inflate(layoutInflater)
applyEdgeToEdgeAllInsets(binding!!.root)
setContentView(binding!!.root)
title = getString(R.string.title_activity_search)
setSupportActionBar(binding!!.toolbarSearch)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
binding!!.toolbarSearch.setNavigationOnClickListener { onBackPressed() }
supportFragmentManager = getSupportFragmentManager()
setSearchHistoryFragment()
viewPagerAdapter = ViewPagerAdapter(this, getSupportFragmentManager())
binding!!.viewPager.adapter = viewPagerAdapter
binding!!.viewPager.offscreenPageLimit = 2 // Because we want all the fragments to be alive
binding!!.tabLayout.setupWithViewPager(binding!!.viewPager)
setTabs()
binding!!.searchBox.queryHint = getString(R.string.search_commons)
binding!!.searchBox.onActionViewExpanded()
binding!!.searchBox.clearFocus()
}
/**
* This method sets the search history fragment.
* Search history fragment is displayed when query is empty.
*/
private fun setSearchHistoryFragment() {
recentSearchesFragment = RecentSearchesFragment()
val transaction = supportFragmentManager!!.beginTransaction()
transaction.add(R.id.searchHistoryContainer, recentSearchesFragment!!).commit()
}
/**
* Sets the titles in the tabLayout and fragments in the viewPager
*/
fun setTabs() {
searchMediaFragment = SearchMediaFragment()
searchDepictionsFragment = SearchDepictionsFragment()
searchCategoryFragment = SearchCategoryFragment()
viewPagerAdapter!!.setTabs(
R.string.search_tab_title_media to searchMediaFragment!!,
R.string.search_tab_title_categories to searchCategoryFragment!!,
R.string.search_tab_title_depictions to searchDepictionsFragment!!
)
viewPagerAdapter!!.notifyDataSetChanged()
compositeDisposable.add(
RxSearchView.queryTextChanges(binding!!.searchBox)
.takeUntil(RxView.detaches(binding!!.searchBox))
.debounce(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(::handleSearch, Timber::e)
)
}
private fun handleSearch(query: CharSequence) {
if (!TextUtils.isEmpty(query)) {
saveRecentSearch(query.toString())
binding!!.viewPager.visibility = View.VISIBLE
binding!!.tabLayout.visibility = View.VISIBLE
binding!!.searchHistoryContainer.visibility = View.GONE
if (isFragmentUIActive(searchDepictionsFragment)) {
searchDepictionsFragment!!.onQueryUpdated(query.toString())
}
if (isFragmentUIActive(searchMediaFragment)) {
searchMediaFragment!!.onQueryUpdated(query.toString())
}
if (isFragmentUIActive(searchCategoryFragment)) {
searchCategoryFragment!!.onQueryUpdated(query.toString())
}
} else {
//Open RecentSearchesFragment
recentSearchesFragment!!.updateRecentSearches()
binding!!.viewPager.visibility = View.GONE
binding!!.tabLayout.visibility = View.GONE
setSearchHistoryFragment()
binding!!.searchHistoryContainer.visibility = View.VISIBLE
}
}
private fun saveRecentSearch(query: String) {
val recentSearch = recentSearchesDao!!.find(query)
// Newly searched query...
if (recentSearch == null) {
recentSearchesDao!!.save(RecentSearch(null, query, Date()))
} else {
recentSearch.lastSearched = Date()
recentSearchesDao!!.save(recentSearch)
}
}
override fun getMediaAtPosition(i: Int): Media? = searchMediaFragment!!.getMediaAtPosition(i)
override fun getTotalMediaCount(): Int = searchMediaFragment!!.getTotalMediaCount()
override fun getContributionStateAt(position: Int): Int? = null
/**
* Reload media detail fragment once media is nominated
*
* @param index item position that has been nominated
*/
override fun refreshNominatedMedia(index: Int) {
if (getSupportFragmentManager().backStackEntryCount == 1) {
onBackPressed()
onMediaClicked(index)
}
}
/**
* This method is called on success of API call for image Search.
* The viewpager will notified that number of items have changed.
*/
override fun viewPagerNotifyDataSetChanged() {
mediaDetails?.notifyDataSetChanged()
}
/**
* Open media detail pager fragment on click of image in search results
* @param position item index that should be opened
*/
override fun onMediaClicked(position: Int) {
hideKeyboard(findViewById(R.id.searchBox))
binding!!.tabLayout.visibility = View.GONE
binding!!.viewPager.visibility = View.GONE
binding!!.mediaContainer.visibility = View.VISIBLE
binding!!.searchBox.visibility =
View.GONE // to remove searchview when mediaDetails fragment open
if (mediaDetails == null || !mediaDetails!!.isVisible) {
// set isFeaturedImage true for featured images, to include author field on media detail
mediaDetails = MediaDetailPagerFragment.newInstance(false, true)
supportFragmentManager!!
.beginTransaction()
.hide(supportFragmentManager!!.fragments[supportFragmentManager!!.backStackEntryCount])
.add(R.id.mediaContainer, mediaDetails!!)
.addToBackStack(null)
.commit()
// Reason for using hide, add instead of replace is to maintain scroll position after
// coming back to the search activity. See https://github.com/commons-app/apps-android-commons/issues/1631
// https://stackoverflow.com/questions/11353075/how-can-i-maintain-fragment-state-when-added-to-the-back-stack/19022550#19022550
supportFragmentManager!!.executePendingTransactions()
}
mediaDetails!!.showImage(position)
}
/**
* This method is called on Screen Rotation
*/
override fun onResume() {
if (supportFragmentManager!!.backStackEntryCount == 1) {
//FIXME: Temporary fix for screen rotation inside media details. If we don't call onBackPressed then fragment stack is increasing every time.
//FIXME: Similar issue like this https://github.com/commons-app/apps-android-commons/issues/894
// This is called on screen rotation when user is inside media details. Ideally it should show Media Details but since we are not saving the state now. We are throwing the user to search screen otherwise the app was crashing.
onBackPressed()
}
super.onResume()
}
/**
* This method is called on backPressed of anyFragment in the activity.
* If condition is called when mediaDetailFragment is opened.
*/
override fun onBackPressed() {
//Remove the backstack entry that gets added when share button is clicked
//fixing:https://github.com/commons-app/apps-android-commons/issues/2296
if (getSupportFragmentManager().backStackEntryCount == 2) {
supportFragmentManager!!
.beginTransaction()
.remove(mediaDetails!!)
.commit()
supportFragmentManager!!.popBackStack()
supportFragmentManager!!.executePendingTransactions()
}
if (getSupportFragmentManager().backStackEntryCount == 1) {
// back to search so show search toolbar and hide navigation toolbar
binding!!.searchBox.visibility = View.VISIBLE //set the searchview
binding!!.tabLayout.visibility = View.VISIBLE
binding!!.viewPager.visibility = View.VISIBLE
binding!!.mediaContainer.visibility = View.GONE
} else {
binding!!.toolbarSearch.visibility = View.GONE
}
super.onBackPressed()
}
/**
* This method is called on click of a recent search to update query in SearchView.
* @param query Recent Search Query
*/
fun updateText(query: String?) {
binding!!.searchBox.setQuery(query, true)
// Clear focus of searchView now. searchView.clearFocus(); does not seem to work Check the below link for more details.
// https://stackoverflow.com/questions/6117967/how-to-remove-focus-without-setting-focus-to-another-control/15481511
binding!!.viewPager.requestFocus()
}
override fun onDestroy() {
super.onDestroy()
//Dispose the disposables when the activity is destroyed
compositeDisposable.dispose()
binding = null
}
}

View file

@ -7,6 +7,6 @@ import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
abstract class PageableDepictionsFragment : BasePagingFragment<DepictedItem>() {
override val errorTextId: Int = R.string.error_loading_depictions
override val pagedListAdapter by lazy {
DepictionAdapter { WikidataItemDetailsActivity.startYourself(context, it) }
DepictionAdapter { WikidataItemDetailsActivity.startYourself(requireContext(), it) }
}
}

View file

@ -1,309 +0,0 @@
package fr.free.nrw.commons.explore.depictions;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.FrameLayout;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.viewpager.widget.ViewPager;
import com.google.android.material.snackbar.Snackbar;
import com.google.android.material.tabs.TabLayout;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.ViewPagerAdapter;
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao;
import fr.free.nrw.commons.category.CategoryImagesCallback;
import fr.free.nrw.commons.databinding.ActivityWikidataItemDetailsBinding;
import fr.free.nrw.commons.explore.depictions.child.ChildDepictionsFragment;
import fr.free.nrw.commons.explore.depictions.media.DepictedImagesFragment;
import fr.free.nrw.commons.explore.depictions.parent.ParentDepictionsFragment;
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.theme.BaseActivity;
import fr.free.nrw.commons.upload.structure.depictions.DepictModel;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import fr.free.nrw.commons.wikidata.WikidataConstants;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
/**
* Activity to show depiction media, parent classes and child classes of depicted items in Explore
*/
public class WikidataItemDetailsActivity extends BaseActivity implements MediaDetailPagerFragment.MediaDetailProvider,
CategoryImagesCallback {
private FragmentManager supportFragmentManager;
private DepictedImagesFragment depictionImagesListFragment;
private MediaDetailPagerFragment mediaDetailPagerFragment;
/**
* Name of the depicted item
* Ex: Rabbit
*/
@Inject BookmarkItemsDao bookmarkItemsDao;
private CompositeDisposable compositeDisposable;
@Inject
DepictModel depictModel;
private String wikidataItemName;
private ActivityWikidataItemDetailsBinding binding;
ViewPagerAdapter viewPagerAdapter;
private DepictedItem wikidataItem;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityWikidataItemDetailsBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
compositeDisposable = new CompositeDisposable();
supportFragmentManager = getSupportFragmentManager();
viewPagerAdapter = new ViewPagerAdapter(getSupportFragmentManager());
binding.viewPager.setAdapter(viewPagerAdapter);
binding.viewPager.setOffscreenPageLimit(2);
binding.tabLayout.setupWithViewPager(binding.viewPager);
final DepictedItem depictedItem = getIntent().getParcelableExtra(
WikidataConstants.BOOKMARKS_ITEMS);
wikidataItem = depictedItem;
setSupportActionBar(binding.toolbarBinding.toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
setTabs();
setPageTitle();
}
/**
* Gets the passed wikidataItemName from the intents and displays it as the page title
*/
private void setPageTitle() {
if (getIntent() != null && getIntent().getStringExtra("wikidataItemName") != null) {
setTitle(getIntent().getStringExtra("wikidataItemName"));
}
}
/**
* This method is called on success of API call for featured Images.
* The viewpager will notified that number of items have changed.
*/
@Override
public void viewPagerNotifyDataSetChanged() {
if (mediaDetailPagerFragment !=null){
mediaDetailPagerFragment.notifyDataSetChanged();
}
}
/**
* This activity contains 3 tabs and a viewpager. This method is used to set the titles of tab,
* Set the fragments according to the tab selected in the viewPager.
*/
private void setTabs() {
List<Fragment> fragmentList = new ArrayList<>();
List<String> titleList = new ArrayList<>();
depictionImagesListFragment = new DepictedImagesFragment();
ChildDepictionsFragment childDepictionsFragment = new ChildDepictionsFragment();
ParentDepictionsFragment parentDepictionsFragment = new ParentDepictionsFragment();
wikidataItemName = getIntent().getStringExtra("wikidataItemName");
String entityId = getIntent().getStringExtra("entityId");
if (getIntent() != null && wikidataItemName != null) {
Bundle arguments = new Bundle();
arguments.putString("wikidataItemName", wikidataItemName);
arguments.putString("entityId", entityId);
depictionImagesListFragment.setArguments(arguments);
parentDepictionsFragment.setArguments(arguments);
childDepictionsFragment.setArguments(arguments);
}
fragmentList.add(depictionImagesListFragment);
titleList.add(getResources().getString(R.string.title_for_media));
fragmentList.add(childDepictionsFragment);
titleList.add(getResources().getString(R.string.title_for_child_classes));
fragmentList.add(parentDepictionsFragment);
titleList.add(getResources().getString(R.string.title_for_parent_classes));
viewPagerAdapter.setTabData(fragmentList, titleList);
binding.viewPager.setOffscreenPageLimit(2);
viewPagerAdapter.notifyDataSetChanged();
}
/**
* Shows media detail fragment when user clicks on any image in the list
*/
@Override
public void onMediaClicked(int position) {
binding.tabLayout.setVisibility(View.GONE);
binding.viewPager.setVisibility(View.GONE);
binding.mediaContainer.setVisibility(View.VISIBLE);
if (mediaDetailPagerFragment == null || !mediaDetailPagerFragment.isVisible()) {
// set isFeaturedImage true for featured images, to include author field on media detail
mediaDetailPagerFragment = MediaDetailPagerFragment.newInstance(false, true);
FragmentManager supportFragmentManager = getSupportFragmentManager();
supportFragmentManager
.beginTransaction()
.replace(R.id.mediaContainer, mediaDetailPagerFragment)
.addToBackStack(null)
.commit();
supportFragmentManager.executePendingTransactions();
}
mediaDetailPagerFragment.showImage(position);
}
/**
* 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) {
return depictionImagesListFragment.getMediaAtPosition(i);
}
/**
* This method is called on backPressed of anyFragment in the activity.
* If condition is called when mediaDetailFragment is opened.
*/
@Override
public void onBackPressed() {
if (supportFragmentManager.getBackStackEntryCount() == 1){
binding.tabLayout.setVisibility(View.VISIBLE);
binding.viewPager.setVisibility(View.VISIBLE);
binding.mediaContainer.setVisibility(View.GONE);
}
super.onBackPressed();
}
/**
* 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() {
return depictionImagesListFragment.getTotalMediaCount();
}
@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 (getSupportFragmentManager().getBackStackEntryCount() == 1) {
onBackPressed();
onMediaClicked(index);
}
}
/**
* Consumers should be simply using this method to use this activity.
*
* @param context A Context of the application package implementing this class.
* @param depictedItem Name of the depicts for displaying its details
*/
public static void startYourself(Context context, DepictedItem depictedItem) {
Intent intent = new Intent(context, WikidataItemDetailsActivity.class);
intent.putExtra("wikidataItemName", depictedItem.getName());
intent.putExtra("entityId", depictedItem.getId());
intent.putExtra(WikidataConstants.BOOKMARKS_ITEMS, depictedItem);
context.startActivity(intent);
}
/**
* This function inflates the menu
*/
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater menuInflater=getMenuInflater();
menuInflater.inflate(R.menu.menu_wikidata_item,menu);
updateBookmarkState(menu.findItem(R.id.menu_bookmark_current_item));
return super.onCreateOptionsMenu(menu);
}
/**
* This method handles the logic on item select in toolbar menu
* Currently only 1 choice is available to open Wikidata item details page in browser
*/
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()){
case R.id.browser_actions_menu_items:
String entityId=getIntent().getStringExtra("entityId");
Uri uri = Uri.parse("https://www.wikidata.org/wiki/" + entityId);
Utils.handleWebUrl(this, uri);
return true;
case R.id.menu_bookmark_current_item:
if(getIntent().getStringExtra("fragment") != null) {
compositeDisposable.add(depictModel.getDepictions(
getIntent().getStringExtra("entityId")
).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(depictedItems -> {
final boolean bookmarkExists = bookmarkItemsDao.updateBookmarkItem(
depictedItems.get(0));
final Snackbar snackbar
= bookmarkExists ? Snackbar.make(findViewById(R.id.toolbar_layout),
R.string.add_bookmark, Snackbar.LENGTH_LONG)
: Snackbar.make(findViewById(R.id.toolbar_layout),
R.string.remove_bookmark,
Snackbar.LENGTH_LONG);
snackbar.show();
updateBookmarkState(item);
}));
} else {
final boolean bookmarkExists
= bookmarkItemsDao.updateBookmarkItem(wikidataItem);
final Snackbar snackbar
= bookmarkExists ? Snackbar.make(findViewById(R.id.toolbar_layout),
R.string.add_bookmark, Snackbar.LENGTH_LONG)
: Snackbar.make(findViewById(R.id.toolbar_layout), R.string.remove_bookmark,
Snackbar.LENGTH_LONG);
snackbar.show();
updateBookmarkState(item);
}
return true;
case android.R.id.home:
onBackPressed();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
private void updateBookmarkState(final MenuItem item) {
final boolean isBookmarked;
if(getIntent().getStringExtra("fragment") != null) {
isBookmarked
= bookmarkItemsDao.findBookmarkItem(getIntent().getStringExtra("entityId"));
} else {
isBookmarked = bookmarkItemsDao.findBookmarkItem(wikidataItem.getId());
}
final int icon
= isBookmarked ? R.drawable.menu_ic_round_star_filled_24px
: R.drawable.menu_ic_round_star_border_24px;
item.setIcon(icon);
}
}

View file

@ -0,0 +1,297 @@
package fr.free.nrw.commons.explore.depictions
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.core.os.bundleOf
import androidx.fragment.app.FragmentManager
import com.google.android.material.snackbar.Snackbar
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.R
import fr.free.nrw.commons.ViewPagerAdapter
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao
import fr.free.nrw.commons.category.CategoryImagesCallback
import fr.free.nrw.commons.databinding.ActivityWikidataItemDetailsBinding
import fr.free.nrw.commons.explore.depictions.child.ChildDepictionsFragment
import fr.free.nrw.commons.explore.depictions.media.DepictedImagesFragment
import fr.free.nrw.commons.explore.depictions.parent.ParentDepictionsFragment
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.upload.structure.depictions.DepictModel
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import fr.free.nrw.commons.utils.applyEdgeToEdgeAllInsets
import fr.free.nrw.commons.utils.handleWebUrl
import fr.free.nrw.commons.wikidata.WikidataConstants
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.functions.Consumer
import io.reactivex.schedulers.Schedulers
import javax.inject.Inject
/**
* Activity to show depiction media, parent classes and child classes of depicted items in Explore
*/
class WikidataItemDetailsActivity : BaseActivity(), MediaDetailProvider, CategoryImagesCallback {
@JvmField
@Inject
var bookmarkItemsDao: BookmarkItemsDao? = null
@JvmField
@Inject
var depictModel: DepictModel? = null
private var supportFragmentManager: FragmentManager? = null
private var depictionImagesListFragment: DepictedImagesFragment? = null
private var mediaDetailPagerFragment: MediaDetailPagerFragment? = null
private var binding: ActivityWikidataItemDetailsBinding? = null
var viewPagerAdapter: ViewPagerAdapter? = null
private var wikidataItem: DepictedItem? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityWikidataItemDetailsBinding.inflate(layoutInflater)
applyEdgeToEdgeAllInsets(binding!!.root)
setContentView(binding!!.root)
supportFragmentManager = getSupportFragmentManager()
viewPagerAdapter = ViewPagerAdapter(this, getSupportFragmentManager())
binding!!.viewPager.adapter = viewPagerAdapter
binding!!.viewPager.offscreenPageLimit = 2
binding!!.tabLayout.setupWithViewPager(binding!!.viewPager)
wikidataItem = intent.getParcelableExtra(WikidataConstants.BOOKMARKS_ITEMS)
setSupportActionBar(binding!!.toolbarBinding.toolbar)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
setTabs()
setPageTitle()
}
/**
* Gets the passed wikidataItemName from the intents and displays it as the page title
*/
private fun setPageTitle() {
if (intent != null && intent.getStringExtra("wikidataItemName") != null) {
title = intent.getStringExtra("wikidataItemName")
}
}
/**
* This method is called on success of API call for featured Images.
* The viewpager will notified that number of items have changed.
*/
override fun viewPagerNotifyDataSetChanged() {
if (mediaDetailPagerFragment != null) {
mediaDetailPagerFragment!!.notifyDataSetChanged()
}
}
/**
* This activity contains 3 tabs and a viewpager. This method is used to set the titles of tab,
* Set the fragments according to the tab selected in the viewPager.
*/
private fun setTabs() {
depictionImagesListFragment = DepictedImagesFragment()
val childDepictionsFragment = ChildDepictionsFragment()
val parentDepictionsFragment = ParentDepictionsFragment()
val wikidataItemName = intent.getStringExtra("wikidataItemName")
val entityId = intent.getStringExtra("entityId")
if (intent != null && wikidataItemName != null) {
val arguments = bundleOf(
"wikidataItemName" to wikidataItemName,
"entityId" to entityId
)
depictionImagesListFragment!!.arguments = arguments
parentDepictionsFragment.arguments = arguments
childDepictionsFragment.arguments = arguments
}
viewPagerAdapter!!.setTabs(
R.string.title_for_media to depictionImagesListFragment!!,
R.string.title_for_child_classes to childDepictionsFragment,
R.string.title_for_parent_classes to parentDepictionsFragment
)
binding!!.viewPager.offscreenPageLimit = 2
viewPagerAdapter!!.notifyDataSetChanged()
}
/**
* Shows media detail fragment when user clicks on any image in the list
*/
override fun onMediaClicked(position: Int) {
binding!!.tabLayout.visibility = View.GONE
binding!!.viewPager.visibility = View.GONE
binding!!.mediaContainer.visibility = View.VISIBLE
if (mediaDetailPagerFragment == null || !mediaDetailPagerFragment!!.isVisible) {
// set isFeaturedImage true for featured images, to include author field on media detail
mediaDetailPagerFragment = MediaDetailPagerFragment.newInstance(false, true)
val supportFragmentManager = getSupportFragmentManager()
supportFragmentManager
.beginTransaction()
.replace(R.id.mediaContainer, mediaDetailPagerFragment!!)
.addToBackStack(null)
.commit()
supportFragmentManager.executePendingTransactions()
}
mediaDetailPagerFragment!!.showImage(position)
}
/**
* 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? {
return depictionImagesListFragment!!.getMediaAtPosition(i)
}
/**
* This method is called on backPressed of anyFragment in the activity.
* If condition is called when mediaDetailFragment is opened.
*/
override fun onBackPressed() {
if (supportFragmentManager!!.backStackEntryCount == 1) {
binding!!.tabLayout.visibility = View.VISIBLE
binding!!.viewPager.visibility = View.VISIBLE
binding!!.mediaContainer.visibility = View.GONE
}
super.onBackPressed()
}
/**
* 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 = depictionImagesListFragment!!.getTotalMediaCount()
override fun getContributionStateAt(position: Int): Int? = null
/**
* Reload media detail fragment once media is nominated
*
* @param index item position that has been nominated
*/
override fun refreshNominatedMedia(index: Int) {
if (getSupportFragmentManager().backStackEntryCount == 1) {
onBackPressed()
onMediaClicked(index)
}
}
/**
* This function inflates the menu
*/
override fun onCreateOptionsMenu(menu: Menu): Boolean {
val menuInflater = menuInflater
menuInflater.inflate(R.menu.menu_wikidata_item, menu)
updateBookmarkState(menu.findItem(R.id.menu_bookmark_current_item))
return super.onCreateOptionsMenu(menu)
}
/**
* This method handles the logic on item select in toolbar menu
* Currently only 1 choice is available to open Wikidata item details page in browser
*/
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.browser_actions_menu_items -> {
val entityId = intent.getStringExtra("entityId")
val uri = Uri.parse("https://www.wikidata.org/wiki/$entityId")
handleWebUrl(this, uri)
return true
}
R.id.menu_bookmark_current_item -> {
if (intent.getStringExtra("fragment") != null) {
compositeDisposable!!.add(
depictModel!!.getDepictions(
intent.getStringExtra("entityId")!!
).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(Consumer<List<DepictedItem?>> { depictedItems: List<DepictedItem?> ->
val bookmarkExists = bookmarkItemsDao!!.updateBookmarkItem(
depictedItems[0]!!
)
val snackbar = if (bookmarkExists)
Snackbar.make(
findViewById(R.id.toolbar_layout),
R.string.add_bookmark, Snackbar.LENGTH_LONG
)
else
Snackbar.make(
findViewById(R.id.toolbar_layout),
R.string.remove_bookmark,
Snackbar.LENGTH_LONG
)
snackbar.show()
updateBookmarkState(item)
})
)
} else {
val bookmarkExists = bookmarkItemsDao!!.updateBookmarkItem(wikidataItem!!)
val snackbar = if (bookmarkExists)
Snackbar.make(
findViewById(R.id.toolbar_layout),
R.string.add_bookmark, Snackbar.LENGTH_LONG
)
else
Snackbar.make(
findViewById(R.id.toolbar_layout), R.string.remove_bookmark,
Snackbar.LENGTH_LONG
)
snackbar.show()
updateBookmarkState(item)
}
return true
}
android.R.id.home -> {
onBackPressed()
return true
}
else -> return super.onOptionsItemSelected(item)
}
}
private fun updateBookmarkState(item: MenuItem) {
val isBookmarked: Boolean = if (intent.getStringExtra("fragment") != null) {
bookmarkItemsDao!!.findBookmarkItem(intent.getStringExtra("entityId"))
} else {
bookmarkItemsDao!!.findBookmarkItem(wikidataItem!!.id)
}
item.setIcon(if (isBookmarked) {
R.drawable.menu_ic_round_star_filled_24px
} else {
R.drawable.menu_ic_round_star_border_24px
})
}
companion object {
/**
* Consumers should be simply using this method to use this activity.
*
* @param context A Context of the application package implementing this class.
* @param depictedItem Name of the depicts for displaying its details
*/
fun startYourself(context: Context, depictedItem: DepictedItem) {
val intent = Intent(context, WikidataItemDetailsActivity::class.java).apply {
putExtra("wikidataItemName", depictedItem.name)
putExtra("entityId", depictedItem.id)
putExtra(WikidataConstants.BOOKMARKS_ITEMS, depictedItem)
}
context.startActivity(intent)
}
}
}

View file

@ -1,31 +0,0 @@
package fr.free.nrw.commons.explore.map;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.media.MediaClient;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
@Singleton
public class ExploreMapCalls {
@Inject
MediaClient mediaClient;
@Inject
public ExploreMapCalls() {
}
/**
* Calls method to query Commons for uploads around a location
*
* @param currentLatLng coordinates of search location
* @return list of places obtained
*/
List<Media> callCommonsQuery(final LatLng currentLatLng) {
String coordinates = currentLatLng.getLatitude() + "|" + currentLatLng.getLongitude();
return mediaClient.getMediaListFromGeoSearch(coordinates).blockingGet();
}
}

View file

@ -0,0 +1,25 @@
package fr.free.nrw.commons.explore.map
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.location.LatLng
import fr.free.nrw.commons.media.MediaClient
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class ExploreMapCalls @Inject constructor() {
@Inject
@JvmField
var mediaClient: MediaClient? = null
/**
* Calls method to query Commons for uploads around a location
*
* @param currentLatLng coordinates of search location
* @return list of places obtained
*/
fun callCommonsQuery(currentLatLng: LatLng): List<Media> {
val coordinates = currentLatLng.latitude.toString() + "|" + currentLatLng.longitude
return mediaClient!!.getMediaListFromGeoSearch(coordinates).blockingGet()
}
}

View file

@ -1,45 +0,0 @@
package fr.free.nrw.commons.explore.map;
import android.content.Context;
import fr.free.nrw.commons.BaseMarker;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.location.LocationServiceManager;
import java.util.List;
public class ExploreMapContract {
interface View {
boolean isNetworkConnectionEstablished();
void populatePlaces(LatLng curlatLng);
void askForLocationPermission();
void recenterMap(LatLng curLatLng);
void hideBottomDetailsSheet();
LatLng getMapCenter();
LatLng getMapFocus();
LatLng getLastMapFocus();
void addMarkersToMap(final List<BaseMarker> nearbyBaseMarkers);
void clearAllMarkers();
void addSearchThisAreaButtonAction();
void setSearchThisAreaButtonVisibility(boolean isVisible);
void setProgressBarVisibility(boolean isVisible);
boolean isDetailsBottomSheetVisible();
boolean isSearchThisAreaButtonVisible();
Context getContext();
LatLng getLastLocation();
void disableFABRecenter();
void enableFABRecenter();
void setFABRecenterAction(android.view.View.OnClickListener onClickListener);
boolean backButtonClicked();
}
interface UserActions {
void updateMap(LocationServiceManager.LocationChangeType locationChangeType);
void lockUnlockNearby(boolean isNearbyLocked);
void attachView(View view);
void detachView();
void setActionListeners(JsonKvStore applicationKvStore);
boolean backButtonClicked();
}
}

View file

@ -0,0 +1,43 @@
package fr.free.nrw.commons.explore.map
import android.content.Context
import android.view.View
import fr.free.nrw.commons.BaseMarker
import fr.free.nrw.commons.kvstore.JsonKvStore
import fr.free.nrw.commons.location.LatLng
import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType
class ExploreMapContract {
interface View {
fun isNetworkConnectionEstablished(): Boolean
fun populatePlaces(curlatLng: LatLng?)
fun askForLocationPermission()
fun recenterMap(curLatLng: LatLng?)
fun hideBottomDetailsSheet()
fun getMapCenter(): LatLng?
fun getMapFocus(): LatLng?
fun getLastMapFocus(): LatLng?
fun addMarkersToMap(nearbyBaseMarkers: List<BaseMarker?>?)
fun clearAllMarkers()
fun addSearchThisAreaButtonAction()
fun setSearchThisAreaButtonVisibility(isVisible: Boolean)
fun setProgressBarVisibility(isVisible: Boolean)
fun isDetailsBottomSheetVisible(): Boolean
fun isSearchThisAreaButtonVisible(): Boolean
fun getContext(): Context?
fun getLastLocation(): LatLng?
fun disableFABRecenter()
fun enableFABRecenter()
fun setFABRecenterAction(onClickListener: android.view.View.OnClickListener?)
fun backButtonClicked(): Boolean
}
interface UserActions {
fun updateMap(locationChangeType: LocationChangeType)
fun lockUnlockNearby(isNearbyLocked: Boolean)
fun attachView(view: View?)
fun detachView()
fun setActionListeners(applicationKvStore: JsonKvStore?)
fun backButtonClicked(): Boolean
}
}

View file

@ -1,213 +0,0 @@
package fr.free.nrw.commons.explore.map;
import static fr.free.nrw.commons.utils.LengthUtils.computeDistanceBetween;
import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestOptions;
import com.bumptech.glide.request.target.CustomTarget;
import com.bumptech.glide.request.transition.Transition;
import fr.free.nrw.commons.BaseMarker;
import fr.free.nrw.commons.MapController;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.utils.ImageUtils;
import fr.free.nrw.commons.utils.LocationUtils;
import fr.free.nrw.commons.utils.PlaceUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import timber.log.Timber;
public class ExploreMapController extends MapController {
private final ExploreMapCalls exploreMapCalls;
public LatLng latestSearchLocation; // Can be current and camera target on search this area button is used
public LatLng currentLocation; // current location of user
public double latestSearchRadius = 0; // Any last search radius
public double currentLocationSearchRadius = 0; // Search radius of only searches around current location
@Inject
public ExploreMapController(ExploreMapCalls explorePlaces) {
this.exploreMapCalls = explorePlaces;
}
/**
* Takes location as parameter and returns ExplorePlaces info that holds currentLatLng, mediaList,
* explorePlaceList and boundaryCoordinates
*
* @param currentLatLng is current geolocation
* @param searchLatLng is the location that we want to search around
* @param checkingAroundCurrentLocation is a boolean flag. True if we want to check around
* current location, false if another location
* @return explorePlacesInfo info that holds currentLatLng, mediaList, explorePlaceList and
* boundaryCoordinates
*/
public ExplorePlacesInfo loadAttractionsFromLocation(LatLng currentLatLng, LatLng searchLatLng,
boolean checkingAroundCurrentLocation) {
if (searchLatLng == null) {
Timber.d("Loading attractions explore map, but search is null");
return null;
}
ExplorePlacesInfo explorePlacesInfo = new ExplorePlacesInfo();
try {
explorePlacesInfo.currentLatLng = currentLatLng;
latestSearchLocation = searchLatLng;
List<Media> mediaList = exploreMapCalls.callCommonsQuery(searchLatLng);
LatLng[] boundaryCoordinates = {mediaList.get(0).getCoordinates(), // south
mediaList.get(0).getCoordinates(), // north
mediaList.get(0).getCoordinates(), // west
mediaList.get(0).getCoordinates()};// east, init with a random location
if (searchLatLng != null) {
Timber.d("Sorting places by distance...");
final Map<Media, Double> distances = new HashMap<>();
for (Media media : mediaList) {
distances.put(media,
computeDistanceBetween(media.getCoordinates(), searchLatLng));
// Find boundaries with basic find max approach
if (media.getCoordinates().getLatitude()
< boundaryCoordinates[0].getLatitude()) {
boundaryCoordinates[0] = media.getCoordinates();
}
if (media.getCoordinates().getLatitude()
> boundaryCoordinates[1].getLatitude()) {
boundaryCoordinates[1] = media.getCoordinates();
}
if (media.getCoordinates().getLongitude()
< boundaryCoordinates[2].getLongitude()) {
boundaryCoordinates[2] = media.getCoordinates();
}
if (media.getCoordinates().getLongitude()
> boundaryCoordinates[3].getLongitude()) {
boundaryCoordinates[3] = media.getCoordinates();
}
}
}
explorePlacesInfo.mediaList = mediaList;
explorePlacesInfo.explorePlaceList = PlaceUtils.mediaToExplorePlace(mediaList);
explorePlacesInfo.boundaryCoordinates = boundaryCoordinates;
// Sets latestSearchRadius to maximum distance among boundaries and search location
for (LatLng bound : boundaryCoordinates) {
double distance = LocationUtils.calculateDistance(bound.getLatitude(),
bound.getLongitude(), searchLatLng.getLatitude(), searchLatLng.getLongitude());
if (distance > latestSearchRadius) {
latestSearchRadius = distance;
}
}
// Our radius searched around us, will be used to understand when user search their own location, we will follow them
if (checkingAroundCurrentLocation) {
currentLocationSearchRadius = latestSearchRadius;
currentLocation = currentLatLng;
}
} catch (Exception e) {
e.printStackTrace();
}
return explorePlacesInfo;
}
/**
* Loads attractions from location for map view, we need to return places in Place data type
*
* @return baseMarkerOptions list that holds nearby places with their icons
*/
public static List<BaseMarker> loadAttractionsFromLocationToBaseMarkerOptions(
LatLng currentLatLng,
final List<Place> placeList,
Context context,
NearbyBaseMarkerThumbCallback callback,
ExplorePlacesInfo explorePlacesInfo) {
List<BaseMarker> baseMarkerList = new ArrayList<>();
if (placeList == null) {
return baseMarkerList;
}
VectorDrawableCompat vectorDrawable = null;
try {
vectorDrawable = VectorDrawableCompat.create(
context.getResources(), R.drawable.ic_custom_map_marker_dark, context.getTheme());
} catch (Resources.NotFoundException e) {
// ignore when running tests.
}
if (vectorDrawable != null) {
for (Place explorePlace : placeList) {
final BaseMarker baseMarker = new BaseMarker();
String distance = formatDistanceBetween(currentLatLng, explorePlace.location);
explorePlace.setDistance(distance);
baseMarker.setTitle(
explorePlace.name.substring(5, explorePlace.name.lastIndexOf(".")));
baseMarker.setPosition(
new fr.free.nrw.commons.location.LatLng(
explorePlace.location.getLatitude(),
explorePlace.location.getLongitude(), 0));
baseMarker.setPlace(explorePlace);
Glide.with(context)
.asBitmap()
.load(explorePlace.getThumb())
.placeholder(R.drawable.image_placeholder_96)
.apply(new RequestOptions().override(96, 96).centerCrop())
.into(new CustomTarget<Bitmap>() {
// We add icons to markers when bitmaps are ready
@Override
public void onResourceReady(@NonNull Bitmap resource,
@Nullable Transition<? super Bitmap> transition) {
baseMarker.setIcon(
ImageUtils.addRedBorder(resource, 6, context));
baseMarkerList.add(baseMarker);
if (baseMarkerList.size()
== placeList.size()) { // if true, we added all markers to list and can trigger thumbs ready callback
callback.onNearbyBaseMarkerThumbsReady(baseMarkerList,
explorePlacesInfo);
}
}
@Override
public void onLoadCleared(@Nullable Drawable placeholder) {
}
// We add thumbnail icon for images that couldn't be loaded
@Override
public void onLoadFailed(@Nullable final Drawable errorDrawable) {
super.onLoadFailed(errorDrawable);
baseMarker.fromResource(context, R.drawable.image_placeholder_96);
baseMarkerList.add(baseMarker);
if (baseMarkerList.size()
== placeList.size()) { // if true, we added all markers to list and can trigger thumbs ready callback
callback.onNearbyBaseMarkerThumbsReady(baseMarkerList,
explorePlacesInfo);
}
}
});
}
}
return baseMarkerList;
}
interface NearbyBaseMarkerThumbCallback {
// Callback to notify thumbnails of explore markers are added as icons and ready
void onNearbyBaseMarkerThumbsReady(List<BaseMarker> baseMarkers,
ExplorePlacesInfo explorePlacesInfo);
}
}

View file

@ -0,0 +1,219 @@
package fr.free.nrw.commons.explore.map
import android.content.Context
import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import fr.free.nrw.commons.BaseMarker
import fr.free.nrw.commons.MapController
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.R
import fr.free.nrw.commons.location.LatLng
import fr.free.nrw.commons.nearby.Place
import fr.free.nrw.commons.utils.ImageUtils.addRedBorder
import fr.free.nrw.commons.utils.LengthUtils.computeDistanceBetween
import fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween
import fr.free.nrw.commons.utils.LocationUtils.calculateDistance
import fr.free.nrw.commons.utils.PlaceUtils.mediaToExplorePlace
import timber.log.Timber
import javax.inject.Inject
class ExploreMapController @Inject constructor(
private val exploreMapCalls: ExploreMapCalls
) : MapController() {
// Can be current and camera target on search this area button is used
private var latestSearchLocation: LatLng? = null
// Any last search radius
private var latestSearchRadius: Double = 0.0
// Search radius of only searches around current location
private var currentLocationSearchRadius: Double = 0.0
@JvmField
// current location of user
var currentLocation: LatLng? = null
/**
* Takes location as parameter and returns ExplorePlaces info that holds currentLatLng, mediaList,
* explorePlaceList and boundaryCoordinates
*
* @param currentLatLng is current geolocation
* @param searchLatLng is the location that we want to search around
* @param checkingAroundCurrentLocation is a boolean flag. True if we want to check around
* current location, false if another location
* @return explorePlacesInfo info that holds currentLatLng, mediaList, explorePlaceList and
* boundaryCoordinates
*/
fun loadAttractionsFromLocation(
currentLatLng: LatLng?, searchLatLng: LatLng?,
checkingAroundCurrentLocation: Boolean
): ExplorePlacesInfo? {
if (searchLatLng == null) {
Timber.d("Loading attractions explore map, but search is null")
return null
}
val explorePlacesInfo = ExplorePlacesInfo()
try {
explorePlacesInfo.currentLatLng = currentLatLng
latestSearchLocation = searchLatLng
val mediaList = exploreMapCalls.callCommonsQuery(searchLatLng)
val boundaryCoordinates = arrayOf(
mediaList[0].coordinates!!, // south
mediaList[0].coordinates!!, // north
mediaList[0].coordinates!!, // west
mediaList[0].coordinates!!
) // east, init with a random location
Timber.d("Sorting places by distance...")
val distances: MutableMap<Media, Double> = HashMap()
for (media in mediaList) {
distances[media] = computeDistanceBetween(media.coordinates!!, searchLatLng)
// Find boundaries with basic find max approach
if (media.coordinates!!.latitude
< boundaryCoordinates[0]!!.latitude
) {
boundaryCoordinates[0] = media.coordinates!!
}
if (media.coordinates!!.latitude
> boundaryCoordinates[1]!!.latitude
) {
boundaryCoordinates[1] = media.coordinates!!
}
if (media.coordinates!!.longitude
< boundaryCoordinates[2]!!.longitude
) {
boundaryCoordinates[2] = media.coordinates!!
}
if (media.coordinates!!.longitude
> boundaryCoordinates[3]!!.longitude
) {
boundaryCoordinates[3] = media.coordinates!!
}
}
explorePlacesInfo.mediaList = mediaList
explorePlacesInfo.explorePlaceList = mediaToExplorePlace(mediaList)
explorePlacesInfo.boundaryCoordinates = boundaryCoordinates
// Sets latestSearchRadius to maximum distance among boundaries and search location
for ((latitude, longitude) in boundaryCoordinates) {
val distance = calculateDistance(
latitude,
longitude, searchLatLng.latitude, searchLatLng.longitude
)
if (distance > latestSearchRadius) {
latestSearchRadius = distance
}
}
// Our radius searched around us, will be used to understand when user search their own location, we will follow them
if (checkingAroundCurrentLocation) {
currentLocationSearchRadius = latestSearchRadius
currentLocation = currentLatLng
}
} catch (e: Exception) {
Timber.e(e)
}
return explorePlacesInfo
}
interface NearbyBaseMarkerThumbCallback {
// Callback to notify thumbnails of explore markers are added as icons and ready
fun onNearbyBaseMarkerThumbsReady(
baseMarkers: List<BaseMarker>?,
explorePlacesInfo: ExplorePlacesInfo?
)
}
companion object {
/**
* Loads attractions from location for map view, we need to return places in Place data type
*
* @return baseMarkerOptions list that holds nearby places with their icons
*/
fun loadAttractionsFromLocationToBaseMarkerOptions(
currentLatLng: LatLng?,
placeList: List<Place>?,
context: Context,
callback: NearbyBaseMarkerThumbCallback,
explorePlacesInfo: ExplorePlacesInfo?
): List<BaseMarker> {
val baseMarkerList: MutableList<BaseMarker> = ArrayList()
if (placeList == null) {
return baseMarkerList
}
var vectorDrawable: VectorDrawableCompat? = null
try {
vectorDrawable = VectorDrawableCompat.create(
context.resources, R.drawable.ic_custom_map_marker_dark, context.theme
)
} catch (e: Resources.NotFoundException) {
// ignore when running tests.
}
if (vectorDrawable != null) {
for (explorePlace in placeList) {
val baseMarker = BaseMarker()
val distance = formatDistanceBetween(currentLatLng, explorePlace.location)
explorePlace.setDistance(distance)
baseMarker.title =
explorePlace.name.substring(5, explorePlace.name.lastIndexOf("."))
baseMarker.position = LatLng(
explorePlace.location.latitude,
explorePlace.location.longitude, 0f
)
baseMarker.place = explorePlace
Glide.with(context)
.asBitmap()
.load(explorePlace.thumb)
.placeholder(R.drawable.image_placeholder_96)
.apply(RequestOptions().override(96, 96).centerCrop())
.into(object : CustomTarget<Bitmap>() {
// We add icons to markers when bitmaps are ready
override fun onResourceReady(
resource: Bitmap,
transition: Transition<in Bitmap>?
) {
baseMarker.icon = addRedBorder(resource, 6, context)
baseMarkerList.add(baseMarker)
if (baseMarkerList.size == placeList.size) {
// if true, we added all markers to list and can trigger thumbs ready callback
callback.onNearbyBaseMarkerThumbsReady(
baseMarkerList,
explorePlacesInfo
)
}
}
override fun onLoadCleared(placeholder: Drawable?) = Unit
// We add thumbnail icon for images that couldn't be loaded
override fun onLoadFailed(errorDrawable: Drawable?) {
super.onLoadFailed(errorDrawable)
baseMarker.fromResource(context, R.drawable.image_placeholder_96)
baseMarkerList.add(baseMarker)
if (baseMarkerList.size == placeList.size) {
// if true, we added all markers to list and can trigger thumbs ready callback
callback.onNearbyBaseMarkerThumbsReady(
baseMarkerList,
explorePlacesInfo
)
}
}
})
}
}
return baseMarkerList
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,240 +0,0 @@
package fr.free.nrw.commons.explore.map;
import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED;
import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.SEARCH_CUSTOM_AREA;
import android.location.Location;
import android.view.View;
import fr.free.nrw.commons.BaseMarker;
import fr.free.nrw.commons.MapController;
import fr.free.nrw.commons.MapController.ExplorePlacesInfo;
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao;
import fr.free.nrw.commons.explore.map.ExploreMapController.NearbyBaseMarkerThumbCallback;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType;
import io.reactivex.Observable;
import java.lang.reflect.Proxy;
import java.util.List;
import timber.log.Timber;
public class ExploreMapPresenter
implements ExploreMapContract.UserActions,
NearbyBaseMarkerThumbCallback {
BookmarkLocationsDao bookmarkLocationDao;
private boolean isNearbyLocked;
private LatLng currentLatLng;
private ExploreMapController exploreMapController;
private static final ExploreMapContract.View DUMMY = (ExploreMapContract.View) Proxy
.newProxyInstance(
ExploreMapContract.View.class.getClassLoader(),
new Class[]{ExploreMapContract.View.class}, (proxy, method, args) -> {
if (method.getName().equals("onMyEvent")) {
return null;
} else if (String.class == method.getReturnType()) {
return "";
} else if (Integer.class == method.getReturnType()) {
return Integer.valueOf(0);
} else if (int.class == method.getReturnType()) {
return 0;
} else if (Boolean.class == method.getReturnType()) {
return Boolean.FALSE;
} else if (boolean.class == method.getReturnType()) {
return false;
} else {
return null;
}
}
);
private ExploreMapContract.View exploreMapFragmentView = DUMMY;
public ExploreMapPresenter(BookmarkLocationsDao bookmarkLocationDao) {
this.bookmarkLocationDao = bookmarkLocationDao;
}
@Override
public void updateMap(LocationChangeType locationChangeType) {
Timber.d("Presenter updates map and list" + locationChangeType.toString());
if (isNearbyLocked) {
Timber.d("Nearby is locked, so updateMapAndList returns");
return;
}
if (!exploreMapFragmentView.isNetworkConnectionEstablished()) {
Timber.d("Network connection is not established");
return;
}
/**
* Significant changed - Markers and current location will be updated together
* Slightly changed - Only current position marker will be updated
*/
if (locationChangeType.equals(LOCATION_SIGNIFICANTLY_CHANGED)) {
Timber.d("LOCATION_SIGNIFICANTLY_CHANGED");
LatLng populateLatLng = exploreMapFragmentView.getMapCenter();
//If "Show in Explore" was selected in Nearby, use the previous LatLng
if (exploreMapFragmentView instanceof ExploreMapFragment) {
ExploreMapFragment exploreMapFragment = (ExploreMapFragment)exploreMapFragmentView;
if (exploreMapFragment.recentlyCameFromNearbyMap()) {
//Ensure this LatLng will not be used again if user searches their GPS location
exploreMapFragment.setRecentlyCameFromNearbyMap(false);
populateLatLng = exploreMapFragment.getPreviousLatLng();
}
}
lockUnlockNearby(true);
exploreMapFragmentView.setProgressBarVisibility(true);
exploreMapFragmentView.populatePlaces(populateLatLng);
} else if (locationChangeType.equals(SEARCH_CUSTOM_AREA)) {
Timber.d("SEARCH_CUSTOM_AREA");
lockUnlockNearby(true);
exploreMapFragmentView.setProgressBarVisibility(true);
exploreMapFragmentView.populatePlaces(exploreMapFragmentView.getMapFocus());
} else { // Means location changed slightly, ie user is walking or driving.
Timber.d("Means location changed slightly");
}
}
/**
* Nearby updates takes time, since they are network operations. During update time, we don't
* want to get any other calls from user. So locking nearby.
*
* @param isNearbyLocked true means lock, false means unlock
*/
@Override
public void lockUnlockNearby(boolean isNearbyLocked) {
this.isNearbyLocked = isNearbyLocked;
if (isNearbyLocked) {
exploreMapFragmentView.disableFABRecenter();
} else {
exploreMapFragmentView.enableFABRecenter();
}
}
@Override
public void attachView(ExploreMapContract.View view) {
exploreMapFragmentView = view;
}
@Override
public void detachView() {
exploreMapFragmentView = DUMMY;
}
/**
* Sets click listener of FAB
*/
@Override
public void setActionListeners(JsonKvStore applicationKvStore) {
exploreMapFragmentView.setFABRecenterAction(v -> {
exploreMapFragmentView.recenterMap(currentLatLng);
});
}
@Override
public boolean backButtonClicked() {
return exploreMapFragmentView.backButtonClicked();
}
public void onMapReady(ExploreMapController exploreMapController) {
this.exploreMapController = exploreMapController;
if (null != exploreMapFragmentView) {
exploreMapFragmentView.addSearchThisAreaButtonAction();
initializeMapOperations();
}
}
public void initializeMapOperations() {
lockUnlockNearby(false);
updateMap(LOCATION_SIGNIFICANTLY_CHANGED);
}
public Observable<ExplorePlacesInfo> loadAttractionsFromLocation(LatLng currentLatLng,
LatLng searchLatLng, boolean checkingAroundCurrent) {
return Observable
.fromCallable(() -> exploreMapController
.loadAttractionsFromLocation(currentLatLng, searchLatLng, checkingAroundCurrent));
}
/**
* Populates places for custom location, should be used for finding nearby places around a
* location where you are not at.
*
* @param explorePlacesInfo This variable has placeToCenter list information and distances.
*/
public void updateMapMarkers(
MapController.ExplorePlacesInfo explorePlacesInfo) {
if (explorePlacesInfo.mediaList != null) {
prepareNearbyBaseMarkers(explorePlacesInfo);
} else {
lockUnlockNearby(false); // So that new location updates wont come
exploreMapFragmentView.setProgressBarVisibility(false);
}
}
void prepareNearbyBaseMarkers(MapController.ExplorePlacesInfo explorePlacesInfo) {
exploreMapController
.loadAttractionsFromLocationToBaseMarkerOptions(explorePlacesInfo.currentLatLng,
// Curlatlang will be used to calculate distances
explorePlacesInfo.explorePlaceList,
exploreMapFragmentView.getContext(),
this,
explorePlacesInfo);
}
@Override
public void onNearbyBaseMarkerThumbsReady(List<BaseMarker> baseMarkers,
ExplorePlacesInfo explorePlacesInfo) {
if (null != exploreMapFragmentView) {
exploreMapFragmentView.addMarkersToMap(baseMarkers);
lockUnlockNearby(false); // So that new location updates wont come
exploreMapFragmentView.setProgressBarVisibility(false);
}
}
public View.OnClickListener onSearchThisAreaClicked() {
return v -> {
// Lock map operations during search this area operation
exploreMapFragmentView.setSearchThisAreaButtonVisibility(false);
if (searchCloseToCurrentLocation()) {
updateMap(LOCATION_SIGNIFICANTLY_CHANGED);
} else {
updateMap(SEARCH_CUSTOM_AREA);
}
};
}
/**
* Returns true if search this area button is used around our current location, so that we can
* continue following our current location again
*
* @return Returns true if search this area button is used around our current location
*/
public boolean searchCloseToCurrentLocation() {
if (null == exploreMapFragmentView.getLastMapFocus()) {
return true;
}
Location mylocation = new Location("");
Location dest_location = new Location("");
dest_location.setLatitude(exploreMapFragmentView.getMapFocus().getLatitude());
dest_location.setLongitude(exploreMapFragmentView.getMapFocus().getLongitude());
mylocation.setLatitude(exploreMapFragmentView.getLastMapFocus().getLatitude());
mylocation.setLongitude(exploreMapFragmentView.getLastMapFocus().getLongitude());
Float distance = mylocation.distanceTo(dest_location);
if (distance > 2000.0 * 3 / 4) {
return false;
} else {
return true;
}
}
}

Some files were not shown because too many files have changed in this diff Show more