Compare commits

...

502 commits
v4.3.0 ... main

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
Ritika Pahwa
ca5c7ec966
Bump up version code to 1053
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
2025-06-21 13:21:05 +05:30
translatewiki.net
9eff9e8e82
Localisation updates from https://translatewiki.net.
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
2025-06-19 14:01:44 +02:00
Rohit Verma
5665bc7f93
fix: make userName private to prevent conflict when passing arguments (#6339)
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
2025-06-17 17:33:43 +09:00
translatewiki.net
20e5df7d49
Localisation updates from https://translatewiki.net.
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
2025-06-16 14:01:42 +02:00
translatewiki.net
d3ae925567
Localisation updates from https://translatewiki.net.
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
2025-06-12 14:01:42 +02:00
translatewiki.net
af82cb2123
Localisation updates from https://translatewiki.net.
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
2025-06-12 06:17:24 +02:00
translatewiki.net
7df52e3f9c
Localisation updates from https://translatewiki.net. 2025-06-12 06:06:17 +02:00
translatewiki.net
6b40560dfc
Localisation updates from https://translatewiki.net. 2025-06-12 05:27:59 +02:00
translatewiki.net
54bb789461
Localisation updates from https://translatewiki.net. 2025-06-12 05:20:46 +02:00
translatewiki.net
7979be17c1
Localisation updates from https://translatewiki.net. 2025-06-12 05:05:50 +02:00
translatewiki.net
91564a1dff
Localisation updates from https://translatewiki.net. 2025-06-12 04:36:42 +02:00
Sonal Yadav
2b5f0e4ac9
drop down menu in the Upload Wizard now show the language in which the pin's label is shown (#6346)
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
2025-06-11 21:11:49 +09:00
translatewiki.net
9b04031c91
Localisation updates from https://translatewiki.net.
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
2025-06-09 14:01:52 +02:00
Ritika Pahwa
8ff52e6815
Fix crash on app startup by bumping up room database version
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
2025-06-08 19:16:01 +05:30
Ritika Pahwa
c41b5cc9da
Add v5.4.1 to CHANGELOG.md
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
2025-06-08 12:38:14 +05:30
Ritika Pahwa
767b625289
Bump up version code to 1052 2025-06-08 12:35:24 +05:30
Sonal Yadav
f45f26e602
Implement single selection logic in custom image picker (#6341)
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
* build failure cause

* Fix image selector logic in custom picker
2025-06-07 23:09:47 +09:00
translatewiki.net
06a613e855
Localisation updates from https://translatewiki.net.
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
2025-06-05 14:02:03 +02:00
Jason-Whitmore
62c5231dc9
ExploreMapFragment.java: modify Overlay onItemSingleTapUp code to place Overlay above other Overlays (#6334)
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
Before these changes, when a user tapped on an icon in the Explore Map, the icon and label would often
appear underneath other icons, making it difficult or impossible to read the text on the label.

These changes add two methods that search for and move the icon/label Overlays above all other icons and overlays.
These two methods are called when the user taps on an icon.

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2025-06-05 17:06:55 +09:00
Saifuddin Adenwala
7a224a9120
Fix invalid resource directory causing test failure (#6337) 2025-06-05 16:47:09 +09:00
translatewiki.net
593335aea3
Localisation updates from https://translatewiki.net.
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
2025-06-03 17:59:39 +02:00
Dev Jadiya
6edc6a22e4
Refactor long log line in SingleWebViewActivity to comply with code style (#6333)
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
2025-06-03 09:30:52 +09:00
translatewiki.net
230604f5ef
Localisation updates from https://translatewiki.net.
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
2025-06-02 14:02:04 +02:00
Jason-Whitmore
73f5200c2d
ExploreMapFragment.java: fix removeMarker code to correctly find the specified marker (#6325)
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
Before this change, removeMarker() would search for the correct Overlay by doing a linear
search and matching the BaseMarker's Place name with the Overlay's title.
This stopped working once the Overlay's title text was changed to be more user friendly.

This change implements a more robust solution. A map is used to directly associate
BaseMarkers with the matching Overlay. The overlay can now be removed from the
overlay list without using any of the information contained within the BaseMarker
or Overlay.

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2025-05-31 12:05:33 +09:00
translatewiki.net
95b8ac74b9
Localisation updates from https://translatewiki.net.
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
2025-05-29 14:01:52 +02:00
rayane
cfc2cfcca1
Fix Kotlin warnings (related to issue #5996) (#6320)
* refactor: replace unused exception variable with underscore

* refactor: code style fix

* fix: remove unnecessary latLng variable and return null directly

* fix: use explicit true/false instead of isFABsExpanded

* refactor: simplify marker update logic by reducing redundant conditions

* refactor: use data object instead of object

* fix: check placeBindings for null instead of checking each destructured value

* fix: check placeBindings for null instead of checking each destructured value

* docs: fix KDoc for contentUri property by removing incorrect @param tag

* docs: correct @see link in KDoc for showBadgesWithCount

* docs: fix KDoc tag from @property to @param for context in BottomSheetAdapter

* docs: comment out KDoc for disabled method to avoid unresolved @param warning

* docs: fix KDoc for onLongPress by removing invalid @param imageUri

* docs: clean up invalid KDoc tags on property and add docs to isEmpty

* docs: remove invalid @param originalImageCoordinates from findOtherImages KDoc

* docs: fix incorrect @param tag in checkDuplicateImage KDoc

* docs: fix incorrect @param in onLongPress KDoc

* docs: fix invalid @param and @return tags on author property

* docs: fix incorrect @param in provideWikidataMediaInterface KDoc

* docs: fix incorrect @param name in getWikiText KDoc

* docs: escape wikilinks with [[ ]] to avoid KDoc resolution warnings

* docs: fix KDoc by adding missing param descriptions to startActivityWithFlags

* docs: fix KDoc by replacing @param with @property for contentUri

* docs: fix malformed KDoc in startYourself, remove invalid @param line

* docs: remove invalid @param tag in createDialogsAndHandleLocationPermissions

* docs: remove invalid @param tags to @property

* @docs: remove invalid @property tags

* docs: clean up KDoc by removing invalid @param and @return tags

* docs: fix incorrect @param name in removeBlocklisted KDoc

* docs: fix incorrect @param name in checkDuplicateImage KDoc

* docs: fix incorrect @param tag

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2025-05-29 20:25:00 +09:00
Rohit Verma
ed1485ca22
Migrated from Groovy to Kotlin DSL and upgrade AGP version (#6322)
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
* chore: migrate groovy build files to Kotlin DSL and upgrade AGP

chore: use version catalog to manage dependencies

chore: move plugins to version catalog

remove deprecated way of enabling build-config and redundant code

chore: setup triplet-play plugin configuration

refactor: move dependency block and tasks to the end of build file

refactor: move dependency block and mark redundant dependencies

* cleanup: remove redundant dependencies from build file

* cleanup: remove git utils file as the functions moved to build file

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2025-05-28 20:43:13 +09:00
Sonal Yadav
c49c85e68b
Fix : UninitializedPropertyAccessException (#6248)
* Fix crash when uploading a duplicate file

* Fix: app crash

* added Kdoc

* remove line b/w kdoc and function

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2025-05-28 17:53:28 +09:00
translatewiki.net
91ca2e6672
Localisation updates from https://translatewiki.net.
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
2025-05-26 14:01:50 +02:00
Sonal Yadav
8849f8984b
Fix: Use concise Wikidata feedback message while keeping full UI text (#6318)
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
2025-05-25 23:17:04 +10:00
translatewiki.net
bb21e4bdcd
Localisation updates from https://translatewiki.net.
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
2025-05-22 14:02:27 +02:00
Jason-Whitmore
eb617ae8ca
Fixes Issue 6308: Explore map shows image at my location rather than at shown location (#6315)
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
* ExploreMapFragment.java: add helper methods and fields to enable proper Explore map behavior

Before this commit, there was no way to tell if the user had arrived from the Nearby and
before the Nearby map center location had been searched for markers.

This commit adds a boolean flag to indicate this situation. Access, modification, and
initialization methods were added for this boolean value. Additionally, a helper
method to retrieve the Nearby map center LatLng was added as a convienience.

* ExploreMapPresenter.java: fix map update code to search for Nearby LatLng when appropriate

Before this commit, when the user selected "Show in explore" in Nearby when no pins were
on the map, Explore would only search for markers at the user's current GPS location,
rather than those at the Nearby map center.

After this commit, code was added to check if the user recently came from the Nearby map.
If so, the stored coordinates of the Nearby map is searched rather than the user's current
GPS coordinates. Additionally, the boolean that indicates that the user recently came
from the Nearby map is set to false. This ensures that the stored Nearby map center
coordinates are not used when the user taps the icon to focus the map on their
current location.

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2025-05-21 16:54:45 +09:00
translatewiki.net
b3c1474b31
Localisation updates from https://translatewiki.net.
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
2025-05-19 14:01:55 +02:00
Ritika Pahwa
21ffcb56fd Bump up version code to 1051
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
2025-05-18 12:44:37 +05:30
translatewiki.net
f977e16774
Localisation updates from https://translatewiki.net.
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
2025-05-12 14:01:59 +02:00
Jason-Whitmore
012020735f
Fixes Issue 6262: [Bug]: Error occurred while loading images (#6291)
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
* ImageInfo.kt: remove call to updateThumbURL()

Before this commit, the updateThumbURL() method would attempt to derive a larger resolution thumbnail URL
from the current thumbnail's width and height. This derived thumbnail URL would sometimes not match
an actual URL. Even creating this thumbnail URL would sometimes fail when doing string manipulation.
Both errors can cause issues, with the second one crashing the thread and preventing images from being
loaded.

This commit simply removes the call to updateThumbURL(). Images with their thumbnails now load correctly,
especially with panoramic images.

* MediaInterface.kt: fix MediaWiki API call to retrieve thumbnail URLs properly

Before this change, the API calls to MediaWiki would request thumbnails with atleast a specific width,
rather than height. These thumbnails would often be extremely low resolution on very wide images,
such as panoramas.

This change instead requests thumbnails with atleast a specific height. Panorama thumbnails are now
at a higher resolution than before. Additionally, the height parameter is now represented as an
integer which can be changed more easily.

* ViewPagerAdapter.java: create new constructor with behavior parameter

Before this commit, ViewPagerAdapter only had one constructor which used the default
behavior where more than the current fragment would be in the Resume state.

This commit adds a constructor where the behavior parameter can be specified.

* ExploreFragment.java: modify onCreateView to use new ViewPagerAdapter constructor

Before this change, onCreateView would use the old ViewPagerAdapter constructor, which
had a default behavior where fragments not currently visible would be in the Resume
state.

After this change, the new ViewPagerAdapter constructor is used with a non-default
behavior where only the visible fragment would be in the Resume state. This has
performance benefits for most users since the Fragments will only be active
when they want to view them.

* ExploreMapFragment.java: remove redundant method call

Before this commit, performMapReadyActions() was called in both onViewCreated and onResume.
In effect, the method is called twice before the user can even see the map, leading to
performance issues.

After this commit, the call in onViewCreated is removed. The method is now called only once
when the user switches to the ExploreMapFragment.

* ExploreMapFragment.java: remove method call that causes recursive loop

Before this commit, there was a call loop that included onScroll and animateTo
on the map. These two method calls caused performance issues in
ExploreMapFragment.

This commit breaks the call loop by removing moveCameraToPosition from getMapCenter.
Also, the removed code did not fit the purpose of getMapCenter.

* ExploreMapFragment.java: fix performMapReadyActions to center to user's last known location

Before this commit, fixing a previous bug caused performMapReadyActions to center the map
on a default location rather than the available last known location.

This commit adds code which will attempt to get the last known user location. If it cannot
be retrieved, the default location is used. The map now centers properly.

* MediaInterface.kt: adjust thumbnail height parameter

Before this commit, the thumbnail height parameter was too small, causing low resolution thumbnails to render.

This commit more than doubles the parameter, increasing the thumbnail resolution.

---------

Co-authored-by: Ritika Pahwa <83745993+RitikaPahwa4444@users.noreply.github.com>
Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2025-05-10 20:04:57 +09:00
Rohit Verma
3f2077a6db
tests: move to androidTest source-set (#6302)
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
2025-05-09 23:36:34 +09:00
translatewiki.net
f06ae4ebfe
Localisation updates from https://translatewiki.net.
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
2025-05-08 14:02:08 +02:00
translatewiki.net
865824a8e3
Localisation updates from https://translatewiki.net.
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
2025-05-05 14:01:48 +02:00
Ifeoluwa Andrew Omole
4d2170257a
Nearby List: Only show place cards with loaded names (#6301)
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
2025-05-04 15:38:18 +05:30
translatewiki.net
0024e72a2e
Localisation updates from https://translatewiki.net.
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
2025-05-01 14:01:50 +02:00
translatewiki.net
60aca9a5e3
Localisation updates from https://translatewiki.net.
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
2025-04-28 14:01:51 +02:00
translatewiki.net
d0f6c16878
Localisation updates from https://translatewiki.net.
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
2025-04-24 14:02:11 +02:00
Yusuke Matsubara
8fded5ef6e
Change back some variable names that were accidentally changed (#6297) 2025-04-24 20:16:03 +09:00
Yusuke Matsubara
329a68216e
Improve credit line in image list (#6295)
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
- When author is not uploader, show both.
- When failing to parse author from HTML, use structured data.
2025-04-23 23:23:09 +09:00
translatewiki.net
30762971db
Localisation updates from https://translatewiki.net.
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
2025-04-21 14:01:47 +02:00
Khushbu Khemchandani
7479d96675
Code Enhancement (Explore Map) (#6293)
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
* Exclude past locations (P585) from Nearby query

* "Send" button text should be white

* Custom picker: logic

* Revert back changes

* Enhancement :- Explore Map information

* Enhancement :- Explore Map information

* Style

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2025-04-21 14:49:15 +09:00
translatewiki.net
ed42d85f67
Localisation updates from https://translatewiki.net.
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
2025-04-17 14:01:53 +02:00
Sonal Yadav
78d29bcf20
FIX : Custom picker detect images that is already in commons (#6288)
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
* Fix: Exclude specific text from being posted in WikidataFeedback

* Add detection logic for images already on Commons in custom picker
2025-04-15 13:53:26 +10:00
Ritika Pahwa
1a13cb3383
Add v5.3.0 to CHANGELOG.md
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
2025-04-15 02:43:33 +05:30
Khushbu Khemchandani
9289dcc42c
UI enhancement Issue(#6285) (#6287)
* Exclude past locations (P585) from Nearby query

* "Send" button text should be white
2025-04-14 22:58:28 +09:00
translatewiki.net
efdc9c5548
Localisation updates from https://translatewiki.net.
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
2025-04-14 14:01:44 +02:00
Khushbu Khemchandani
69b3544107
Exclude past locations (P585) from Nearby query (#6284)
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
2025-04-14 10:54:29 +09:00
Ritika Pahwa
5b5aeead88
Bump up version code to 1050
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
2025-04-13 13:09:19 +05:30
Prinuel
4bacac1f8b
BookmarkLocationsFragment.kt:fix android studio warnings for this file, : Eliminated three unused imports, and changed calls to object: FilePicker.HandleActivityResult to use lamda (#6283)
Co-authored-by: bethel-m <bethelcletus87@gmail.com>
2025-04-13 14:39:14 +09:00
samimshoaib01
6aeb3c07cc
ui: make recenter FAB theme-aware using Material attributes (#6281)
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
2025-04-12 21:05:42 +05:30
Sonal Yadav
2c41176a6e
Mark for closed locations (P3999) in Nearby (#6273)
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
* Exclude closed locations (P3999) from Nearby query

* feat: Show  for P3999 items (official closure)

* revert changes

* Add P3999 (date of closure) support for non-existent places

* Typo fixing

* fix-typo

* .

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2025-04-11 18:00:47 +09:00
translatewiki.net
e3dd00bcfa
Localisation updates from https://translatewiki.net.
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
2025-04-10 14:01:48 +02:00
Jason-Whitmore
262efe4d8c
ExploreMapFragment.java: fix removeMarker() to remove the correct marker (#6279)
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
Before this change, the removeMarker() method would determine the correct overlay to remove
by comparing an overlay's Place coordinates with the target BaseMarker's Place coordinates.
If two different markers had the same Place coordinates, the incorrect marker would be removed,
leading to more than one green label appearing on the screen.

After this change, the removeMarker() method now compares an overlay's title with the BaseMarker's
Place name. removeMarker() will work properly as long as all BaseMarker's Place names are unique.
Also, null checks were added to removeMarker().
2025-04-10 14:19:31 +09:00
Dmitry Brant
2eed441462
Enable EmailAuth support. (#6277)
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
2025-04-08 20:59:28 +09:00
translatewiki.net
56fa8ceb5a
Localisation updates from https://translatewiki.net.
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
2025-04-07 14:01:50 +02:00
Rohit Verma
7bf9276d1a
fix: resolve IndexOutOfBounds error when removing images from top card (#6124)
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
replace deprecated onBackPressed with onBackPressedCallback

remove unit test for deprecated onBackPressed method

remove if-check before deleting picture to prevent hiding top thumbnail card

hide the thumbnail card on fragments other than MediaDetailFragment

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2025-04-05 22:47:27 +09:00
Prinuel
51da9e4dd6
FooterAdapter.kt: changed enum access of FooterItem, from FooterItem.values to FooterItem.entries, this is the more efficient way of accessing Enum values as introduced in kotlin 1.9 (#6271)
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
Co-authored-by: bethel-m <bethelcletus87@gmail.com>
2025-04-03 21:10:41 +09:00
translatewiki.net
731ff62faf
Localisation updates from https://translatewiki.net. 2025-04-03 14:01:45 +02:00
translatewiki.net
fdfd7781e9
Localisation updates from https://translatewiki.net.
Some checks failed
Android CI / Run tests and generate APK (push) Has been cancelled
2025-03-31 14:01:52 +02:00
Jason-Whitmore
6e090c8d7a
ExploreMapFragment.java: fix marker labels in Explore map fragment to display the username (#6260)
Before this change, the labels that would appear on the marker when tapped did not include
the author or username. Instead, it displayed "Unknown".

After this change, the labels now display the author name. If the author name is not
available, the username will be displayed. If both are unavailable, the default value
of "Unknown" will be displayed. To improve the readability of the text, any HTML text
is removed from the username/author.
2025-03-31 17:49:06 +09:00
Ritika Pahwa
44966645ca
Add v5.2.0 to CHANGELOG.md 2025-03-29 13:21:03 +05:30
translatewiki.net
669f3043ae
Localisation updates from https://translatewiki.net. 2025-03-27 13:01:54 +01:00
translatewiki.net
5a5e660a43
Localisation updates from https://translatewiki.net. 2025-03-24 13:01:43 +01:00
Ritika Pahwa
2e05a58e8b
Bump up version code to 1049 2025-03-22 14:07:51 +05:30
translatewiki.net
f1f4e8baff
Localisation updates from https://translatewiki.net. 2025-03-20 13:01:41 +01:00
Sonal Yadav
828f69fc46
Update Privacy Policy Link to GitHub.io (#6255)
* "moved privacy policy"

* Update PRIVACY_POLICY_URL to https://commons-app.github.io/privacy-policy as suggested

* Use BuildConfig.PRIVACY_POLICY_URL in launchPrivacyPolicy function
2025-03-20 07:21:33 +09:00
Ritika Pahwa
954a7aee91
Bump up version code and name (#6250)
* Bump up version code and name

* Remove unintended change
2025-03-18 23:28:19 +09:00
translatewiki.net
fa0bdf5747
Localisation updates from https://translatewiki.net. 2025-03-17 13:01:37 +01:00
Sonal Yadav
67ac92ff57
Fix NullPointerException in onBackPressed() (#6249) 2025-03-17 07:53:58 +09:00
Sonal Yadav
e1466c866b
Fix NullPointerException in UploadCategoriesFragment (#6246) 2025-03-14 16:16:14 +10:00
translatewiki.net
ba89894dc4
Localisation updates from https://translatewiki.net. 2025-03-13 13:01:53 +01:00
translatewiki.net
c46c1d2353
Localisation updates from https://translatewiki.net. 2025-03-10 13:01:11 +01:00
Saifuddin Adenwala
30322707fc
Migrated media/zoomControllers package to kotlin (#6204)
* Rename .java to .kt

* Migrated media/zoomControllers package to kotlin

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2025-03-08 22:47:23 +09:00
Parneet Singh
972bf785f1
Fix empty username (#6209)
* revert changes from #5860

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* read author prop instead

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* Use user prop if author is empty or null

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* fix test

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

---------

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>
2025-03-08 22:26:41 +09:00
Rohit Verma
32d485cc51
fix: null context issue by removing context getter override (#6218)
Refactored related code to resolve build issues
2025-03-08 22:02:28 +09:00
Parneet Singh
d11439f85d
add cancel button listener (#6216)
Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>
2025-03-07 09:52:52 +09:00
Rohit Verma
681881f4f6
fix: crash when opening leader board tab (#6220) 2025-03-07 08:21:31 +09:00
Nicolas Raoul
939d01b267 Bumped to 5.1.3 to reflect feature parity but the real 5.1.3 release was performed in the google_play_safe branch 2025-03-06 23:00:45 +09:00
translatewiki.net
d233de6103
Localisation updates from https://translatewiki.net. 2025-03-06 13:01:40 +01:00
Parneet Singh
139a296bd3
Comment in PR with generated builds. (#6226)
* revert prior attempt

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* trigger workflows_run event after uploading artifacts

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

---------

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>
2025-03-03 21:44:08 +09:00
translatewiki.net
6b56075df8
Localisation updates from https://translatewiki.net. 2025-03-03 13:01:29 +01:00
Rohit Verma
218476acbc
fix: rename isDarkTheme property to prevent recursion (#6231) 2025-03-02 06:19:43 +10:00
translatewiki.net
fa24b93830
Localisation updates from https://translatewiki.net. 2025-02-27 13:01:51 +01:00
Parneet Singh
b2f655522e
bump to action's v4 (#6225)
Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>
2025-02-27 10:11:32 +09:00
Nicolas Raoul
88eedc3506 Revert previous commit, wrong branch
This reverts commit aa84dedd64.
2025-02-26 18:03:24 +09:00
Nicolas Raoul
aa84dedd64 Removed Explore, Peer review, because we are at risk of being removed from Google Play tomorrow 2025-02-26 18:00:48 +09:00
Saifuddin Adenwala
1c7dce9e12
Migrated bookmarks locations to Kotlin and adapt room database (#6148)
* Rename .java to .kt

* Refactor: Migrate bookmark location logic to Kotlin

This commit migrates the bookmark location logic to Kotlin, enhancing code maintainability and readability.

-   Removes the `BookmarkLocationsContentProvider`, `BookmarkLocationsController`, and `BookmarkLocationsDao` Java classes.
-   Creates `BookmarkLocationsDao.kt` and `BookmarkLocationsContentProvider.kt` in Kotlin.
-   Migrates the logic from `BookmarkLocationsFragment.java` to `BookmarkLocationsFragment.kt`.
-   Updates test files to reflect these changes.
-   Addresses associated code review comments.

* Refactor: Migrate to Room Database for Bookmark Locations

This commit migrates the bookmark locations functionality from a custom content provider to Room database.

Key changes:

*   **Removal of `BookmarkLocationsContentProvider`:** This class, which previously handled data storage, has been removed.
*   **Introduction of `BookmarksLocations`:** This data class now represents a bookmarked location, serving as the Room entity.
*   **Creation of `BookmarkLocationsDao`:** This Room DAO handles database interactions for bookmark locations, including:
    *   Adding, deleting, and querying bookmarked locations.
    *   Checking if a location is already bookmarked.
    *   Updating the bookmark status of a location.
    *   Retrieving all bookmarked locations as `Place` objects.
*   **`BookmarkLocationsViewModel`:** Added to manage the data layer for bookmark locations
*   **`NearbyUtil`:** Created a Util class for Nearby to manage the bookmark locations.
*   **Updates in `PlaceAdapter` and `PlaceAdapterDelegate`:** These classes have been modified to work with the new Room-based data layer.
*   **Updates in `AppDatabase`:** The database now includes `BookmarksLocations` as an entity and exposes the `bookmarkLocationsDao`.
*   **Updates in `FragmentBuilderModule` and `CommonsApplicationModule`**: for DI
*   **Removal of `DBOpenHelper` upgrade for locations**: as it is no longer needed
* **Updates in `NearbyParentFragmentPresenter`**: refactored the logic to use the dao functions
* **Updates in `NearbyParentFragment`**: refactored the logic to use the util and dao functions
* **Update in `BookmarkLocationsController`**: removed as its no longer needed
* **Add `toPlace` and `toBookmarksLocations`**: extension functions to map between data class and entities
* **Update in `CommonsApplication`**: to remove old db table.

* Refactor: Improve bookmark location handling and update database version

This commit includes the following changes:

-   Updates the database version to 20.
-   Refactors bookmark location handling within `NearbyParentFragment` to improve logic and efficiency.
-   Introduces `getBookmarkLocationExists` in `NearbyUtil` to directly update bookmark icons in the bottom sheet adapter.
-   Removes unused provider.
-   Adjusts `BookmarkLocationsFragment`'s `PlaceAdapter` to use `lifecycleScope` for better coroutine management.
-   Refactors `updateBookmarkLocation` in `NearbyParentFragmentPresenter`.

* Toggle bookmark icon in BottomSheetAdapter instead of finding the location each time in the bookmark

* Update bookmark button image in `BottomSheetAdapter`
* Add new toggle function to `BottomSheetAdapter`
* Call the toggle function in `NearbyParentFragment`

* Refactor: Load bookmarked locations using Flow

*   `BookmarkLocationsController`: Changed to use `Flow` to load bookmarked locations.
*   `BookmarkLocationsDao`: Changed to return `Flow` of bookmark location instead of a list.
*   `BookmarkLocationsFragment`: Used `LifecycleScope` to collect data from flow and display it.
*   Removed unused `getAllBookmarkLocations()` from `BookmarkLocationsViewModel`.
*   Used `map` in `BookmarkLocationsDao` to convert from `BookmarksLocations` to `Place` list.

* Loading locations data in fragment

*   BookmarkLocationsController: Changed `loadFavoritesLocations` to be a suspend function.
*   BookmarkLocationsFragment: Updated the `initList` function to call the controller's suspend function.
*   BookmarkLocationsDao: Changed `getAllBookmarksLocations` and `getAllBookmarksLocationsPlace` to be suspend functions.

* Refactor BookmarkLocationControllerTest and related files to use coroutines

*  Migrated `bookmarkDao!!.getAllBookmarksLocations()` to `bookmarkDao!!.getAllBookmarksLocationsPlace()` and added `runBlocking`
*  Added `runBlocking` for `loadBookmarkedLocations()` and `testInitNonEmpty()`
*  Added `runBlocking` for `getAllBookmarksLocations()` and update `updateMapMarkers`

These changes improve the test structure by ensuring the database functions are executed within a coroutine context.

* Refactor BookmarkLocationsFragment and add tests for BookmarkLocationsDao

*   Moved `initList` to `BookmarkLocationsFragment` for better lifecycle handling.
*   Added test case for `BookmarkLocationsFragment`'s onResume.
*   Added test cases for `BookmarkLocationsDao` operations like adding, retrieving, finding, deleting and updating bookmarks.

* Refactor BookmarkLocationsFragment to load favorites only when view is not null

* BookmarkLocationsFragment: load favorites locations only when view is not null.
* BookmarkLocationsFragmentTest: added spy and verify to test onResume() call initList() method.

* Refactor database and add migration

*   `AppDatabase`: Updated to use room migration.
*   `CommonsApplicationModule`: added migration from version 19 to 20 to `appDatabase`

* Rename .java to .kt

* Refactor: Migrate bookmark location logic to Kotlin

This commit migrates the bookmark location logic to Kotlin, enhancing code maintainability and readability.

-   Removes the `BookmarkLocationsContentProvider`, `BookmarkLocationsController`, and `BookmarkLocationsDao` Java classes.
-   Creates `BookmarkLocationsDao.kt` and `BookmarkLocationsContentProvider.kt` in Kotlin.
-   Migrates the logic from `BookmarkLocationsFragment.java` to `BookmarkLocationsFragment.kt`.
-   Updates test files to reflect these changes.
-   Addresses associated code review comments.

* Refactor: Migrate to Room Database for Bookmark Locations

This commit migrates the bookmark locations functionality from a custom content provider to Room database.

Key changes:

*   **Removal of `BookmarkLocationsContentProvider`:** This class, which previously handled data storage, has been removed.
*   **Introduction of `BookmarksLocations`:** This data class now represents a bookmarked location, serving as the Room entity.
*   **Creation of `BookmarkLocationsDao`:** This Room DAO handles database interactions for bookmark locations, including:
    *   Adding, deleting, and querying bookmarked locations.
    *   Checking if a location is already bookmarked.
    *   Updating the bookmark status of a location.
    *   Retrieving all bookmarked locations as `Place` objects.
*   **`BookmarkLocationsViewModel`:** Added to manage the data layer for bookmark locations
*   **`NearbyUtil`:** Created a Util class for Nearby to manage the bookmark locations.
*   **Updates in `PlaceAdapter` and `PlaceAdapterDelegate`:** These classes have been modified to work with the new Room-based data layer.
*   **Updates in `AppDatabase`:** The database now includes `BookmarksLocations` as an entity and exposes the `bookmarkLocationsDao`.
*   **Updates in `FragmentBuilderModule` and `CommonsApplicationModule`**: for DI
*   **Removal of `DBOpenHelper` upgrade for locations**: as it is no longer needed
* **Updates in `NearbyParentFragmentPresenter`**: refactored the logic to use the dao functions
* **Updates in `NearbyParentFragment`**: refactored the logic to use the util and dao functions
* **Update in `BookmarkLocationsController`**: removed as its no longer needed
* **Add `toPlace` and `toBookmarksLocations`**: extension functions to map between data class and entities
* **Update in `CommonsApplication`**: to remove old db table.

* Refactor: Improve bookmark location handling and update database version

This commit includes the following changes:

-   Updates the database version to 20.
-   Refactors bookmark location handling within `NearbyParentFragment` to improve logic and efficiency.
-   Introduces `getBookmarkLocationExists` in `NearbyUtil` to directly update bookmark icons in the bottom sheet adapter.
-   Removes unused provider.
-   Adjusts `BookmarkLocationsFragment`'s `PlaceAdapter` to use `lifecycleScope` for better coroutine management.
-   Refactors `updateBookmarkLocation` in `NearbyParentFragmentPresenter`.

* Toggle bookmark icon in BottomSheetAdapter instead of finding the location each time in the bookmark

* Update bookmark button image in `BottomSheetAdapter`
* Add new toggle function to `BottomSheetAdapter`
* Call the toggle function in `NearbyParentFragment`

* Refactor: Load bookmarked locations using Flow

*   `BookmarkLocationsController`: Changed to use `Flow` to load bookmarked locations.
*   `BookmarkLocationsDao`: Changed to return `Flow` of bookmark location instead of a list.
*   `BookmarkLocationsFragment`: Used `LifecycleScope` to collect data from flow and display it.
*   Removed unused `getAllBookmarkLocations()` from `BookmarkLocationsViewModel`.
*   Used `map` in `BookmarkLocationsDao` to convert from `BookmarksLocations` to `Place` list.

* Loading locations data in fragment

*   BookmarkLocationsController: Changed `loadFavoritesLocations` to be a suspend function.
*   BookmarkLocationsFragment: Updated the `initList` function to call the controller's suspend function.
*   BookmarkLocationsDao: Changed `getAllBookmarksLocations` and `getAllBookmarksLocationsPlace` to be suspend functions.

* Refactor BookmarkLocationControllerTest and related files to use coroutines

*  Migrated `bookmarkDao!!.getAllBookmarksLocations()` to `bookmarkDao!!.getAllBookmarksLocationsPlace()` and added `runBlocking`
*  Added `runBlocking` for `loadBookmarkedLocations()` and `testInitNonEmpty()`
*  Added `runBlocking` for `getAllBookmarksLocations()` and update `updateMapMarkers`

These changes improve the test structure by ensuring the database functions are executed within a coroutine context.

* Refactor BookmarkLocationsFragment and add tests for BookmarkLocationsDao

*   Moved `initList` to `BookmarkLocationsFragment` for better lifecycle handling.
*   Added test case for `BookmarkLocationsFragment`'s onResume.
*   Added test cases for `BookmarkLocationsDao` operations like adding, retrieving, finding, deleting and updating bookmarks.

* Refactor BookmarkLocationsFragment to load favorites only when view is not null

* BookmarkLocationsFragment: load favorites locations only when view is not null.
* BookmarkLocationsFragmentTest: added spy and verify to test onResume() call initList() method.

* Refactor database and add migration

*   `AppDatabase`: Updated to use room migration.
*   `CommonsApplicationModule`: added migration from version 19 to 20 to `appDatabase`

* Resolve conflicts and attach the `commons.db` with `common_room.db` during the migration to persist the data

* Refactor database migration to handle null values and use direct insertion

*   Modify the database migration from version 19 to 20 to properly handle null values and use direct data insertion.
*   Create a new table `bookmarks_locations` with `NOT NULL` constraints.
*   Directly insert data into the new table from old database, safely handling `NULL` values by using `DEFAULT` empty string value.
*   Close old database cursor and the database connection.
* Drop the old `bookmarksLocations` table.

* Refactor database schema to delete `bookmarksLocations` table and migrate data

*   Delete `bookmarksLocations` table from the database during schema upgrade
*   Migrate data from `bookmarksLocations` to `bookmarks_locations` in new schema
*   Add `INSERT OR REPLACE` to prevent duplication during migration
*   Delete `CONTRIBUTIONS_TABLE` and `BOOKMARKS_LOCATIONS` in the application start-up to have a fresh DB for first start-up after update

* Update sqlite-based database version to 22.

* Update sqlite-based database version to 22.

* Refactor CommonsApplicationModule to utilize application context

* Initialize and use application context directly

* Utilize lateinit for application context

* Added line breaks for improved readability.

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2025-02-24 22:58:26 +09:00
translatewiki.net
1c4797d3aa
Localisation updates from https://translatewiki.net. 2025-02-24 13:01:49 +01:00
Sujal
b2927483fa
Update android-ci-comment.yml (#6200) 2025-02-24 16:19:35 +09:00
Sujal
fda87b7823
Fixed Build Status Badge (#6203)
* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md
2025-02-24 15:52:35 +09:00
Nicolas Raoul
71d3d12020
limit to the best issues for newcomers 2025-02-24 10:56:41 +09:00
Parneet Singh
50eb13a850
delete file (#6205)
Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>
2025-02-23 22:36:52 +09:00
Sujal
a8e38f4329
Updated Icon & Added Animation for Nearby (#6201)
* Applied better animation in nearby

* Refactor: Reduce Duration of Rotate Animation

Decreased the duration of the rotate animation from 1000ms to 500ms in `rotate.xml`, resulting in a faster rotation speed.
2025-02-23 22:36:07 +09:00
Sujal
d32ab15d42
Optimize Image Handling and Open Wikidata Media within app (#6187)
* implementing

* implementing

* implementing

* implementing

* implementing

* make new changes

* done

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2025-02-22 13:18:12 +09:00
Sujal
8dd1091608
Separate Workflow for Commenting on PR with APK Links (#6199)
* Update android.yml

* Create android-ci-comment.yml

* Update android.yml
2025-02-22 13:11:57 +09:00
translatewiki.net
44f69fcabd
Localisation updates from https://translatewiki.net. 2025-02-20 13:01:50 +01:00
Sujal
8d0da86569
Automatically Link APKs in PR Discussion After Building (#6195)
* Update android.yml

* Update android.yml

* Update android.yml

* Update android.yml

* Update android.yml
2025-02-19 21:43:58 +09:00
Jason-Whitmore
98b25acab9
Fixes Issue 5933: Nearby: Display of all nearby pins makes the app sluggish, leads to crashes (#6181)
* Place.java: change getWikiDataEntityID() method to increase speed

Before this commit, this method would perform the String replace method on the Wikidata link every time
getWikiDataEntityID() was called. Also, getWikiDataLink() was called. This caused poor performance
since both method calls are slow.

This commit changes the method to only run the slow methods if the entityID field is empty or
null. Once the field is populated, the method simply returns the field. This change allows
getWikiDataEntityID() to run much faster.

* NearbyParentFragmentPresenter.kt: change async and place update parameters

Before this commit, the parameters that configure the async and place update code contributed to
the slow loading of the red and green map markers.

This commit changes the parameters such that the red and green map markers load much faster.

These parameters may need further tuning. This commit's changes are simply an educated
guess at a good parameter set.

* NearbyParentFragment.kt: rewrite Java Drawable caching code to Kotlin

Before this commit, the code which cached Drawables was written in Java.

This commit rewrites that code into the new Kotlin file, which replaces the Java file.

* NearbyParentFragmentPresenter.kt: change loadPlacesDataAsync to retry HTTP requests after failure

Before this commit, when an HTTP request failed, the entire batch of Places would not get updated,
even if it was only one Place in the batch that caused the failure.

This commit changes the code such that upon HTTP request failure, new HTTP requests are sent with
one Place per request. This allows more Places to be fetched from the server.
2025-02-19 15:42:24 +09:00
Sujal
40241b4142
Fix #6188 (Nearby upload not being linked from Wikidata), though it introduces issue #6191
* Make neccesary changes

* Make neccesary changes

* spacing

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2025-02-17 23:02:45 +09:00
translatewiki.net
7a685b1241
Localisation updates from https://translatewiki.net. 2025-02-17 13:01:49 +01:00
translatewiki.net
34943542bf
Localisation updates from https://translatewiki.net. 2025-02-13 13:01:47 +01:00
Sujal
6345fef6bf
Migrated nearby parent fragment file to kotlin (#6177)
* Rename .java to .kt

* Migrated

* Migrated

* Migrated
2025-02-12 16:51:22 +09:00
translatewiki.net
a529ba8032
Localisation updates from https://translatewiki.net. 2025-02-10 13:01:39 +01:00
Sonal Yadav
e9e2697369
Fix: Fix crash when adding location after removing a picture (#6175)
* Fix: Resolved the Crash in UploadMediaDetailFragment

* Fix uninitialized basicKvStoreFactory in UploadMediaPresenter

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2025-02-07 23:10:18 +09:00
Sujal
12cadd0186
Migrated contributions folder Files from java to kotlin (#6176)
* Rename .java to .kt

* Migrated ContributionController

* Rename .java to .kt

* Migrated ContributionDao

* Rename .java to .kt

* Migrated ContributionsContract,ContributionFragment,ContributionListAdapter,ContributionsListContract from java to Kotlin

* Rename .java to .kt

* converted/Migrated

* converted/Migrated

* Rename .java to .kt

* Migrated ContributionController

* Rename .java to .kt

* Migrated ContributionDao

* Rename .java to .kt

* Migrated ContributionsContract,ContributionFragment,ContributionListAdapter,ContributionsListContract from java to Kotlin

* Rename .java to .kt

* Show placeholder and display depiction section when no depictions are available (#6163) (#6165)

* corrected

* corrected

* Update MediaDetailFragment.kt

Spelling correction

* Migrated AboutActivity from Java to Kotlin (#6158)

* Rename Constants to Follow Kotlin Naming Conventions

>This PR refactors constant names in the project to adhere to Kotlin's UPPERCASE_SNAKE_CASE naming convention, improving code readability and maintaining consistency across the codebase.

>Renamed the following constants in LoginActivity:
>saveProgressDialog → SAVE_PROGRESS_DIALOG
>saveErrorMessage → SAVE_ERROR_MESSAGE
>saveUsername → SAVE_USERNAME
>savePassword → SAVE_PASSWORD

>Updated all references to these constants throughout the project.

* Update Project_Default.xml

* Refactor variable names to adhere to naming conventions

Renamed variables to use camel case:
-UPLOAD_COUNT_THRESHOLD → uploadCountThreshold
-REVERT_PERCENTAGE_FOR_MESSAGE → revertPercentageForMessage
-REVERT_SHARED_PREFERENCE → revertSharedPreference
-UPLOAD_SHARED_PREFERENCE → uploadSharedPreference

Renamed variables with uppercase initials to lowercase for alignment with Kotlin conventions:
-Latitude → latitude
-Longitude → longitude
-Accuracy → accuracy

Refactored the following variable names:
-NUMBER_OF_QUESTIONS → numberOfQuestions
-MULTIPLIER_TO_GET_PERCENTAGE → multiplierToGetPercentage

* Refactor Dialog View Initialization with Null-Safe Calls

This PR refactors the dialog setup code in CustomSelectorActivity to improve safety and readability by replacing explicit casts with null-safe generic calls for findViewById.

>Replaced explicit casting (as Button and as TextView) with the generic findViewById<T>() method for improved type safety.
>Added null-safety (?.) to avoid potential crashes if a view is not found in the dialog layout.

why changed:-
>Prevents runtime crashes caused by NullPointerException when a view is missing in the layout.

* Refactor Unit Test: Replace Unsafe Casting with Type-Safe Mocking for findViewById

>PR refactors the unit test for NearbyParentFragment by replacing unsafe casting in the findViewById mocking statements with type-safe

>Ensured all findViewById mocks now use a consistent, type-safe format (findViewById<View>(...)) to reduce verbosity and potential casting errors.

>Verified the functionality of prepareViewsForSheetPosition remains unchanged, ensuring no regression in test behavior.

* Update NearbyParentFragmentUnitTest.kt

* Refactor: Rename Constants to Follow CamelCase Naming Convention

>Updated all constant variable names to follow the camelCase naming convention, removing underscores in the middle or end.

>Ensured variable names remain descriptive and align with code readability best practices.

* Replace private val with const val for URL constants in QuizController

* Renaming the constant to use UPPER_SNAKE_CASE

* Renaming the constant to use UPPER_SNAKE_CASE

* Update Done

* **Refactor: Convert `minimumThresholdForSwipe` to a compile-time constant**

* Convert AboutActivity from Java to Kotlin

This PR converts the AboutActivity class from Java to Kotlin

>Testing:
>Verified all functionalities of the AboutActivity, including toolbar setup, intent launches, and dialog interactions, to ensure behavior remains consistent post-conversion.
>Successfully ran unit tests for AboutActivity to confirm the correctness of methods and logic.

* Thank you for the suggestion! Since these methods all take a single View parameter, replacing them with method references is a great way to simplify the code and improve readability. I'll updated the code accordingly. Added a TODO in the code as a reminder to refactor this in the future.

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>

* Localisation updates from https://translatewiki.net.

* Feat: Make it smoother to switch between nearby and explore maps (#6164)

* Nearby: Add 'Show in Explore' 3-dots menu item

* MainActivity: Add methods to pass extras between Nearby and Explore

* MainActivity: Extend loadFragment() to support passing fragment arguments

* Nearby: Add ability to navigate to Explore fragment on 'Show in Explore' click

* Explore: Read fragment arguments for Nearby map data and update Explore map if present

* Explore: Add 'Show in Nearby' 3-dots menu item. Only visible when Map tab is selected

* Explore: On 'Show in Nearby' click, navigate to Nearby fragment, passing map data as fragment args

* Nearby: Read fragment arguments for Explore map data and update Nearby map if present

* MainActivity: Fix memory leaks when navigating between bottom nav destinations

* Explore: Fix crashes caused by unattached map fragment

* Refactor code to pass unit tests

* Explore: Format javadocs

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>

* Localisation updates from https://translatewiki.net.

* enhance spammy category filter (#6167)

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* Localisation updates from https://translatewiki.net.

* correction

* correction

* correction

* GitHub workflow to build betaDebug (#6174)

* [Bug fix] Check if duplicate exist using both original and modified file's checksum (#6169)

* check original file's SHA too along with modified one

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* fix tests

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

---------

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* Add multiline input for caption and description (#6173)

* allow multiple lines for description/caption

* make caption multiline too

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>

* correction

---------

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>
Co-authored-by: Akshay Komar <146421342+Akshaykomar890@users.noreply.github.com>
Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
Co-authored-by: translatewiki.net <l10n-bot@translatewiki.net>
Co-authored-by: Ifeoluwa Andrew Omole <iomole3@gmail.com>
Co-authored-by: Parneet Singh <111801812+parneet-guraya@users.noreply.github.com>
Co-authored-by: Matija Nalis <mnalis-git@voyager.hr>
2025-02-07 10:03:38 +09:00
Matija Nalis
1e77b1457a
Add multiline input for caption and description (#6173)
* allow multiple lines for description/caption

* make caption multiline too

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2025-02-05 15:56:57 +09:00
Parneet Singh
43dca1dd14
[Bug fix] Check if duplicate exist using both original and modified file's checksum (#6169)
* check original file's SHA too along with modified one

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* fix tests

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

---------

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>
2025-02-05 04:20:22 +10:00
Matija Nalis
30a7f702a1
GitHub workflow to build betaDebug (#6174) 2025-02-04 19:34:14 +09:00
translatewiki.net
0293b865b4
Localisation updates from https://translatewiki.net. 2025-02-03 13:01:34 +01:00
Parneet Singh
7566ddf529
enhance spammy category filter (#6167)
Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>
2025-02-01 09:13:17 +09:00
translatewiki.net
e653857437
Localisation updates from https://translatewiki.net. 2025-01-31 09:49:38 +01:00
Ifeoluwa Andrew Omole
7b291535e0
Feat: Make it smoother to switch between nearby and explore maps (#6164)
* Nearby: Add 'Show in Explore' 3-dots menu item

* MainActivity: Add methods to pass extras between Nearby and Explore

* MainActivity: Extend loadFragment() to support passing fragment arguments

* Nearby: Add ability to navigate to Explore fragment on 'Show in Explore' click

* Explore: Read fragment arguments for Nearby map data and update Explore map if present

* Explore: Add 'Show in Nearby' 3-dots menu item. Only visible when Map tab is selected

* Explore: On 'Show in Nearby' click, navigate to Nearby fragment, passing map data as fragment args

* Nearby: Read fragment arguments for Explore map data and update Nearby map if present

* MainActivity: Fix memory leaks when navigating between bottom nav destinations

* Explore: Fix crashes caused by unattached map fragment

* Refactor code to pass unit tests

* Explore: Format javadocs

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2025-01-30 21:58:00 +09:00
translatewiki.net
9dc9a3b8ab
Localisation updates from https://translatewiki.net. 2025-01-30 13:01:33 +01:00
Akshay Komar
5d4474ead6
Migrated AboutActivity from Java to Kotlin (#6158)
* Rename Constants to Follow Kotlin Naming Conventions

>This PR refactors constant names in the project to adhere to Kotlin's UPPERCASE_SNAKE_CASE naming convention, improving code readability and maintaining consistency across the codebase.

>Renamed the following constants in LoginActivity:
>saveProgressDialog → SAVE_PROGRESS_DIALOG
>saveErrorMessage → SAVE_ERROR_MESSAGE
>saveUsername → SAVE_USERNAME
>savePassword → SAVE_PASSWORD

>Updated all references to these constants throughout the project.

* Update Project_Default.xml

* Refactor variable names to adhere to naming conventions

Renamed variables to use camel case:
-UPLOAD_COUNT_THRESHOLD → uploadCountThreshold
-REVERT_PERCENTAGE_FOR_MESSAGE → revertPercentageForMessage
-REVERT_SHARED_PREFERENCE → revertSharedPreference
-UPLOAD_SHARED_PREFERENCE → uploadSharedPreference

Renamed variables with uppercase initials to lowercase for alignment with Kotlin conventions:
-Latitude → latitude
-Longitude → longitude
-Accuracy → accuracy

Refactored the following variable names:
-NUMBER_OF_QUESTIONS → numberOfQuestions
-MULTIPLIER_TO_GET_PERCENTAGE → multiplierToGetPercentage

* Refactor Dialog View Initialization with Null-Safe Calls

This PR refactors the dialog setup code in CustomSelectorActivity to improve safety and readability by replacing explicit casts with null-safe generic calls for findViewById.

>Replaced explicit casting (as Button and as TextView) with the generic findViewById<T>() method for improved type safety.
>Added null-safety (?.) to avoid potential crashes if a view is not found in the dialog layout.

why changed:-
>Prevents runtime crashes caused by NullPointerException when a view is missing in the layout.

* Refactor Unit Test: Replace Unsafe Casting with Type-Safe Mocking for findViewById

>PR refactors the unit test for NearbyParentFragment by replacing unsafe casting in the findViewById mocking statements with type-safe

>Ensured all findViewById mocks now use a consistent, type-safe format (findViewById<View>(...)) to reduce verbosity and potential casting errors.

>Verified the functionality of prepareViewsForSheetPosition remains unchanged, ensuring no regression in test behavior.

* Update NearbyParentFragmentUnitTest.kt

* Refactor: Rename Constants to Follow CamelCase Naming Convention

>Updated all constant variable names to follow the camelCase naming convention, removing underscores in the middle or end.

>Ensured variable names remain descriptive and align with code readability best practices.

* Replace private val with const val for URL constants in QuizController

* Renaming the constant to use UPPER_SNAKE_CASE

* Renaming the constant to use UPPER_SNAKE_CASE

* Update Done

* **Refactor: Convert `minimumThresholdForSwipe` to a compile-time constant**

* Convert AboutActivity from Java to Kotlin

This PR converts the AboutActivity class from Java to Kotlin

>Testing:
>Verified all functionalities of the AboutActivity, including toolbar setup, intent launches, and dialog interactions, to ensure behavior remains consistent post-conversion.
>Successfully ran unit tests for AboutActivity to confirm the correctness of methods and logic.

* Thank you for the suggestion! Since these methods all take a single View parameter, replacing them with method references is a great way to simplify the code and improve readability. I'll updated the code accordingly. Added a TODO in the code as a reminder to refactor this in the future.

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2025-01-30 16:41:27 +09:00
Sujal
36f844a709
Show placeholder and display depiction section when no depictions are available (#6163) (#6165)
* corrected

* corrected

* Update MediaDetailFragment.kt

Spelling correction
2025-01-29 22:50:29 +09:00
Tanmay Gupta
e01ecb20fa
maps intent: preserve zoom and show red pin (#6160)
Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2025-01-28 11:36:35 +09:00
yuvraj-coder1
41170d81d9
fix: logout user after account deletion by navigating to login screen (#6159) 2025-01-27 22:39:32 +10:00
translatewiki.net
7400872f87
Localisation updates from https://translatewiki.net. 2025-01-27 13:01:31 +01:00
Tanmay Gupta
aedcd7f9b9
Review: Load thumbnail instead of original image (#6153)
Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2025-01-24 16:02:33 +09:00
Tanmay Gupta
bb974f8935
Android CI: Update upload-artifact version (#6157) 2025-01-24 09:52:09 +09:00
translatewiki.net
77bad3380c
Localisation updates from https://translatewiki.net. 2025-01-23 13:01:40 +01:00
translatewiki.net
3570377678
Localisation updates from https://translatewiki.net. 2025-01-20 13:01:41 +01:00
Akshay Komar
d4ababc0a5
Refactor: Rename Constants to Follow CamelCase Naming Convention (#6126)
* Rename Constants to Follow Kotlin Naming Conventions

>This PR refactors constant names in the project to adhere to Kotlin's UPPERCASE_SNAKE_CASE naming convention, improving code readability and maintaining consistency across the codebase.

>Renamed the following constants in LoginActivity:
>saveProgressDialog → SAVE_PROGRESS_DIALOG
>saveErrorMessage → SAVE_ERROR_MESSAGE
>saveUsername → SAVE_USERNAME
>savePassword → SAVE_PASSWORD

>Updated all references to these constants throughout the project.

* Update Project_Default.xml

* Refactor variable names to adhere to naming conventions

Renamed variables to use camel case:
-UPLOAD_COUNT_THRESHOLD → uploadCountThreshold
-REVERT_PERCENTAGE_FOR_MESSAGE → revertPercentageForMessage
-REVERT_SHARED_PREFERENCE → revertSharedPreference
-UPLOAD_SHARED_PREFERENCE → uploadSharedPreference

Renamed variables with uppercase initials to lowercase for alignment with Kotlin conventions:
-Latitude → latitude
-Longitude → longitude
-Accuracy → accuracy

Refactored the following variable names:
-NUMBER_OF_QUESTIONS → numberOfQuestions
-MULTIPLIER_TO_GET_PERCENTAGE → multiplierToGetPercentage

* Refactor Dialog View Initialization with Null-Safe Calls

This PR refactors the dialog setup code in CustomSelectorActivity to improve safety and readability by replacing explicit casts with null-safe generic calls for findViewById.

>Replaced explicit casting (as Button and as TextView) with the generic findViewById<T>() method for improved type safety.
>Added null-safety (?.) to avoid potential crashes if a view is not found in the dialog layout.

why changed:-
>Prevents runtime crashes caused by NullPointerException when a view is missing in the layout.

* Refactor Unit Test: Replace Unsafe Casting with Type-Safe Mocking for findViewById

>PR refactors the unit test for NearbyParentFragment by replacing unsafe casting in the findViewById mocking statements with type-safe

>Ensured all findViewById mocks now use a consistent, type-safe format (findViewById<View>(...)) to reduce verbosity and potential casting errors.

>Verified the functionality of prepareViewsForSheetPosition remains unchanged, ensuring no regression in test behavior.

* Update NearbyParentFragmentUnitTest.kt

* Refactor: Rename Constants to Follow CamelCase Naming Convention

>Updated all constant variable names to follow the camelCase naming convention, removing underscores in the middle or end.

>Ensured variable names remain descriptive and align with code readability best practices.

* Replace private val with const val for URL constants in QuizController

* Renaming the constant to use UPPER_SNAKE_CASE

* Renaming the constant to use UPPER_SNAKE_CASE

* Update Done

* **Refactor: Convert `minimumThresholdForSwipe` to a compile-time constant**

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2025-01-18 23:06:52 +09:00
yuvraj-coder1
1c6ebafb29
feat: show the proper message in the custom picker when all images are either uploaded or marked. (#6095)
* feat: show the message "Congratulations, all pictures in this album have been either uploaded or marked as not for upload." in the custom picker when all images are either uploaded or marked.

* refactor: fix indentation in the code

* refactor: replace LiveData with StateFlow

* fix: fixed the bug that was causing one ImageFragment testcase to fail.

* fix: fixed the Congratulation message being shown for a brief moment while switching off the switch

* refactor: move hardcoded string to strings.xml.

* docs: add comment to clarify visibility logic in ImageFragment

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2025-01-18 23:04:28 +09:00
Sujal
23e1f01783
fix/handle-shared-files-and-error-handling (#6141) 2025-01-18 03:19:16 +10:00
Tanmay Gupta
ef032b0f93
Disable vanishing option when logged out (#6135) 2025-01-18 03:19:00 +10:00
Yusuke Matsubara
35a2fe87db
Use variant-dependent variable to make URLs for vanishing (#6117) 2025-01-17 17:33:50 +09:00
Rohit Verma
9f1fe8737f
Fix crash when opening in-app Camera for the very first time (#6139)
* fix: correctly handle permission callbacks on Main thread

The PermissionUtils was incorrectly executing permission callbacks on a background thread, leading to a Handler error. Now, it is using Main dispatcher.

* fix crash when opening camera while having partial storage access
2025-01-17 15:37:07 +09:00
translatewiki.net
2d6583fea6
Localisation updates from https://translatewiki.net. 2025-01-16 13:01:53 +01:00
Tanmay Gupta
1e64acdf1d
If depicted Wikidata item has no associated Commons category property, then suggest categories from its P18 (#6130)
* Fix NPE with UploadMediaDetails.captionText

* Store P18 instead of processed image url in DepictedItem

* Add routes for fetching category info from titles

* Consider depict's P18 when suggesting categories

* Add tests

* Corrected DepictedItem constructor arguments

* Add test for DepictedItem::primaryImage
2025-01-16 18:04:04 +09:00
Tanmay Gupta
1f33926ed5
Share login state with SingleWebViewActivity (#6136) 2025-01-16 17:49:09 +09:00
Ifeoluwa Andrew Omole
16ac08fe21
change status bar color to grey in dark mode (#6120)
Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2025-01-16 17:41:37 +09:00
Sujal
70291a0cb2
Migrated Java profile package to kotlin (#6119)
* Rename .java to .kt

* Migrate Profile Package to Kotlin #6118 #5928

* Migrate Profile Package to Kotlin #6118 #5928

* Migrate Profile Package to Kotlin #6118 #5928
2025-01-16 16:36:02 +09:00
Tanmay Gupta
62136b5b09
Fix NPE with UploadMediaDetails.captionText (#6128)
Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2025-01-15 22:52:35 +09:00
Kaartic Sivaraam
76078cf3b5 Merge remote-tracking branch 'origin/v5.1.0' 2025-01-15 12:52:36 +05:30
Kaartic Sivaraam
be0b1db193 Version v5.1.2 2025-01-15 12:50:53 +05:30
Kaartic Sivaraam
a796a8adcf category: fix category search in explore
This is a change that replicates the fix done via PR #6093.
This is based over v5.1.1 as the fix seems necessary for
the category search to work properly on the explore screen.
2025-01-15 12:32:50 +05:30
Kaartic Sivaraam
efc9ae8fb6 Merge remote-tracking branch 'origin/v5.1.0' 2025-01-15 11:53:59 +05:30
Jason-Whitmore
d4a9bacd91
Fixes Issue #5832: Navigation Banner Appears in Media Details Screen (#6121)
* MediaDetailFragment.kt: add helper method to retrieve ContributionFragment instance

Before this commit, there was no easy way to check for and retrieve the ContributionFragment instance that
was either the parent or grandparent (parent's parent) fragment. A complicated if check was required to
retrieve it.

After this commit, there is a simple helper method which will retrieve the ContributionFragment instance.
Existing code can now be replaced by calling this method.

* MediaDetailFragment.kt: replace code that is meant to hide nearby card

Before this commit, code would attempt to find and hide the nearby card that would appear
when the user was looking at media details. However, this code did not work.

After this commit, the old code has been replaced with code that correctly hides the
nearby card. Also, this new code uses a helper method call and is overall easier to read.

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2025-01-14 08:17:14 +09:00
Paul Hawke
0e735512bb
Convert upload to kotlin (part 3) (#6104)
* Convert UploadCategoriesFragment to kotlin

* Convert UploadBaseFragment to kotlin

* Convert UploadItem to kotlin

* Convert UploadModel to kotlin

* Convert UploadMediaDetailAdapter to kotlin

* Convert UploadActivity to kotlin

* Convert UploadMediaPresenter to kotlin

* Convert UploadMediaDetailFragment to kotlin

* Fix NPE that broke uploads
2025-01-13 23:04:09 +09:00
translatewiki.net
6d64357d45
Localisation updates from https://translatewiki.net. 2025-01-13 13:01:48 +01:00
Akshay Komar
78666ccbde
Refactor Dialog View Initialization with Null-Safe Calls (#6114)
* Rename Constants to Follow Kotlin Naming Conventions

>This PR refactors constant names in the project to adhere to Kotlin's UPPERCASE_SNAKE_CASE naming convention, improving code readability and maintaining consistency across the codebase.

>Renamed the following constants in LoginActivity:
>saveProgressDialog → SAVE_PROGRESS_DIALOG
>saveErrorMessage → SAVE_ERROR_MESSAGE
>saveUsername → SAVE_USERNAME
>savePassword → SAVE_PASSWORD

>Updated all references to these constants throughout the project.

* Update Project_Default.xml

* Refactor variable names to adhere to naming conventions

Renamed variables to use camel case:
-UPLOAD_COUNT_THRESHOLD → uploadCountThreshold
-REVERT_PERCENTAGE_FOR_MESSAGE → revertPercentageForMessage
-REVERT_SHARED_PREFERENCE → revertSharedPreference
-UPLOAD_SHARED_PREFERENCE → uploadSharedPreference

Renamed variables with uppercase initials to lowercase for alignment with Kotlin conventions:
-Latitude → latitude
-Longitude → longitude
-Accuracy → accuracy

Refactored the following variable names:
-NUMBER_OF_QUESTIONS → numberOfQuestions
-MULTIPLIER_TO_GET_PERCENTAGE → multiplierToGetPercentage

* Refactor Dialog View Initialization with Null-Safe Calls

This PR refactors the dialog setup code in CustomSelectorActivity to improve safety and readability by replacing explicit casts with null-safe generic calls for findViewById.

>Replaced explicit casting (as Button and as TextView) with the generic findViewById<T>() method for improved type safety.
>Added null-safety (?.) to avoid potential crashes if a view is not found in the dialog layout.

why changed:-
>Prevents runtime crashes caused by NullPointerException when a view is missing in the layout.
2025-01-11 08:46:19 +09:00
Neel Doshi
39b513da12
feat : Account Vanishing (#6098)
* feat : Account Vanishing

* Added Comment for SingleWebViewActivity

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2025-01-09 23:27:08 +09:00
translatewiki.net
18f599b554
Localisation updates from https://translatewiki.net. 2025-01-09 13:01:40 +01:00
Akshay Komar
3e7565c7e3
Refactor variable names to adhere to naming conventions (#6111)
* Rename Constants to Follow Kotlin Naming Conventions

>This PR refactors constant names in the project to adhere to Kotlin's UPPERCASE_SNAKE_CASE naming convention, improving code readability and maintaining consistency across the codebase.

>Renamed the following constants in LoginActivity:
>saveProgressDialog → SAVE_PROGRESS_DIALOG
>saveErrorMessage → SAVE_ERROR_MESSAGE
>saveUsername → SAVE_USERNAME
>savePassword → SAVE_PASSWORD

>Updated all references to these constants throughout the project.

* Update Project_Default.xml

* Refactor variable names to adhere to naming conventions

Renamed variables to use camel case:
-UPLOAD_COUNT_THRESHOLD → uploadCountThreshold
-REVERT_PERCENTAGE_FOR_MESSAGE → revertPercentageForMessage
-REVERT_SHARED_PREFERENCE → revertSharedPreference
-UPLOAD_SHARED_PREFERENCE → uploadSharedPreference

Renamed variables with uppercase initials to lowercase for alignment with Kotlin conventions:
-Latitude → latitude
-Longitude → longitude
-Accuracy → accuracy

Refactored the following variable names:
-NUMBER_OF_QUESTIONS → numberOfQuestions
-MULTIPLIER_TO_GET_PERCENTAGE → multiplierToGetPercentage
2025-01-09 17:22:42 +09:00
Saifuddin Adenwala
87a453cb72
Migrated concurrency module from Java to Kotiln (#6110)
* Rename .java to .kt

* Migrated concurrency module to Kotlin
2025-01-08 15:42:53 +09:00
Nachiket Jadhav
fdbe504ca9
fix: logo getting cropped in landscape mode of login page (#6106)
* Fixed logo getting cropped in landscape mode of login page

* Indentation added at line 12

* New Dimension created in dimens.xml which is used in land\activity_login.xml
2025-01-08 05:33:41 +10:00
yuvraj-coder1
b2159ed87f
feat: Long-pressing a row in "Uploads" copies the caption to clipboard, and displays a snack saying "Caption copied to clipboard" (#6105)
* feat: Long-pressing a row in "Uploads" copies the caption to clipboard, and displays a snack saying "Caption copied to clipboard

* refactor: using string resources for the text instead of hard coded values
2025-01-07 07:12:24 +10:00
Akshay Komar
ecb19d6984
Rename Constants to Follow Kotlin Naming Conventions (#6107)
* Rename Constants to Follow Kotlin Naming Conventions

>This PR refactors constant names in the project to adhere to Kotlin's UPPERCASE_SNAKE_CASE naming convention, improving code readability and maintaining consistency across the codebase.

>Renamed the following constants in LoginActivity:
>saveProgressDialog → SAVE_PROGRESS_DIALOG
>saveErrorMessage → SAVE_ERROR_MESSAGE
>saveUsername → SAVE_USERNAME
>savePassword → SAVE_PASSWORD

>Updated all references to these constants throughout the project.

* Update Project_Default.xml
2025-01-07 03:00:02 +10:00
translatewiki.net
940c0740b0
Localisation updates from https://translatewiki.net. 2025-01-06 13:01:38 +01:00
Neel Doshi
cebe1c2a1f
chore : lint fix (#6099)
- Removed unused constants which were there in `Pref`
- Removed unused parameter requireActivity from `Setting Fragment`

Revert "chore : lint fix"

This reverts commit 599203343f4c3c050b45e4ccf53fd23097818cc4.
2025-01-06 02:48:05 +10:00
Paul Hawke
1d8d1d6b03
Remove the extra byte buffer copying while creating file chunks (#6091)
Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2025-01-05 20:44:30 +09:00
Akshay Komar
25e467b3a5
Refactor Nearby Location Tests to Improve Assertion Logic (#6103)
-Refactored testSearchCloseToCurrentLocationWhenFar: Simplified assertion by using assertFalse(!isClose) for better readability and logical clarity.

-Added testSearchCloseToCurrentLocationWhenClose: Created a new test case to validate behavior when the search is close to the current location, ensuring assertTrue(isClose) for correctness.

-Improved Coverage: These changes enhance the test coverage and reliability of the searchCloseToCurrentLocation function.
2025-01-05 17:21:17 +09:00
Parneet Singh
038ae9acd4
fix null pointer exception (#6093)
Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>
2025-01-03 22:32:06 +10:00
translatewiki.net
ea20a64b34
Localisation updates from https://translatewiki.net. 2025-01-02 13:01:43 +01:00
Tanmay Gupta
411184fde8
Nearby: Fixed recenter and legend FABs inconsistencies (#6092)
Using app:useCompatPadding=true on both the FABs caused padding issues, replaced that with android:layout_margin. As the minimumSdk version is >=lollipop, I believe useCompatPadding is not required.
2025-01-02 15:09:18 +09:00
translatewiki.net
bf89f11606
Localisation updates from https://translatewiki.net. 2024-12-30 13:01:44 +01:00
Kaartic Sivaraam
391408ed17 CHANGELOG: fix some typos 2024-12-29 22:43:21 +05:30
Kaartic Sivaraam
9bbb1a98db Version 5.1.1 2024-12-29 22:36:39 +05:30
Sujal
faa58a19de
Fixed bug #5876 (#6086) 2024-12-29 16:09:03 +09:00
Tanmay Gupta
d2751595cb
Nearby: Markers now show place via pin base (#6090) 2024-12-29 15:46:29 +09:00
Gautham Mohanraj
46cefa4899
Fix/feedback to map navigation (#6087)
* feat:Added try catch block for the Feedback

* fix:Added finish after the feedback submitted so that we navigate back to the map
2024-12-29 11:03:47 +09:00
Tanmay Gupta
5bc58284aa
Nearby: Avoid reloading entire map upon cache clear (#6089) 2024-12-29 10:44:47 +09:00
Parneet Singh
a6444968fa
add cancel button (#6078)
Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>
2024-12-29 08:20:32 +09:00
Parneet Singh
dec56a3342
rm file (#6079)
Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>
2024-12-29 07:53:04 +09:00
Parneet Singh
91e41c4c18 Always show upload icon (#6022)
* fix issue5847 as owner required: make the icon always visible and just adjust the count accordingly,comment the setVisibility method and make pending_upload_icon always visible

* set pending_upload_icon android visible level: visible and tool visible level: gone

* fix issue 5847, The upload icon is now set to always be visible, while the original code has been commented out and retained with a note for potential re-use in the future.

* refactor

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

---------

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>
Co-authored-by: bxy379987 <bxy379987@gmail.com>
Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2024-12-28 23:55:27 +05:30
Neel Doshi
b714b45bfd Migrated exifInterface to androidx exif.interface (#6013) 2024-12-28 23:43:26 +05:30
Parneet Singh
22238f55cd
fix parsing (#6075)
Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>
2024-12-27 01:27:03 +10:00
Sonal Yadav
4244373a5d
Refactor focus change listener to improve readability and avoid crashes (#6080) 2024-12-27 01:26:08 +10:00
Tanmay Gupta
75ca96a526
Nearby: show cached pins even when internet is unavailable (fixes #6051) (#6081)
* Place: Made location @Embedded

* Nearby: Move handling map scroll to presenter

* PlacesRepository: Add methods for fetching places in geo bounds

* Nearby: add getScreenTopRight/BottomLeft and refactor old code

* PlacesRepository: Add methods for fetching places in map bounds

* Nearby: Complete offline pins implementation

* Nearby offline pins: Add snackbar message

* Nearby offline pins: Add javadoc

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2024-12-26 23:55:21 +09:00
translatewiki.net
0d71da106f
Localisation updates from https://translatewiki.net. 2024-12-26 13:01:25 +01:00
Parneet Singh
86cdf96f3d
make prop nullable (#6073)
Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>
2024-12-25 00:55:12 +09:00
Sonal Yadav
e7864ac1dd
Refactor usage of Html.fromHtml to handle deprecation (#6074) 2024-12-25 00:44:25 +09:00
Paul Hawke
a9058d129e
Convert upload to kotlin (part 2) (#6069)
* Convert UploadCategoriesFragment to kotlin

* Convert DepictsFragment to kotlin

* Convert MediaLicensePresenter to kotlin

* Convert MediaLicenseFragment to kotlin

* Converted SimilarImageDialogFragment to kotlin

* Convert ThumbnailsAdapter to kotlin

* Convert UploadPresenter to kotlin

* Convert UploadBaseFragment to kotlin

* Convert UploadMediaDetailInputFilter to kotlin

* Convert UploadItem to kotlin

* Convert UploadController to kotlin

* Fix nullability of the UploadItem
2024-12-24 16:11:46 +09:00
Tanmay Gupta
369e79be5e
Nearby: No longer keeps loading until timeout when map is zoomed out (#6070)
* Nearby: Search for actual map center

* Add query syntax and methods

* Nearby: Added binary search for loading pins

* Add NearbyQueryParams and refactor

* Add unit tests and complete implementation

* Nearby: Increase max radius from 100km to 300km

* Nearby: Centermost pins now appear on top

* getNearbyItemCount: Added javadoc

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2024-12-24 15:21:45 +09:00
translatewiki.net
c963cd9ea4
Localisation updates from https://translatewiki.net. 2024-12-23 13:02:14 +01:00
Sonal Yadav
7479767266
Fix: Allow back button functionality to dismiss language selection dialog (#6067)
Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2024-12-23 20:38:45 +09:00
Sonal Yadav
b55c61ddb8
Fixed the migrations warning under Kotlin header #13282 (#6060)
* Fixed  Migration warnings under Kotlin header

* suppresses Lint

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2024-12-22 23:14:20 +09:00
Neel Doshi
f1e8e48769
Removed deneme8 Timber tag (#6065) 2024-12-22 22:05:25 +09:00
Neel Doshi
d0bde4a3fe
Replaces Log to Timber (#6062) 2024-12-22 14:44:57 +09:00
Tanmay Gupta
6a32454347
Nearby: Fix map moving by itself (#6061)
fixes #6046

the OnFocusChangeListener for nearby place list items sometimes gets invoked when new items aer set, even when the list is hidden, if an item had previously been clicked in it. This in turn causes the onItemClick to be called. This commit adds a check to make sure the list is not hidden when onItemClick is invoked this way.
2024-12-22 08:39:30 +09:00
Tanmay Gupta
4dd16054ca
Nearby: Fix disappearing of pins loaded from cache (#6052) (#6057)
* Nearby Fix disappearing of pins loaded from cache

Fixes #6052

* Remove outdated tests

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2024-12-21 20:10:33 +09:00
Mohit Kambli
4b152fc15f
Fix #6054: Rename SAVEPROGRESSDAILOG to SAVEPROGRESSDIALOG (#6058) 2024-12-21 19:55:51 +09:00
Tanmay Gupta
c891c2b0df
Nearby: Fix race condition and lag when loading pin details, faster overlay management (#6047)
* temporary fixes part one

* temporary fixes part two

* temporary fixes part three

* temporary fixes part four

* temporary fixes part five

* reformatting

* remove code no longer in use

* Migrate NearbyParentFragmentPresenter to Kotlin

* Partially replace temporary experimental fixes

* Replace temporary experimental fixes part 2

* Replace temporary experimental fixes part 3

* Replace temporary fixes completely

* Fix caching and loading places in Nearby list

* Add place bookmarking logic, Remove all old code

* Nearby Presenter: Close channel properly

* Nearby pins now load starting from the center

Fixes #6049

* Add comments and javadoc for Nearby Presenter

* Fix warnings, Fix formatting, Add javadoc

* Pass unit tests
2024-12-20 22:30:53 +10:00
Parneet Singh
70b4f78a5d
fix multi-line paste (#6050)
Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>
2024-12-20 22:29:08 +10:00
Parneet Singh
4c9637c821
Add pull down to refresh in Contributions screen (#6041)
* pull down to refresh

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* add kdoc

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* only enabled for self user

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* fix test

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

---------

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>
2024-12-20 10:06:07 +09:00
translatewiki.net
a4b74794cb
Localisation updates from https://translatewiki.net. 2024-12-19 13:01:57 +01:00
Parneet Singh
5500b03976
Feature: Bookmark Categories (#6035)
* database setup and insert/delete working

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* add new categories screen in bookmarks page

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* add theme

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* set tab layout scrollable

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* cleanup

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* fix tests

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* bump database version

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* add docs

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

---------

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>
2024-12-17 21:49:32 +09:00
translatewiki.net
0153cbe0ed
Localisation updates from https://translatewiki.net. 2024-12-16 13:01:57 +01:00
Tanmay Gupta
e8970ab7f2
FilePicker: Fix error when multiple images are returned from Documents based picker (#6033)
* FilePicker: Correctly handle Documents result

* Empty commit to re-trigger CI checks
2024-12-15 21:21:30 +09:00
Tanmay Gupta
a933b92efa
Fix caption lost on accepting 'Is this a pic of' (#6030)
Fixes issue 5842 by correcting the implementation of onUserConfirmedUploadIsOfPlace in UploadMediaDetailsContract's UserActionListener

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2024-12-14 19:12:44 +09:00
Haoyu Weng
f2d1f7dbbb
[Enhancement] Fix #5042 Add copy link button (#5080)
* Add a "copy link" button next to the share button. Add relative English and Simplified Chinese text in strings.xml.

* Change the icon from copy to link

* Fixed typo

* Fixed case

---------

Co-authored-by: Justweng <justweng19@gmail.com>
Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2024-12-14 18:45:44 +09:00
Tanmay Gupta
235e8cdba2
Fix edit button shown when image has no location (#6029)
Changed the commons.filepicker.UploadableFile's hasLocation method to use the existing ImageCoordinates class for detecting the presence of geolocation in an image's exif
2024-12-14 17:31:25 +09:00
Parneet Singh
c1acdbe31a
remove method (#6028)
Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>
2024-12-14 13:15:52 +09:00
Paul Hawke
2c8c441f25
Convert upload to kotlin (part 1) (#6024)
* Convert upload dagger module to kotlin

* Code cleanup and convert the upload contract to kotlin

* Code cleanup and convert CategoriesContract to kotlin

* Code cleanup and convert MediaLicenseContract to kotlin

* Code cleanup and convert UploadMediaDetailsContract to kotlin

* Code cleanup, fixed nullability and converted DepictsContract to kotlin

* Removed unused class

* Convert FileMetadataUtils to kotlin

* Convert EXIFReader to kotlin

* Convert FileUtils to kotlin

* Convert FileUtilsWrapper to kotlin

* Convert ImageProcessingService to kotlin

* Convert PageContentsCreator to kotlin

* Convert PendingUploadsPresenter and contract to Kotlin with some code-cleanup

* Convert ReadFBMD to kotlin

* Convert SimilarImageInterface to kotlin

* Removed unused classes

* Fix merge/rebase issue

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2024-12-13 23:15:13 +09:00
Tanmay Gupta
cb007608d9
Issue #5996 - Fix Android Studio warnings in LocationPickerActivity.kt (#6026)
* Replace deprecated zoomLevel with zoomLevelDouble

* Replace deprecated Html.fromHtml call

* LocationPickerActivity: Replace deprecated methods with androidx

* LocationPickerActivity: Reformatted overlong lines

* Renamed package LocationPicker to locationpicker

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2024-12-13 22:49:40 +09:00
Tanmay Gupta
8a55b5e613
FolderDeletionHelper: Fix unintentional deletion (#6027)
* fix issue6020: prevent unintentional deletion of subfolders and non-images by custom selector

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2024-12-13 22:41:47 +09:00
Gautham Mohanraj
b2810bcef1
Resolve crash when submitting feedback without internet access (#6018)
* fix: resolve crash when submitting feedback without internet access

* feat:Added check for wheather internet connection was available for a network

* feat:Added SnackBar for Retry and User Info

* feat:Made the feedback dialog not leave screen in case of error

* feat:Removed the network checking from the function

* feat:Added try catch block for the Feedback

* feat:Removed Unnecessary imports

* feat:Used Snackbar and timber instead of log and Toast

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2024-12-13 22:30:30 +09:00
translatewiki.net
f51b607312
Localisation updates from https://translatewiki.net. 2024-12-12 13:02:18 +01:00
Parneet Singh
3bfa3612c6
Always show upload icon (#6022)
* fix issue5847 as owner required: make the icon always visible and just adjust the count accordingly,comment the setVisibility method and make pending_upload_icon always visible

* set pending_upload_icon android visible level: visible and tool visible level: gone

* fix issue 5847, The upload icon is now set to always be visible, while the original code has been commented out and retained with a note for potential re-use in the future.

* refactor

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

---------

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>
Co-authored-by: bxy379987 <bxy379987@gmail.com>
Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2024-12-11 23:29:50 +09:00
Parneet Singh
9a876fa5e2
make dialog modal (#6015)
Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>
2024-12-11 15:08:39 +09:00
Saifuddin Adenwala
c175a4ee03
Migrated category module from Java to Kotlin (#6016)
* Rename .java to .kt

* Rebased category PR

* Resolved conflicts

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2024-12-11 11:17:05 +09:00
Saifuddin Adenwala
3030a6fca7
Migrated helper modules to kotlin (#6007)
* Rename .java to .kt

* Migrated delete and description module to kotlin (WIP)

* Fix: Unit tests

* Fix: Unit tests

* Rename .java to .kt

* Migrated data, db, and converter module to kotlin

* Fix: Unit tests

* Fix: Unit tests

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2024-12-11 07:43:36 +09:00
Paul Hawke
73311970c5
Convert wikidata/mwapi to kotlin (part 4) (#6010)
* Convert ImageDetails to kotlin

* Convert MwException/MwServiceError to kotlin

* Convert ListUserResponse to kotlin

* Convert MwPostResponse to kotlin

* Convert MwQueryResponse to kotlin

* Convert MwQueryResult to kotlin

* Convert MwQueryPage to kotlin

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2024-12-10 12:18:42 +09:00
Neel Doshi
56ada36b83
Migrated exifInterface to androidx exif.interface (#6013) 2024-12-10 03:54:11 +10:00
Parneet Singh
85d9aef2f3
Feature: Show where file is being used on Commons & Other wikis (#6006)
* add url to build config

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* add network call functions

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* return response asynchronously

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* inject page size in the request

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* rename from Commons..Response.kt to ..Response.kt

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* convert to .kt

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* ui setup working

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* fix merge conflict

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* cleanup

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* fix CI

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* use suspend function for network calls

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* doc

* doc

* doc

* doc

* doc

---------

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>
Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2024-12-09 23:07:44 +09:00
translatewiki.net
04a07ed655
Localisation updates from https://translatewiki.net. 2024-12-09 13:02:13 +01:00
Parneet Singh
cc74707894
delete empty files (#6009)
Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>
2024-12-08 14:51:37 +09:00
Neel Doshi
64fd10d00e
Migrate Feedback module from Java to Kt (#5985)
* Rename `.java` to `.kt`

* Migrated FeedbackContentCreator to kotlin

* Rename FeedbackDialog from `java` to `kt`

* Migrated Feedback Dialog from `java` to `kt`

* Renamed OnFeedbackSubmitCallback to kotlij

* Migrated OnFeedbackSubmitCallback to kotlin

* Fixed: TestCase Failure

* Fixed Test : Changed Private Modifier to Public

* Suppressed deprecated and added TODO for lint

* Linked Deprecation with Github Issue

* Rename Feedback from java to kt

* Migrated Feedback Data Class to Kotlin

* Modified the data class to var for mutuability
2024-12-07 16:19:00 +09:00
Paul Hawke
015c5d5c63
Convert wikidata/mwapi to kotlin (part 3) (#6004)
* Convert Edit to kotlin along with deleting unused class

* Converted ExtMetadata to kotlin

* Convert ImageInfo to kotlin

* Removed unused class

* Convert Notification to kotlin

* Convert PageProperties to kotlin

* Convert PageTitle to kotlin

* Convert Namespace to kotlin
2024-12-07 12:20:06 +09:00
Neel Doshi
64354fb9e4
chore : Lint fix (#5995)
* Refactor : Fixed lint issues on EditActivity

- Using 'Log' instead of 'Timber'
- Line is longer than allowed by code style (> 100 columns)
- Use of getter method instead of property access syntax
- Should be replaced with Kotlin function
- Cascade 'if' should be replaced with 'when'

* Suppress Deprecation

* Linked Deprecated with Github Issue

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2024-12-07 02:24:22 +10:00
Neel Doshi
a8387f01c9
Bug Fixs & Enhancement of Achievement Screen (#5666)
* Rename AchievementFragment from `.java` to `.kt`

* Migrated AchievementFragment to kotlin

* Revamped Achievement Screen

* fixed AchievementFragment Unit Test

* fixed Level on MoreBottomSheetFragment

* Implemented Badge and Minor Code Refactor

* Fixed the badge issue & made the badge clickable

* Removed Redundant XML Code & Converted badges to green color and added values inside it

* Fixed : showSnackBarWithRetry Test

* Fixed : Theme issues on Light Mode

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2024-12-06 23:15:47 +09:00
Paul Hawke
ae52267a27
Convert wikidata/mwapi to kotlin (part 2) (#5999)
* Convert DepictSearchResponse to kotlin

* Convert Entities to kotlin

* Convert WikiSite to kotlin

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2024-12-06 17:50:29 +09:00
Saifuddin Adenwala
f8d519e8eb
Migrated filepicker from Java to Kotlin (#5997)
* Rename .java to .kt

* Migrated filepicker module from Java to Kotlin

* Rename .java to .kt

* Migrated filepicker module from Java to Kotlin

* fix: test cases
2024-12-06 17:31:40 +09:00
Paul Hawke
3777f18bf9
Convert mwapi/wikidata to kotlin (part 1) (#5991)
* Convert OkHttpJsonApiClient and CategoryApi to kotlin

* Convert GsonUtil to kotlin

* Convert WikidataConstants to kotlin

* Convert WikidataEditListener to kotlin

* Convert WikidataEditService to kotlin

* work in progress

* Convert RequiredFieldsCheckOnReadTypeAdapterFactory to kotlin

* Converted type adapters

* Convert WikiSiteTypeAdapter to kotlin

* Fixed nullability
2024-12-05 23:13:38 +09:00
translatewiki.net
9dd504e560
Localisation updates from https://translatewiki.net. 2024-12-05 13:01:47 +01:00
Paul Hawke
33548fa57d
Convert profile package to kotlin (#5979)
* Convert ViewModelFactory to kotlin

* Convert UpdateAvatarResponse and related test to Kotlin

* Convert LeaderboardResponse and related test to kotlin

* Convert LeaderboardListAdapter to kotlin

* Convert UserDetailAdapter to kotlin

* Convert LeaderboardListViewModel to kotlin

* Convert DataSourceClass to kotlin

* Convert the LeaderboardFragment to kotlin

* Converted AchievementsFragment to kotlin

* Revert "Converted AchievementsFragment to kotlin"

This reverts commit 4fcbb81e5d.

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2024-12-03 15:47:25 +09:00
Saifuddin Adenwala
8265cc6306
Migrate location and language module from Java to Kotlin (#5988)
* Rename .java to .kt

* Migrated location and language module from Java to Kotlin

* Changed lastLocation visibility
2024-12-03 15:27:11 +09:00
Saifuddin Adenwala
771f370f9a
Migration of locationpicker module from Java to Kotlin (#5981)
* Rename .java to .kt

* Migrated location picker module from Java to Kotlin
2024-12-02 16:54:26 +09:00
Neel Doshi
fb1ef3212d
Migrated Bookmark from Java to Kotlin (#5960)
* Rename Bookmark Pages from `.java` to `.kt`

* Migrated Bookmark Pages to kotlin
2024-11-30 10:51:53 +09:00
Paul Hawke
1e5521b434
Convert dependency inject ("di") package to kotlin (#5976)
* Convert a batch of easier modules

* Convert the NetworkingModule to kotlin

* Converted the ApplicationlessInjection to kotlin

* Convert CommonsDaggerAppCompatActivity to kotlin

* Convert CommonsDaggerContentProvider to kotlin

* Convert CommonsDaggerIntentService to kotlin

* Convert CommonsDaggerService to kotlin

* Convert CommonsDaggerSupportFragment to kotlin

* Convert CommonsDaggerBroadcastReceiver to kotlin

* Convert CommonsApplicationModule to kotlin

* Fix imports and make them consistent
2024-11-30 10:50:42 +09:00
Paul Hawke
dac3657536
Migrate kvstore to kotlin (#5973)
* Get good test around the basic KvStore before starting conversion

* Converted BasicKvStore to kotlin and removed dead code

* Converted JsonKvStore to kotlin

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2024-11-29 14:01:29 +09:00
Saifuddin Adenwala
d6c4cab207
Migrated logging module from Java to Kotlin (#5972)
* Migrated logging module from Java to Kotlin

* Rename .java to .kt

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2024-11-29 13:50:33 +09:00
Paul Hawke
1afff73c24
Migrate campaigns package to kotlin (#5969)
* Convert ICampaignsView to kotlin along with simple fix

* Convert CampaignView to Kotlin

* Convert CampaignsPresenter to Kotlin

* Convert CampaignsPresenter to kotlin

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2024-11-29 13:44:26 +09:00
translatewiki.net
a6152f937e
Localisation updates from https://translatewiki.net. 2024-11-28 13:01:55 +01:00
Rohit Verma
794dbb8f92
Fix modification on bottom sheet's data when coming from Nearby Banner and clicked on other pins (#5937)
* refactor getIconFor method and remove call to highlightNearestPlace()

It ensures single responsibility on getIconFor method

* refactor and fix icon issue when coming from nearby banner

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2024-11-28 17:37:56 +09:00
Paul Hawke
0c969c365b
Convert auth package to kotlin (#5966)
* Convert SessionManager to kotlin along with other small fixes

* Convert WikiAccountAuthenticator to kotlin

* Migrate WikiAccountAuthenticatorService to kotlin

* Converted AccountUtil to kotlin

* Convert SignupActivity to kotlin

* Convert LoginActivity to kotlin

* Merge from main
2024-11-28 17:09:25 +09:00
Saifuddin Adenwala
238023056f
Migrated navtab module from Java to Kotlin (#5965)
* Rename .java to .kt

* Migrated navtab module from Java to Kotlin

* Migrated navtab module from Java to Kotlin
2024-11-27 22:58:46 +09:00
Saifuddin Adenwala
381f9eca0c
Migrated notification module from Java to Kotlin (#5955)
* Rename .java to .kt

* Migration of notification module from Java to Kotlin
2024-11-26 21:29:31 +09:00
translatewiki.net
874773b881
Localisation updates from https://translatewiki.net. 2024-11-25 13:01:54 +01:00
Saifuddin Adenwala
00cfd83521
Migrated quiz module from Java to Kotlin (#5952)
* Rename .java to .kt

* Migrated quiz module to Kotlin

* unit test failing fixed

* unit test failing fixed
2024-11-24 19:17:05 +09:00
Saifuddin Adenwala
bafae821e2
Migration of review module from Java to Kotlin (#5950)
* Rename .java to .kt

* Migrated repository module to Kotlin

* Rename .java to .kt

* Migrated review module to Kotlin
2024-11-23 21:45:46 +09:00
Rohit Verma
e070c5dbe8
Fix unit tests (#5947)
* move createLocale() method to companion object and add test dependency

* use mockk() from Mockk library for mocking sealed classes

* change method parameter to null-able String type

* add null check for accessing property from unit tests

* change method signature to match old method's signature

It fixes the NullPointerException when running ImageProcessingUnitTest

* Fix unresolved references and make properties public for unit tests

* fix tests in UploadRepositoryUnitTest by making return type null-able
2024-11-23 08:35:34 +09:00
Saifuddin Adenwala
fe347c21fd
Migrated recentlanguages and repository module from Java to Kotlin (#5948)
* Rename .java to .kt

* Migrated recentlanguages module to Kotlin

* Rename .java to .kt

* Migrated repository module to Kotlin
2024-11-22 22:58:16 +09:00
u7479759
088dd2479e
Changed to data classes, and added immutability (#5905)
Co-authored-by: Jinniu Du <127721018+Donutcheese@users.noreply.github.com>
2024-11-21 21:42:54 +09:00
Saifuddin Adenwala
cf88f9b796
Migrated settings modules from Java to Kotlin (#5944)
* Rename .java to .kt

* Migrated settings module to Kotlin
2024-11-21 21:16:42 +09:00
translatewiki.net
5f1d284309
Localisation updates from https://translatewiki.net. 2024-11-21 13:01:41 +01:00
Saifuddin Adenwala
ed18a37577
Migrated ui and theme modules from Java to Kotlin (#5942)
* Rename .java to .kt

* Migrated ui and theme module to Kotlin
2024-11-20 22:55:13 +09:00
Saifuddin Adenwala
cb4ffd8ca8
Migrated widget module from Java to Kotlin (#5940)
* Rename .java to .kt

* Migrated widget module to Kotlin
2024-11-20 12:41:50 +09:00
Saifuddin Adenwala
0fdb0044b9
Migrated util module from Java to Kotlin (#5938)
* Rename .java to .kt

* Migrated the following files in util module to Kotlin
- AbstractTextWatcher
- ActivityUtils
- CommonsDateUtil
- DateUtil

* Rename .java to .kt

* Migrated the following files in util module to Kotlin
- DeviceInfoUtil
- ExecutorUtils
- FragmentUtils

* Rename .java to .kt

* Migrated the following files in util module to Kotlin
- ImageUtils
- ImageUtilsWrapper
- LangCodeUtils
- LayoutUtils
- LengthUtils
- LocationUtils
- MapUtils

* Rename .java to .kt

* Migrated all remaining files in util module
2024-11-18 22:40:35 +09:00
translatewiki.net
c439143dd3
Localisation updates from https://translatewiki.net. 2024-11-18 13:01:52 +01:00
Saifuddin Adenwala
5c8c4032e9
Migrated util module files from java to kotlin (#5935)
* Rename .java to .kt

* Migrated the following files in util module to Kotlin
- AbstractTextWatcher
- ActivityUtils
- CommonsDateUtil
- DateUtil

* Rename .java to .kt

* Migrated the following files in util module to Kotlin
- DeviceInfoUtil
- ExecutorUtils
- FragmentUtils
2024-11-17 22:35:25 +09:00
translatewiki.net
248c7b0ceb
Localisation updates from https://translatewiki.net. 2024-11-14 13:02:04 +01:00
Alex Gailis
183e84c098
Improve Unique File Name Search (#5877)
* Modified findUniqueFileName() in UploadWorker.kt to use a random 3-digit alphanumeric hash to append to a file name to make it unique. This improves speed over using an incrementing number to append as there are fewer collisions.

* Modified findUniqueFileName() in UploadWorker.kt to use a random 5-digit numeric hash rather than the previous 3-digit alphanumeric hash

* Removed unnecessary variable "chars"

---------

Co-authored-by: Jinniu Du <127721018+Donutcheese@users.noreply.github.com>
Co-authored-by: Zihan Pan <u7726755@anu.edu.au>
Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2024-11-14 11:35:05 +09:00
StrawberryShortcake
634bc3ede1
custom-selector: adds a button to delete the current folder in custom selector (#5925)
* Issue #5811: "Delete folder" menu in custom image selector

* Issue 5811: folder deletion for api < 29.

* Issue 5811: folder deletion for api < 29.

* Issue 5811: folder deletion for api 29.

* Issue 5811: folder deletion

* Issue 5811: fixes merge conflicts, replaces used function onActivityResult with an ActivityResultLauncher

* Update Constants.java

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2024-11-13 22:11:38 +09:00
translatewiki.net
17a8845dfd
Localisation updates from https://translatewiki.net. 2024-11-11 13:01:42 +01:00
Saifuddin Adenwala
a70d585df8
Hide edit options for logged-out users in Explore screen (#5920) 2024-11-07 22:59:32 +10:00
translatewiki.net
3bd7b533d4
Localisation updates from https://translatewiki.net. 2024-11-07 13:01:44 +01:00
translatewiki.net
e388f456dc
Localisation updates from https://translatewiki.net. 2024-11-05 15:18:29 +01:00
translatewiki.net
c46928252c
Localisation updates from https://translatewiki.net. 2024-11-04 13:01:44 +01:00
Rohit Verma
091ddb5db1
revert back to MainScope for database and ui updates (#5914)
Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2024-11-04 18:40:54 +09:00
Kaartic Sivaraam
9ca285be53 Link to the elaborate changelog in GitHub release 2024-11-04 00:17:33 +05:30
Kaartic Sivaraam
707997bd1f Version v5.1.0 2024-11-04 00:10:15 +05:30
translatewiki.net
72a80f0616 Localisation updates from https://translatewiki.net. 2024-11-04 00:09:11 +05:30
Hisham Akmal
f011abef1d
Improve string formatting in OkHttpJsonApiClient.java (#5916) 2024-11-01 09:20:16 +09:00
translatewiki.net
7c826502b6
Localisation updates from https://translatewiki.net. 2024-10-31 13:01:42 +01:00
translatewiki.net
4a46488e4e Localisation updates from https://translatewiki.net. 2024-10-29 07:24:44 +05:30
translatewiki.net
197855af0e
Localisation updates from https://translatewiki.net. 2024-10-28 13:02:08 +01:00
u7119288
610919ec67 Partial fixes for errors and warnings reported by ./gradlew lint (#5885)
* BaseMarker.kt: removed unneeded cast

* TransformImageImpl.kt: removed unreachable code

* ZoomableActivity.kt: removed Unnecessary safe call on a non-null receiver of type ZoomableDraweeView

* ZoomableActivity.kt: removed Unnecessary safe call on a non-null receiver of type ZoomableDraweeView

* DescriptionEditActivity.kt: removed unnecessary non-null assertion (!!) on a non-null receiver of type DescriptionEditHelper

* Media.kt: Property would not be serialized into a 'Parcel'. Added '@IgnoredOnParcel' annotation to remove the warning

* ZoomableActivity.kt: removed Unnecessary non-null assertion (!!) on a non-null receiver of type ZoomableDraweeView

* CategoryClient.kt: removed condition 'page.categoryInfo() == null' as it's always 'false'

* DescriptionEditActivity.kt: removed unnecessary safe call on a non-null receiver of type SessionManager

* DescriptionEditActivity.kt: removed unnecessary safe call on a non-null receiver of type DescriptionEditHelper

* WikidataFeedback.kt: removed unneeded cast

* FailedUploadsFragment.kt: removed unneeded non-null assertion (!!)

* PendingUploadsFragment.kt: removed unneeded non-null assertion (!!)

* AchievementsFragment.java: Changed toUpperCase to toUpperCase(Locale.getDefault())

* ExploreFragment.java: Changed toUpperCase to toUpperCase(Locale.getDefault())

* FileUtils.java: Changed toUpperCase to toUpperCase(Locale.getDefault())

* AchievementsFragment.java: Changed toUpperCase to toUpperCase(Locale.ROOT)

* ExploreFragment.java: Changed toUpperCase to toUpperCase(Locale.ROOT)

* LocationPickerActivity.java: Changed toUpperCase to toUpperCase(Locale.ROOT)

* MediaDetailFragment.java: Changed toUpperCase to toUpperCase(Locale.ROOT)

* NearbyFilterSearchRecyclerViewAdapter.java: Changed toUpperCase to toUpperCase(Locale.ROOT)

* ProfileActivity.java: Changed toUpperCase to toUpperCase(Locale.ROOT)

* RecentSearchesFragment.java: Changed toUpperCase to toUpperCase(Locale.ROOT)

* ReviewActivity.java: Changed toUpperCase to toUpperCase(Locale.ROOT)

* SearchActivity.java: Changed toUpperCase to toUpperCase(Locale.ROOT)

* UploadMediaPresenter.java: Changed toUpperCase to toLowerCase(Locale.ROOT)

* CategoriesMediaFragment.kt: Changed arguments!! to requireArguments()

* ChildDepictionsFragment.kt: Changed arguments!! to requireArguments()

* DepictedImagesFragment.kt: Changed arguments!! to requireArguments()

* DepictsFragment: Changed Objects.requireNonNull(getView()) to requireViews(), Objects.requireNonNull(getActivity())) to requireActivity()

* ParentCategoriesFragment.kt: Changed arguments!! to requireArguments()

* ParentCategoriesFragment.kt: Changed arguments!! to requireArguments()

* SubCategoriesFragment.kt: Changed arguments!! to requireArguments()

* SubCategoriesFragment.kt: Changed Objects.requireNonNull(getView()) to requireViews(), Objects.requireNonNull(getActivity()) to requireActivity()

* UploadMediaDetailFragment.java: Changed arguments!! to requireArguments()

* WikipediaInstructionsDialogFragment.kt: Changed arguments!! to requireArguments()

* BookmarkItemsDao.java: Added @SuppressLint("Range"), as -1 is expected behavior not index doesn't exist for getColumnIndex()

* BookmarkLocationsDao.java: Added @SuppressLint("Range"), as -1 is expected behavior not index doesn't exist for getColumnIndex()

* AndroidManifest.xml: Removed redundant label  android:label="@string/app_name"

* bs\strings.xml: Added missing few quantity

* hr\strings.xml: Added missing few quantity

* hr\strings.xml: Added missing zero, two, few, many quantities for lines 23 - 63

* Revert "hr\strings.xml: Added missing zero, two, few, many quantities for lines 23 - 63"

This reverts commit 47232466ab.

* cy\strings.xml: Added missing zero, two, few, many quantities for lines 23 - 63

* sr\strings.xml: Added missing few quantities for lines 35 to 70

* ro\strings.xml: Added missing few quantity and removed not needed zero for <plurals name="contributions_subtitle" fuzzy="true">

* cs\strings.xml: Added missing few many missing quantities for lines 33 - 74

* lt\strings.xml: Added missing few, many missing quantities for lines 34 - 58

* lt\strings.xml: Replaced . . . with ...

* ca\strings.xml: Added missing many quantities for lines 21 - 51, replaced . . . with ...

* ser\strings.xml: Added missing few quantities, replaced . . . with ...

* br\strings.xml: Added missing two, few, many quantities

* pt\strings.xml: Added missing many quantity, changed . . . to ... and ignored typo as it is correct for European Portuguese

* it\strings.xml: changed . . . to ...

* pt\strings.xml: fixed many quantity

* ca\strings.xml: fixed many quantity

* sr\strings.xml: fixed many quantity

* cy\strings.xml: corrected quantities for "share_license_summary

* fr\strings.xml: changed . . . to ... and add many quantities using the other quantity

* fr\strings.xml: changed . . . to ... and add many quantities using the other quantity. Fixed some typos, added ignore for correct spellings but has warning

* getColumnIndex(): added @SuppressLint("Range") as -1 is expected result for column name doesn't exist

* values-b+sr+Latn\strings.xml: changed . . . to ...

* Revert "values-b+sr+Latn\strings.xml: changed . . . to ..."

This reverts commit 95b909c29f.

* values-b+roa+tara\strings.xml: changed . . . to ...

* Revert "values-b+roa+tara\strings.xml: changed . . . to ..."

This reverts commit b5db1a3e68.

* values-b+roa+tara\strings.xml: changed . . . to ...

* values-b+sr+Latn\strings.xml: changed . . . to ..., add few based on other quantity. Ignored one ImpliedQuantity warning as it is correct.

* it\strings.xml: changed . . . to ..., add many based on other quantity.

* pt-rBR\strings.xml: changed . . . to ..., add many based on other quantity. Fixed typos, ignored warning for "one" quantity as translation didn't use number

* si\strings.xml: Ignored ImpliedQuantity warning as it uses 1 not %d. Removed not needed zero quantity

* si\strings.xml: Ignored ImpliedQuantity warning as it uses 1 not %d. Removed not needed zero quantity. Fixed wrong %1$d. Changed . . . to ...

* mk\strings.xml: changed . . . to ... and ignored ImpliedQuantity as it doesn't use 1

* sl\strings.xml: changed . . . to ... and ignored ImpliedQuantity as it doesn't use %1$d

* ru\strings.xml: changed . . . to ... and ignored ImpliedQuantity as it doesn't use %1$d

* uk\strings.xml: changed . . . to ... and ignored ImpliedQuantity as it doesn't use %1$d

* is\strings.xml: changed . . . to ... and ignored ImpliedQuantity as it doesn't use %1$d

* strings.xml: changed . . . to ...

* af\strings.xml: removed not needed zero quantity

* de\strings.xml: fixed duplicate word typo

* diq\strings.xml: changed - to dash (-)

* hi\strings.xml: removed not needed zero

* in\strings.xml: removed not needed one quantity

* iw\strings.xml: removed not needed many quantity

* ja\strings.xml: removed not needed one quantity

* ko\strings.xml: removed not needed one quantity

* ky\strings.xml: removed not needed one quantity

* mr\strings.xml: removed not needed zero quantity

* my\strings.xml: removed not needed one quantity

* su\strings.xml: removed not needed one quantity

* th\strings.xml: removed not needed one and zero quantity

* zh\strings.xml: removed not needed one quantity

* activity_description_edit.xml: changed android:tint to app:tint, changed layout_alignParentRight to layout_alignParentEnd

* bottom_sheet_details_explore.xml: changed android:tint to app:tint, added focusable, changed to margin layout

* bottom_sheet_item_layout.xml: changed android:tint to app:tint, added focusable

* bottom_sheet_details_explore.xml: changed android:tint to app:tint, added focusable, changed margin layout and removed not needed and invalid params

* item_place.xml.xml: changed android:tint to app:tint, added focusable, changed margin layout and removed not needed and invalid params

* layout_campagin.xml: changed android:tint to app:tint, added focusable, changed margin layout and removed not needed and invalid params

* layout_contribution.xml.xml: changed android:tint to app:tint

* nearby_card_view.xml: changed android:tint to app:tint, added focusable, changed margin layout and removed not needed and invalid params

* nearby_row_button.xml: changed android:tint to app:tint, added focusable, changed margin layout and removed not needed and invalid params

* toolbar_location_picker.xml: changed android:tint to app:tint

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2024-10-28 08:35:50 +05:30
Parneet Singh
d21c63f1db CommonsApplication migrate to kotlin & some lint fixes (#5879)
* convert to kotlin

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* use lateinit instead of nullable types

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* instance property access fix

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* refactor constants name with uppercased ones

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* remove unused

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* fix imports in test

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* use mockk for kotlin to fix tests

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

---------

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>
2024-10-28 08:35:50 +05:30
Parneet Singh
6adedd9789 fix test (#5893)
Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>
2024-10-28 08:35:50 +05:30
u7119288
522f1fe192
Partial fixes for errors and warnings reported by ./gradlew lint (#5885)
* BaseMarker.kt: removed unneeded cast

* TransformImageImpl.kt: removed unreachable code

* ZoomableActivity.kt: removed Unnecessary safe call on a non-null receiver of type ZoomableDraweeView

* ZoomableActivity.kt: removed Unnecessary safe call on a non-null receiver of type ZoomableDraweeView

* DescriptionEditActivity.kt: removed unnecessary non-null assertion (!!) on a non-null receiver of type DescriptionEditHelper

* Media.kt: Property would not be serialized into a 'Parcel'. Added '@IgnoredOnParcel' annotation to remove the warning

* ZoomableActivity.kt: removed Unnecessary non-null assertion (!!) on a non-null receiver of type ZoomableDraweeView

* CategoryClient.kt: removed condition 'page.categoryInfo() == null' as it's always 'false'

* DescriptionEditActivity.kt: removed unnecessary safe call on a non-null receiver of type SessionManager

* DescriptionEditActivity.kt: removed unnecessary safe call on a non-null receiver of type DescriptionEditHelper

* WikidataFeedback.kt: removed unneeded cast

* FailedUploadsFragment.kt: removed unneeded non-null assertion (!!)

* PendingUploadsFragment.kt: removed unneeded non-null assertion (!!)

* AchievementsFragment.java: Changed toUpperCase to toUpperCase(Locale.getDefault())

* ExploreFragment.java: Changed toUpperCase to toUpperCase(Locale.getDefault())

* FileUtils.java: Changed toUpperCase to toUpperCase(Locale.getDefault())

* AchievementsFragment.java: Changed toUpperCase to toUpperCase(Locale.ROOT)

* ExploreFragment.java: Changed toUpperCase to toUpperCase(Locale.ROOT)

* LocationPickerActivity.java: Changed toUpperCase to toUpperCase(Locale.ROOT)

* MediaDetailFragment.java: Changed toUpperCase to toUpperCase(Locale.ROOT)

* NearbyFilterSearchRecyclerViewAdapter.java: Changed toUpperCase to toUpperCase(Locale.ROOT)

* ProfileActivity.java: Changed toUpperCase to toUpperCase(Locale.ROOT)

* RecentSearchesFragment.java: Changed toUpperCase to toUpperCase(Locale.ROOT)

* ReviewActivity.java: Changed toUpperCase to toUpperCase(Locale.ROOT)

* SearchActivity.java: Changed toUpperCase to toUpperCase(Locale.ROOT)

* UploadMediaPresenter.java: Changed toUpperCase to toLowerCase(Locale.ROOT)

* CategoriesMediaFragment.kt: Changed arguments!! to requireArguments()

* ChildDepictionsFragment.kt: Changed arguments!! to requireArguments()

* DepictedImagesFragment.kt: Changed arguments!! to requireArguments()

* DepictsFragment: Changed Objects.requireNonNull(getView()) to requireViews(), Objects.requireNonNull(getActivity())) to requireActivity()

* ParentCategoriesFragment.kt: Changed arguments!! to requireArguments()

* ParentCategoriesFragment.kt: Changed arguments!! to requireArguments()

* SubCategoriesFragment.kt: Changed arguments!! to requireArguments()

* SubCategoriesFragment.kt: Changed Objects.requireNonNull(getView()) to requireViews(), Objects.requireNonNull(getActivity()) to requireActivity()

* UploadMediaDetailFragment.java: Changed arguments!! to requireArguments()

* WikipediaInstructionsDialogFragment.kt: Changed arguments!! to requireArguments()

* BookmarkItemsDao.java: Added @SuppressLint("Range"), as -1 is expected behavior not index doesn't exist for getColumnIndex()

* BookmarkLocationsDao.java: Added @SuppressLint("Range"), as -1 is expected behavior not index doesn't exist for getColumnIndex()

* AndroidManifest.xml: Removed redundant label  android:label="@string/app_name"

* bs\strings.xml: Added missing few quantity

* hr\strings.xml: Added missing few quantity

* hr\strings.xml: Added missing zero, two, few, many quantities for lines 23 - 63

* Revert "hr\strings.xml: Added missing zero, two, few, many quantities for lines 23 - 63"

This reverts commit 47232466ab.

* cy\strings.xml: Added missing zero, two, few, many quantities for lines 23 - 63

* sr\strings.xml: Added missing few quantities for lines 35 to 70

* ro\strings.xml: Added missing few quantity and removed not needed zero for <plurals name="contributions_subtitle" fuzzy="true">

* cs\strings.xml: Added missing few many missing quantities for lines 33 - 74

* lt\strings.xml: Added missing few, many missing quantities for lines 34 - 58

* lt\strings.xml: Replaced . . . with ...

* ca\strings.xml: Added missing many quantities for lines 21 - 51, replaced . . . with ...

* ser\strings.xml: Added missing few quantities, replaced . . . with ...

* br\strings.xml: Added missing two, few, many quantities

* pt\strings.xml: Added missing many quantity, changed . . . to ... and ignored typo as it is correct for European Portuguese

* it\strings.xml: changed . . . to ...

* pt\strings.xml: fixed many quantity

* ca\strings.xml: fixed many quantity

* sr\strings.xml: fixed many quantity

* cy\strings.xml: corrected quantities for "share_license_summary

* fr\strings.xml: changed . . . to ... and add many quantities using the other quantity

* fr\strings.xml: changed . . . to ... and add many quantities using the other quantity. Fixed some typos, added ignore for correct spellings but has warning

* getColumnIndex(): added @SuppressLint("Range") as -1 is expected result for column name doesn't exist

* values-b+sr+Latn\strings.xml: changed . . . to ...

* Revert "values-b+sr+Latn\strings.xml: changed . . . to ..."

This reverts commit 95b909c29f.

* values-b+roa+tara\strings.xml: changed . . . to ...

* Revert "values-b+roa+tara\strings.xml: changed . . . to ..."

This reverts commit b5db1a3e68.

* values-b+roa+tara\strings.xml: changed . . . to ...

* values-b+sr+Latn\strings.xml: changed . . . to ..., add few based on other quantity. Ignored one ImpliedQuantity warning as it is correct.

* it\strings.xml: changed . . . to ..., add many based on other quantity.

* pt-rBR\strings.xml: changed . . . to ..., add many based on other quantity. Fixed typos, ignored warning for "one" quantity as translation didn't use number

* si\strings.xml: Ignored ImpliedQuantity warning as it uses 1 not %d. Removed not needed zero quantity

* si\strings.xml: Ignored ImpliedQuantity warning as it uses 1 not %d. Removed not needed zero quantity. Fixed wrong %1$d. Changed . . . to ...

* mk\strings.xml: changed . . . to ... and ignored ImpliedQuantity as it doesn't use 1

* sl\strings.xml: changed . . . to ... and ignored ImpliedQuantity as it doesn't use %1$d

* ru\strings.xml: changed . . . to ... and ignored ImpliedQuantity as it doesn't use %1$d

* uk\strings.xml: changed . . . to ... and ignored ImpliedQuantity as it doesn't use %1$d

* is\strings.xml: changed . . . to ... and ignored ImpliedQuantity as it doesn't use %1$d

* strings.xml: changed . . . to ...

* af\strings.xml: removed not needed zero quantity

* de\strings.xml: fixed duplicate word typo

* diq\strings.xml: changed - to dash (-)

* hi\strings.xml: removed not needed zero

* in\strings.xml: removed not needed one quantity

* iw\strings.xml: removed not needed many quantity

* ja\strings.xml: removed not needed one quantity

* ko\strings.xml: removed not needed one quantity

* ky\strings.xml: removed not needed one quantity

* mr\strings.xml: removed not needed zero quantity

* my\strings.xml: removed not needed one quantity

* su\strings.xml: removed not needed one quantity

* th\strings.xml: removed not needed one and zero quantity

* zh\strings.xml: removed not needed one quantity

* activity_description_edit.xml: changed android:tint to app:tint, changed layout_alignParentRight to layout_alignParentEnd

* bottom_sheet_details_explore.xml: changed android:tint to app:tint, added focusable, changed to margin layout

* bottom_sheet_item_layout.xml: changed android:tint to app:tint, added focusable

* bottom_sheet_details_explore.xml: changed android:tint to app:tint, added focusable, changed margin layout and removed not needed and invalid params

* item_place.xml.xml: changed android:tint to app:tint, added focusable, changed margin layout and removed not needed and invalid params

* layout_campagin.xml: changed android:tint to app:tint, added focusable, changed margin layout and removed not needed and invalid params

* layout_contribution.xml.xml: changed android:tint to app:tint

* nearby_card_view.xml: changed android:tint to app:tint, added focusable, changed margin layout and removed not needed and invalid params

* nearby_row_button.xml: changed android:tint to app:tint, added focusable, changed margin layout and removed not needed and invalid params

* toolbar_location_picker.xml: changed android:tint to app:tint

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2024-10-27 22:59:09 +09:00
Rohit Verma
cdc4f89da5
Database bug fix (#5902)
* make database function calls suspending and update room version

* replace MainScope with coroutineScope for database operations

* add suspend keyword and refactor code
2024-10-27 22:38:40 +09:00
Parneet Singh
bc065c8792
CommonsApplication migrate to kotlin & some lint fixes (#5879)
* convert to kotlin

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* use lateinit instead of nullable types

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* instance property access fix

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* refactor constants name with uppercased ones

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* remove unused

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* fix imports in test

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* use mockk for kotlin to fix tests

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

---------

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>
2024-10-26 23:19:34 +09:00
Parneet Singh
7c58891892
fix test (#5893)
Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>
2024-10-26 13:32:35 +09:00
Noah Vendrig
3e020ed973
Fixes #5806 Implemented "Refresh" button to clear the cache and reload the Nearby map (#5891)
* Changed files required to get the app to run correctly. Removed suspend from affected DAO files and funcs, and changed to (Kotlin v1.9.22) and (Kotlin compiler v1.5.8)

* Created refresh button icon, and added it to the nearby_fragment_menu.xml (header of the nearby page). Created function refresh() in NearbyParentFragment.java to handle refresh functionality.

* Replaced refresh() func with emptyCache() and reloadMap()

* Attempt at reloadMap(), no testing done yet.

* added changes for a possibly working emptyCache implementation (needs testing).

* Tested changes as working, edited emptyCache to correctly clear cache and then reload map

---------

Co-authored-by: MarcusBarta <marcusbarta@icloud.com>
2024-10-25 14:19:07 +09:00
translatewiki.net
becc07d26b
Localisation updates from https://translatewiki.net. 2024-10-24 14:02:18 +02:00
Parneet Singh
1659a4ce22
add dependency (#5887)
Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>
2024-10-24 19:07:16 +09:00
Parneet Singh
1e7aabad16
Use new result API (#5875)
* remove unused result expectancy for settings screen launch

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* initial refactor to new result api, wip

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* refactor camera launcher

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* revert callback for video handling

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* invoke callbacks when cancelled

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* handle gallery picker result based on preference

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* remove old method of refactoring for file picker

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* remove legacy result handling callback

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* request code used for handling result was never used for launching an activity, hence removed

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* extract voice result handling into function

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* refactor test

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* remove unused tests

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* cleanup

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* fix-docs

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* add space after ,

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

---------

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>
2024-10-23 21:58:39 +09:00
cambo14
f1205c19be
UploadMediaDetailAdapter: made selecting a language deselect all others (#5883)
>> Made it so that selecting a language results in the hashmap storing the currently selected language(s) being cleared. Considered refactoring the hashmap storing this into a single pair storing the language positition index and its code, as only one language should ever be selected, however I am not confident that this would not introduce unintended side-effects
2024-10-23 03:50:37 +10:00
whe128
9c1c95f5cf
Fix for #5846: After uploading via Nearby, I am sent back to Nearby, where I am mislead into thinking that I must upload again #5846 (#5874)
Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2024-10-23 02:56:02 +10:00
Hanna Truong
ba7348f83f
Fixes issue #5841: Nearby pins: Make it easier to understand what the colors mean (#5881)
* UI design for legend to explain the colors of the nearby pins

* Add listener for the button to toggle the visibility of the legend (make it hideable)

* Change wording for legend and make text localizable

* Fixed typo

* Fixed typo

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2024-10-22 21:27:40 +09:00
myyyy
7b0b604834
Fix: Prevent RecyclerView from resetting scroll position after returning from preview (#5873) (#5880)
Resolved an issue where RecyclerView would incorrectly scroll to the top after exiting fullscreen preview. Adjusted scroll behavior to maintain position unless actioned images are filtered. Implemented observer logic adjustments to handle dataset updates more efficiently.

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2024-10-21 22:41:32 +09:00
translatewiki.net
014feb54e5
Localisation updates from https://translatewiki.net. 2024-10-21 14:02:00 +02:00
LachlanMajor
56d0beb22a
Fixes #5840 Custom select folder display breaks after exiting media preview (#5866)
* ImageFragment.kt: notifyDataSetChanged() added to update observers about init call in handleResult()

* ImageFragment.kt: unnecessary initialisation after exiting media preview was removed from passSelectedImages
2024-10-20 21:36:15 +10:00
Christo Joby Antony
63f1ed8a2d
Fix for #5808: Update the cached Place Items on the successful association of Wiki Item (#5864)
* (fixes #5855) fetch item label in nearby based on user configured language.

* implement a method to delete Place entity from database

* clear the cache for a item when a wikiItem is updated.

* fix style issue

* Update placesRepository on successful WikiItemEdit

* Revert the delete operations added to the PlaceDAO

* Set name of the place to avoid race condition with NearbyParentFragment. Update Success Notification to show only after the pin has been updated.

* Clean-up

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2024-10-19 22:20:35 +09:00
ErenratZeng
c77e5abba7
notification: classify email messages and add mail check prompt (#5865)
This commit adds logic to classify notifications as "email" type when the notification text contains "sent you an email". It also updates the email notification prompt to support localization, ensuring a better user experience across different regions.

### Problem:
1. Previously, email-related notifications from the backend were missing a URL. As a result, when users clicked on these notifications, there was no link to open, and the notifications were categorized as UNKNOWN. This led to a poor user experience since there was no feedback provided when the user clicked on an email notification.
2. Additionally, the existing code used hardcoded English text for the email notification prompt, which did not provide a localized experience for users in different regions.

### Solution:
1. Added logic to categorize email-related notifications as `EMAIL` when the notification text contains "sent you an email".
2. Replaced the hardcoded "Check your email inbox" string with a localized string and added translations for multiple languages, including zh, zh-rhk, zh-rcn, zh-rtw, and ja.

### Changes:
- **NotificationClient**:
  - Modified `WikimediaNotification.toCommonsNotification()` to check if the notification text contains "sent you an email". If it does, the notification is classified as `EMAIL_MESSAGE` instead of the default `UNKNOWN`.

- **NotificationActivity**:
  - In the `NotificatinAdapter` click handler, added a check for `EMAIL_MESSAGE` type. When an email-type notification is clicked, a localized "Check your mail box" prompt is shown using `Snackbar`, instead of attempting to open a URL (which is typically missing for such notifications).
  - Modified to fetch the string using `getString(R.string.check_your_mail_box)` to support localization.

- **NotificationType**:
  - Added a new `EMAIL` type to categorize email-related notifications.

- **Localization**:
  - Added localized translations for "Check your mail box" in zh, zh-rhk, zh-rcn, zh-rtw, and ja.

Co-authored-by: Qiutong Zeng <Qiutong.zeng@anu.edu.au>
2024-10-18 15:31:54 +09:00
Parneet Singh
817e07b921
set permission required only upto android 5.1 (#5863)
Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>
2024-10-18 10:15:56 +09:00
translatewiki.net
82c7dcfe18
Localisation updates from https://translatewiki.net. 2024-10-17 14:02:05 +02:00
Andrew Gardner
0a7fe662d4
(fixes #3464) Replace assert() usages with assertThat() (#5861)
* Updated instances of assert() in WelcomeActivityTest.kt to asserThat().

New imports:
 - org.hamcrest.CoreMatchers.equalTo
 - org.hamcrest.CoreMatchers.assertThat

* Updated instances of assert() in LatLngTest.kt to asserThat().

New imports:
 - org.hamcrest.CoreMatchers.equalTo
 - import org.hamcrest.CoreMatchers.not
 - org.hamcrest.CoreMatchers.assertThat

* Updated instances of assert() in LabelTest.kt to asserThat().

New imports:
 - org.hamcrest.CoreMatchers.equalTo
 - org.hamcrest.CoreMatchers.assertThat

* Corrected sign error typo in LatLngTest.kt
2024-10-16 22:22:48 +09:00
Zhenhao Li
ef3f6b7977
solve the issue 5856 (#5860) 2024-10-16 22:01:14 +09:00
Christo Joby Antony
3ac608c82c
(fixes #5855) fetch item label in nearby based on user configured language. (#5858) 2024-10-16 12:49:38 +09:00
Parneet Singh
f889ed1821
Refactor FilePicker.java intent result handling (#5851)
* remove unnecessary video flag check

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* handle when custom selector operation cancelled

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* dispatch appropriate request code to handle using respective callbacks

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* remove wrong control statements

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* refactor gallery picker test

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

---------

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>
2024-10-16 11:20:00 +09:00
translatewiki.net
95e0a0d143
Localisation updates from https://translatewiki.net. 2024-10-14 14:02:11 +02:00
Nicolas Raoul
dadc4b6a11
Bumped coordinates2country, recognizes more countries 2024-10-13 22:56:53 +09:00
translatewiki.net
ba7a559714
Localisation updates from https://translatewiki.net. 2024-10-10 14:01:43 +02:00
translatewiki.net
c7065e103b
Localisation updates from https://translatewiki.net. 2024-10-07 14:01:46 +02:00
translatewiki.net
f47a0dd49c
Localisation updates from https://translatewiki.net. 2024-10-03 14:01:48 +02:00
tristan
d0e64d7886
Issue 5835 nearby (#5843)
* NearbyParentFragment.java:
OnScroll - removed distance threshold, delay search by 800ms, discard multiple OnScroll within 800ms.
OnDestroy - destroy any queued OnScroll events

* NearbyParentFragment.java:
loadPlacesDataAsync - set batchSize from 50 back to original 3

* comment

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2024-10-02 13:03:25 +09:00
translatewiki.net
3d49b1f79a
Localisation updates from https://translatewiki.net. 2024-09-30 14:01:51 +02:00
translatewiki.net
16a1375800
Localisation updates from https://translatewiki.net. 2024-09-23 14:01:49 +02:00
translatewiki.net
249e284a93
Localisation updates from https://translatewiki.net. 2024-09-19 14:02:01 +02:00
tristan
2d82a430c4
Issue-5662-kotlinstyle (#5833)
* *.kt: bulk correction of formatting using ktlint --format

* *.kt: replace wildcard imports and second stage auto format ktlint --format

* QuizQuestionTest.kt: modified property names to camel case to meet ktlint standard

* LevelControllerTest.kt: modified property names to camel case to meet ktlint standard

* QuizActivityUnitTest.kt: modified property names to camel case to meet ktlint standard

* MediaDetailFragmentUnitTests.kt: modified property names to camel case to meet ktlint standard

* UploadWorker.kt: modified property names to camel case to meet ktlint standard

* UploadClient.kt: modified property names to camel case to meet ktlint standard

* BasePagingPresenter.kt: modified property names to camel case to meet ktlint standard

* DescriptionEditActivity.kt: modified property names to camel case to meet ktlint standard

* OnSwipeTouchListener.kt: modified property names to camel case to meet ktlint standard

* MediaDetailFragmentUnitTests.kt: corrected excessive line length to meet ktlint standard

* DepictedItem.kt: corrected property name format and catch format to for  ktlint standard

* UploadCategoryAdapter.kt: corrected class definition format to meet ktlint standard

* CustomSelectorActivity.kt: reformatted function names to first letter lowercase to meet ktlint standard

* MediaDetailFragmentUnitTests.kt: fix string literal indentation to meet ktlint standard

* NotForUploadDao.kt: file renamed to match class name, new file NotForUploadStatusDao.kt

* UploadedDao.kt: file renamed to match class name, new file UploadedStatusDao.kt

* Urls.kt: fixed excessive line length for ktLint standard

* Snak_partial.kt & Statement_partial.kt: refactored to remove underscores in class names to meet ktLint standard

* *.kt: fixed consecutive KDOC error for ktLint

* PageableBaseDataSourceTest.kt & UploadPresenterTest.kt: fixed excessive line lengths to meet ktLint standard

* CheckboxTriStatesTest.kt: renamed file to match class name to meet ktLint standard

* .kt: resolved backing-property-naming error in ktLint, made matching properties public, matched names and refactored

* TestConnectionFactory.kt: fixed property naming to adhere to ktLint standard
2024-09-19 13:56:45 +09:00
Jason-Whitmore
950539c55c
Fixes Issue #5713: "Edit location" is hard to use (and confusing) (#5767)
* LocationPickerActivity.java: fix "Show in Map App" bug

Once the "Show in Map App" button is pressed, the Map app will center on the current photo's EXIF location,
if that data is available. If not, the map app will center on where the location picker's map is centered.
Javadoc updated to reflect this small change.

* LocationPickerActivity.java: add methods to easily move the map center

Before this change, any time the map center had to move, several lines of code had to be written.

This change creates several methods which move the map center to a specified location. By using these new methods,
moving the map center only takes one simple method call.

* LocationPickerActivity.java: add null check to method

The original method did not include a null check for the input GeoPoint.

This change includes a null check. If the input Geopoint is null, the method will simply return.

* LocationPickerActivity.java: remove redundant method, renaming method

Before this change, there were two methods with the same behavior.

This change removes the newer method and renames the old method to the descriptive name the newer one had.
Additionally, code which calls this method has been changed to reflect the new name.

* LocationPickerActivity.java: rearrange method calls.

Before this change, a method call to move the map to the device's GPS location was called at the very end of the map setup method.
This would move the map away from the media's EXIF location data (if it was available).

This change places the method call to move the map to the device's GPS location before the method call to move the map to the media's EXIF location (if the data is available).
In short, if no exif data is available, the map attempts to move to the device's GPS location. If the exif location data does exist, the map will move to that location.

* LocationPickerActivity.java: rewrite method scope, name, and documentation

Before this commit, the method name was unclear and the documentation did not fully explain the method's behavior. Additionally, it was a public method.

This commit renames the method (also changing calls to it), adds more documentation, and changes the method scope from public to private.

* LocationPickerActivity.java: create method to move location picker map to device GPS location

Before this method was created, several lines of code were required to move the location picker map to the device's GPS location.

After this method was created, the location picker map can be moved to the GPS location in a single method call.

* LocationPickerActivity.java: add code to move map to either EXIF or device GPS location

Before this change, there was no explicit if check on whether there was EXIF data available before moving the location picker map.

After this change, an explicit if check (which checks the activity name string) will move the location picker map to either
the EXIF location (if that data is available) or to the device's GPS location (if EXIF data is not available).

* LocationPickerActivity.java: refactor showInMapApp method

Before this change, the showInMapApp method had correct behavior, but could be rewritten to become more clear.

After this change, the showInMapApp code has been rewritten.
Specifically, common code in an if statement has been factored out.

* LocationPickerActivity.java: add null checks to showInMapApp

Before this change, there was no null checks when accessing data from an object,
which could create null pointer exceptions at runtime.

After this change, null checks are introduced to make sure null pointer exceptions will not occur.

* LocationPickerActivity.java: rewrite comments to clarify if statement

Before this change, a comment in showInMapApp did not properly describe an if statement's intended behavior.

After this change, the old comment was removed and 2 new comments were added in each part of the if statement
to properly describe the intended behavior.

* LocationPickerActivity.java: replace code with a method call

Before this change, there was several lines of code which moved the map center to the EXIF location.

After this change, the several lines of code have been replaced with a simpler method call which produces
the same result.

* LocationPickerActivity.java: remove redundant code

Before this change, there was two sections of code which moved the map center to the same location. Both of these code sections
occur very close to each other (one in onCreate(), the other in setupMapView())

After this change, the code section in onCreate() has been removed. With the map centering code only in the setupMapView() method.
This change eliminates redundancy, reduces the amount of code in onCreate(), and makes this java file easier to understand.

* content_location_picker.xml: adjust picker pin and shadow location

Before this commit, the location picker pin and shadow graphics were incorrectly located on the screen.
Specifically, the bottom of the pin was not located at the center of the view. Since pressing the
checkmark button saves the location at the center of the view, users would most likely save the wrong location.

After this commit, the location picker pin and shadow graphics have been moved upward. The shadow graphic is
now located at the exact center of the view, and the bottom of the location picker pin is directly over the
center as well. Users may now use the bottom of the pin as an accurate location picker.

* LocationPickerActivity.java: fix bug with permissions menu moving map

Prior to this change, if the menu asking for permissions to access the device's GPS was accepted, the map
would automatically move to the most recent or current GPS location, rather than staying at the uploaded
image's available EXIF location.

After this change, the map will stay centered at the image's available EXIF location, as intended.
The relevant method name and javadoc have been changed to more accurately describe the method's behavior.

* LocationPickerActivity.java: fix map centering bug when editing location on already uploaded media

Before this commit, when the user pressed the edit location icon on media which was already uploaded,
the map would center on the device's current GPS location.

After this commit, pressing the same edit location icon will now center the map on the location metadata.
This is done by checking the activity string to see if the media is already uploaded. If so, a method
is called to center the map on the media's location metadata.

* LocationPickerActivity.java: replace references to EXIF in documentation

Before this commit, there were several mentions of EXIF location data in the javadocs. It is not clear if
this is correct, since many images have location data that is chosen by the user rather than derived from EXIF.

After this commit, the more generic term "location metadata" is used in place of EXIF location data.
Hopefully this change will help avoid confusion in the future.

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2024-09-18 21:18:46 +09:00
Rajat Sarangal
f751ab4a75
Optimize the Layout ViewBinding (#5830) 2024-09-18 11:43:48 +09:00
Rohit Verma
3e915f9848
Upgrade to SDK 34 (#5790)
* change the overridden method signature as per API 34

* add version check condition to compare with API 23 before adding flag

* refactor: add final keywords, fix typo, and remove redundant spaces

For optimized code only

* upgrade: migrate to SDK 34 and upgrade APG

Additionally, add Jetpack Compose to the project

* AndroidManifest: add new permission for API 34

DescriptionActivity should not be exposed

* refactor: permission should not be check on onCreate for some cases

* add method to get correct storage permission and check partial access

Additionally, add final keywords to reduce compiler warnings

* refactor: prevent app from crashing for SDKs >= 34

* add new UI component to allows user to manage partially access photos

Implement using composeView

* change the overridden method signature as per API 34

* add version check condition to compare with API 23 before adding flag

* refactor: add final keywords, fix typo, and remove redundant spaces

For optimized code only

* upgrade: migrate to SDK 34 and upgrade APG

Additionally, add Jetpack Compose to the project

* AndroidManifest: add new permission for API 34

DescriptionActivity should not be exposed

* refactor: permission should not be check on onCreate for some cases

* add method to get correct storage permission and check partial access

Additionally, add final keywords to reduce compiler warnings

* refactor: prevent app from crashing for SDKs >= 34

* add new UI component to allows user to manage partially access photos

Implement using composeView

* replace deprecated circular progress bar with material progress bar

* remove redundant appcompat dependency

* add condition to check for partial access on API >= 34

It prevents invoking photo picker on UploadActivity.

* UploadWorker: add foreground service type

* fix typos in UploadWorker.kt

* add permission to access media location
2024-09-17 23:58:44 +09:00
translatewiki.net
eb027b74ce
Localisation updates from https://translatewiki.net. 2024-09-16 14:01:51 +02:00
ujjwal2900
9393dda9a4
Added date and time to Feedback (#5797)
* Add Date and Time in UTC format to Feedback

* Add UTC date to the Subject instead of adding it to the body

* Change the UTC Date format to yyyy/MM/dd HH:mm:ss

* Minor changes

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2024-09-13 14:39:02 +09:00
translatewiki.net
c3cd30ce5c
Localisation updates from https://translatewiki.net. 2024-09-12 14:02:01 +02:00
Rohit Verma
9edde234ec
Localization: fix incorrect Unicode for space after bullet points in French language (#5824)
* localization: fix incorrect unicode for space

* Remove bullet string resource as it doesn't need translation
2024-09-10 17:41:45 +09:00
translatewiki.net
76e4c38299
Localisation updates from https://translatewiki.net. 2024-09-09 14:01:50 +02:00
translatewiki.net
65ae5060c5
Localisation updates from https://translatewiki.net. 2024-09-05 14:04:50 +02:00
translatewiki.net
b9bb9bcf34
Localisation updates from https://translatewiki.net. 2024-09-02 14:01:50 +02:00
Kanahia
93f1e1ec29
Added pending uploads screen (#5752)
* Added pending uploads screen

* Added failed uploads fragment

* Improved progress bars

* Implemented pause functionality

* Improved pause feature

* Fixed issue with sorting when adding more pictures during an upload

* Improved Tap to View notification

* Fixed issue with on going upload deletion

* Improved the deletion feature

* Fixed indentations and unit tests

* Fixed bugs

* Fixed failing test

* Added error message in Failed Uploads Fragment

* Improved error notification

* Moved auto-retry from the Main Activity to UploadProgressActivity

* Fixed large uploads issue

* Minor fixes

* Removed HashSet

* Fixed issue with progress bar

* Bug fixes

* Moved Auto Retry to MainActivity

* Fixed conflicts

* Fixed issue with upload icon

* Fixed null ptr issue on changing modes

* Improved recycler view

* Fixed irrelevant network call

* Fixed irrelevant network call

* Fixed constantly failing uploads

* Fixed constantly failing uploads

* Fixed constantly failing uploads

* Added error log

* Fixed refresh icon visibility in light mode

* Changed progress in progress activity

* Fixed progress bar issue

* Improved icons

* Improved deletion and removed cancelledUploads Hashset

* Fixed sorting, list size issue

* Improved current implementation

* Implemented flag for workers

* Implemented flag for workers

* Fixed sorting bug

* Fixed upload icon

* Improved pausing

* Made changes to visibility implementation

* Added image duplicity check on restart of failed image

* minor adjustments

* added javadoc/kdoc and fixed minor bug

* Fixed unit tests

* Added synchronized(lock)

* Added check to prevent multiple uploads starting at once

* Ignored failing test cases

* Temporary commit - Added jcenter

* Temporary commit - Removed library/commented

* Temporary commit - Removed library/commented

* Updated com.jraska.livedata:testing-ktx

* Ignored failing test - UploadControllerTest.kt

* Ignored failing test - UploadModelUnitTest

* Ignored failing test - UploadModelUnitTest

* Ignored failing test - UploadModelUnitTest

* Ignored failing test - UploadModelUnitTest

* Ignored failing test - UploadModelUnitTest

* Ignored failing test - UploadModelUnitTest

* Ignored failing test - UploadModelUnitTest

* Ignored failing test - UploadModelUnitTest

* Ignored failing test - UploadPresenterTest.kt

* Ignored failing test - UploadPresenterTest.kt

* Ignored failing test - UploadPresenterTest.kt

* Ignored failing test - UploadPresenterTest.kt

* Ignored failing test - UploadPresenterTest.kt

* Ignored failing test - UploadPresenterTest.kt

* Ignored failing test - UploadPresenterTest.kt

* Ignored failing test - UploadPresenterTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing tests - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - DepictedItemTest.kt

* Ignored failing test - DepictedItemTest.kt

* Ignored failing test - DepictedItemTest.kt

* Ignored failing test - DepictedItemTest.kt

* Ignored failing test - DepictedItemTest.kt

* Ignored failing test - DepictedItemTest.kt

* Ignored failing test - DepictedItemTest.kt

* Ignored failing test - DepictedItemTest.kt

* Ignored failing test - FilesUtilsTest.kt

* Ignored failing test - WikiBaseClientUnitTest.kt

* Ignored failing test - WikiBaseClientUnitTest.kt

* Ignored failing test - WikiBaseClientUnitTest.kt

* Ignored failing test - WikidataClientTest.kt

* Ignored failing test - WikidataClientTest.kt

* Fixed unit tests

* Updated kdoc

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2024-08-30 15:22:54 +09:00
translatewiki.net
62d6dea219
Localisation updates from https://translatewiki.net. 2024-08-29 14:01:37 +02:00
Rohit Verma
190135d36c
Fix failing tests for updateDepictsProperty method (#5795)
* tests: fix failing testUpdateDepictsProperty

* replace deprecated circular progress bar with material progress bar

* refactor: update SettingsActivity to not use custom appCompatDeletegate

It is required because that delegate is automatically handled in new libraries.
2024-08-28 15:29:16 +09:00
Rohit Verma
ec4a6bc0c4
remove asynchronous call to fetch local depictedItems (#5792) 2024-08-27 23:07:12 +09:00
translatewiki.net
39a0b88e3a
Localisation updates from https://translatewiki.net. 2024-08-26 14:02:06 +02:00
Rohit Verma
46df64d208
Prevent deletion of other structured data when editing depicts (#5741)
* restructure :  minor changes to comments to improve readability

* api: remove clear flag to prevent deletion of structured data

* WikiBaseInterface: add new api methods

Get Method: to get claims for an entity
Post method: to delete claims

* WikiBaseClient: add methods to handle response for new APIs

* typo:  update call to method with updated typo

* DepictEditHelper: call update property method with entity id

* refactor: dismiss progress dialog on error

* DepictsDao: remove usage of runBlocking as it was blocking main thread

Refactor methods to perform well with coroutines

* refactor: update usage of method to match changes in DepictsDao

* refactor: use named parameters to improve readability

* claims: add new data classes to represent remove claims

* WikidataEditService: modify update depicts property method

Performs deletion of old claims and creation of new claims

* refactor: make methods more organized
2024-08-26 15:43:50 +09:00
djbloop
31bb1a73c0
First of two fixes for bug #5726: hide nominate for deletion when logged out (#5773) 2024-08-26 13:24:15 +09:00
ujjwal2900
096c075548
Removed duplicate code in addMarkersToMap method (#5783) 2024-08-25 13:01:55 +09:00
Nicolas Raoul
a81d48cc9d
Updating jraska/livedata-testing for GSoC (#5785) 2024-08-22 22:41:27 +09:00
translatewiki.net
a3a5980ebd
Localisation updates from https://translatewiki.net. 2024-08-19 14:02:05 +02:00
translatewiki.net
060d41f973
Localisation updates from https://translatewiki.net. 2024-08-15 14:01:56 +02:00
translatewiki.net
dc6cc82751
Localisation updates from https://translatewiki.net. 2024-08-12 14:01:56 +02:00
translatewiki.net
9a31d2318e
Localisation updates from https://translatewiki.net. 2024-08-08 14:01:40 +02:00
translatewiki.net
862f8f0b04
Localisation updates from https://translatewiki.net. 2024-08-05 14:01:44 +02:00
Kanahia
2d63f351ed
Made Split to Nearby Query into a fast query for coordinates + a details query for each pin (#5731)
* Splitted the query

* Made changes to the query

* Improvised query

* Improvised query by dividing in the batches

* Fixed failing tests

* Improved batches

* Improved sorting

* Fixes issue caused by search this area button

* Fixed failing tests

* Fixed unnecessary reloads on onResume

* Fixed few pins not loading on changing apps

* Improved zoom level and fixed the pins not loading from the center

* Removed toggle chips and changed pin's color

* Fixed wikidata url

* Fixed unit tests

* Implemented retry with delay of 5000ms

* Fixed exception issue and pins issue

* Added change color icon to pin

* Improved pin clicking

* Removed search this area button

* Implemented caching of places

* Fixed unit test

* Factorized methods

* Changed primary key from location to entity id

* Fixed tests

* Fixed conflicts

* Fixed unit test

* Fixed unit test

* Fixed the bug

* Fixed issue with pin loading on the first launch

* Updated javadocs

* Temporary commit - only for testing

* Replaced Temporary commit

* Temporary commit - Added jcenter

* Made minor changes

* Fixed unit tests

* Fixed unit tests

* Fixed minor bug
2024-08-04 09:35:09 +09:00
translatewiki.net
ba6c8fe8d0
Localisation updates from https://translatewiki.net. 2024-08-01 14:01:42 +02:00
translatewiki.net
dbfe3b50f9
Localisation updates from https://translatewiki.net. 2024-07-29 14:02:27 +02:00
translatewiki.net
59da70aca1
Localisation updates from https://translatewiki.net. 2024-07-25 14:01:45 +02:00
translatewiki.net
d2d8eb9153
Localisation updates from https://translatewiki.net. 2024-07-22 14:01:59 +02:00
Kaartic Sivaraam
7d9f8d27bc
menu: rename menu as "User profile" (#5771)
As suggested by @whym on #5754, the name "User page" is ambiguous with
the on-wiki user page. We actually show the leaderboard when the menu
is clicked on. So, rename the menu as "User profile" instead.

Ref: https://github.com/commons-app/apps-android-commons/issues/5754#issuecomment-2196796213
2024-07-20 23:19:16 +10:00
Matija Nalis
7f6b45aeb6
Update allowed recent years to include 2020s (#5761)
* document regex due to #47

* also count 2020s as "recent years"

* clarify that not all years are ignored

* clarify "year" is current year

* original logic fix
from https://github.com/commons-app/apps-android-commons/pull/5761#pullrequestreview-2144120347

* better variale name for ".*0s.*"
as that regex will match e.g. `1920s` and `80s` too, so the original `is20xxsYear` would be confusing name for it

* consolidate duplicated code to spammyCategory

* clarify regexes via variables

* spammyCategory should always be skipped

* return is simple now, so we can get rid of extra val oldDecade

* fix curYearInString

* some clarification comments

* refactor: rename containsYear to isSpammyCategory

This is done as the name containsYear is ambiguous.
It not just checks for year to identify spammy categories.

* refactor: rename containsYear to isSpammyCategory (take 2)

 A continuation of fe74c77ab (refactor: rename containsYear
 to isSpammyCategory, 2024-07-17)

---------

Co-authored-by: Kaartic Sivaraam <kaartic.sivaraam@gmail.com>
2024-07-20 23:16:20 +10:00
Amir E. Aharoni
34addbe33a
Remove unnecessary double quotes from a message (#5777) 2024-07-18 11:47:03 +09:00
Kaartic Sivaraam
5be76044b1
wikidata-feedback: avoid sentence lego (#5775)
Fixes #5763
2024-07-17 16:22:41 +09:00
translatewiki.net
73e9ed8e26
Localisation updates from https://translatewiki.net. 2024-07-15 14:02:06 +02:00
translatewiki.net
4d8a7dc138
Localisation updates from https://translatewiki.net. 2024-07-11 14:02:14 +02:00
Amir E. Aharoni
5bcbaa1beb
Improve the messages about being in a different place (#5764)
Remove parentheses and comma splice,
and replace them with more straightforward grammar.
2024-07-08 23:16:26 +09:00
translatewiki.net
018a924c53
Localisation updates from https://translatewiki.net. 2024-07-08 14:02:00 +02:00
Kanahia
3779cfb6a5
Added Wikitalk Page (#5740)
* Added wikitalk page and improved bottomsheet for landscape mode

* Improved wikitalk page

* Fixed italics

* Fixed little bug

* Improved the wiki talk page

* .

* changed commons url to wikidata url

* changed commons url to wikidata url + 1

* fixed bookmark issue

* Added kdoc and javadoc

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2024-07-08 15:13:57 +09:00
Kaartic Sivaraam
36905711d0 Version v5.0.2 2024-07-07 01:05:55 +05:30
translatewiki.net
05ffd123e4
Localisation updates from https://translatewiki.net. 2024-07-04 14:01:57 +02:00
translatewiki.net
923acd802c
Localisation updates from https://translatewiki.net. 2024-07-01 14:01:39 +02:00
translatewiki.net
f8e1030164
Localisation updates from https://translatewiki.net. 2024-06-27 14:01:37 +02:00
translatewiki.net
63ec5c8433
Localisation updates from https://translatewiki.net. 2024-06-20 14:01:59 +02:00
Kaartic Sivaraam
69cd9c94d1
Make sure to clear cookies on logout (#5727)
* Ensure to clear the cookies when logging out

It turns out that we failed to clear the cookies from the cookie JAR
when logging the user out. As a consequence, the cookie were retained
and it was possible to edit depictions as the previous user even without
logging in to the app (using the retained cookies).

Make sure we properly clear the cookies when we log the user out.

As an aside, the fact that the edit button shouldn't have been shown
is a different issue being tracked in #5726

* session: reuse removeAccount method for log out

The removeAccount method takes care of invoking the non-deprecated
API in applicable API levels. The logout method did not do such a
thing. Avoid redundancy, and reuse the removeAccount method for
logging out.
2024-06-18 22:31:34 +02:00
Kaartic Sivaraam
1808699e89
Make new feedback to be added as a new section to the end of the page (#5753)
* feedback: add the feedback as a new section at end of the page

Addresses feedback on #5542. For auto-archiving of section
to work properly on our feedback page, the new sections need to
be created at the end of the page rather than at the top.

So, adjust the feedback addition logic to make it such that the
feedback is appended to the bottom of the page.

* Replace lambda with a method reference

* feedback: replace edit summary with something more relevant

The summary of the feedback page was unhelpful. Make it more helpful by
using a more helpful summary that at least mentions the version of the
app for which the feedback is posted.

* test: try to fix test case related to feedback change
2024-06-18 21:40:30 +02:00
translatewiki.net
0e39d93721
Localisation updates from https://translatewiki.net. 2024-06-17 14:02:06 +02:00
Adam English
c2ac0f659a
Add code on line 340. Replace the 'Add location' button with 'Edit location' button when user clicks yes in similar image dialog (#5756)
Co-authored-by: starrain <zxyadelaide@gmail.com>
2024-06-15 22:17:28 +02:00
translatewiki.net
68df749ad2
Localisation updates from https://translatewiki.net. 2024-06-13 14:02:35 +02:00
translatewiki.net
5cdfb85a9c
Localisation updates from https://translatewiki.net. 2024-06-11 11:28:25 +02:00
Evangelos Talos
48bd3c07b8
Add ProgressBar to MediaDetailPagerFragment for Image Loading Indicator (#5736)
* Add progress bar to fragment_media_detail_pager.xml

* Add progress bar to MediaDetailPagerFragment.java

* Add javadoc & comments

* Fix tests

---------

Co-authored-by: Giannis Karyotakis <110292528+karyotakisg@users.noreply.github.com>
2024-06-10 20:06:06 +09:00
Jason-Whitmore
3dc7180784
LocationPickerActivity.java: Fix location picker bug with incorrect map centering (#5716)
This commit moves the center of the map to the image's location, if the image has
location EXIF data. If the image does not have location EXIF data, the map will
center on the device's current GPS location.
2024-06-09 21:24:40 +02:00
Kaartic Sivaraam
bb4cfe421a
feedback: add info about where the feedback gets posted (#5748)
* feedback: add info about where the feedback gets posted

Fixes #5747

* feedback: avoid underscore in the link's alternative text

* wording

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2024-06-09 21:08:38 +02:00
Evangelos Talos
22dd69cabb
Improved Map Marker Visibility Based on App Theme (#5744)
* Update map markers

* Modify code to use different map marker for themes

* Update map markers for bookmarked

* Add 2 tests

---------

Co-authored-by: Giannis Karyotakis <110292528+karyotakisg@users.noreply.github.com>
2024-06-09 21:07:02 +02:00
Zack Chi
bde09072fb
Update Java version from Java 8 to Java 11 (#5732)
Co-authored-by: Zack Chi <zach.chi.email@gmail.com>
Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2024-06-09 20:56:38 +02:00
pacha0
8fdfb8e6dc
Fixes bug #5721 (Crash when adding and then removing media details) (#5750)
I replaced uploadMediaDetail.get(position) with a direct reference to uploadMediaDetail in 3 locations to avoid out of bounds errors.

The usage of RecyclerView in this code is still a bit buggy, and maybe a RecyclerView is not necessary here since the number of items is limited. The fix also revealed problems in the logic of adding the "addButton" only to the last item, which couldn't be reproduced before because the app crashed first. I can submit a new bug for that if this fix goes live.

I did not feel very comfortable with the code so I restricted my commit to fixing the first reported crash, which was resolved in my tests.
2024-06-06 15:10:26 +02:00
translatewiki.net
06e25a074c Localisation updates from https://translatewiki.net. 2024-06-03 14:03:12 +02:00
Giannis Karyotakis
fe7a2f2a8c
Fix voice input bug (#5739)
* Fix voice input bug

* Add javadocs

---------

Co-authored-by: vtalos <v.talos23@gmail.com>
2024-06-02 23:06:43 +05:30
translatewiki.net
18e03ed038 Localisation updates from https://translatewiki.net. 2024-05-30 14:02:21 +02:00
pacha0
ff21e73928
Updated skip_login_title string to sound more natural (#5746) 2024-05-30 09:49:03 +09:00
translatewiki.net
cd045a2a2a Localisation updates from https://translatewiki.net. 2024-05-27 14:01:46 +02:00
translatewiki.net
9a2a56c1cf Localisation updates from https://translatewiki.net. 2024-05-23 14:03:04 +02:00
translatewiki.net
041c293808 Localisation updates from https://translatewiki.net. 2024-05-20 14:01:58 +02:00
translatewiki.net
4545059035 Localisation updates from https://translatewiki.net. 2024-05-16 14:03:00 +02:00
translatewiki.net
7e84a447d4 Localisation updates from https://translatewiki.net. 2024-05-09 14:02:10 +02:00
translatewiki.net
c67cf4b07e Localisation updates from https://translatewiki.net. 2024-05-06 14:02:07 +02:00
Kanahia
6b93c34f9e
Changed Unknown to Wikidata Description (#5697)
* Delete app/src/main/res/values-yue-hant directory

* Changed Unknown to description
2024-05-03 07:23:19 +09:00
translatewiki.net
d6ac307f63 Localisation updates from https://translatewiki.net. 2024-05-02 14:02:38 +02:00
Evangelos Talos
c178c5de41
Enhancing Multi-Upload Functionality for Consistent Depiction Categorization (#5700)
* Add AlertDialog for categories and modularize receiveSharedItems

* Improve nearby-place search function for a multi-upload

Enhance the depiction consistency of a multi-upload by ensuring that it corresponds to a single place

* Add javadoc

* Update strings.xml

* Renamed setImageTobeUploaded to setImageToBeUploaded

* Make uploadIsOnPlace private & add a setter

* Rename uploadIsOnPlace to uploadIsOfAPlace

* Use singular when there is only one picture

* Add a 'Do not show again' checkbox on the dialog

* Update strings.xml

---------

Co-authored-by: Giannis Karyotakis <110292528+karyotakisg@users.noreply.github.com>
Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
2024-05-02 20:12:32 +09:00
translatewiki.net
6aa9303d0f Localisation updates from https://translatewiki.net. 2024-04-29 14:02:13 +02:00
Kaartic Sivaraam
af028cbddc Version v5.0.1 (fix release variant of v5.0.0)
The same as v5.0.0 except this fixes the release variants of the
app for v5.0.0.
2024-04-28 16:58:49 +05:30
Kaartic Sivaraam
da0b2c28e3 app: adjust R8 rules to ensure our model classes are not obfuscated
With refactoring of the dataclient module, the model classes now live
within the app's source code itself. So, the existing R8 rules became
obsolete and resulted in the prodRelease version of the app not
working.

So, adjust the R8 rules so that R8 doesn't obsfuscate the model classes
that now live within the app.
2024-04-28 16:21:14 +05:30
Kaartic Sivaraam
4cc5224556 CHANGELOG: add link to the v5.0.0 GitHub release 2024-04-28 15:07:13 +05:30
Kaartic Sivaraam
c692766c41 CHANGELOG: fix diff link to appropriately use the v5.0.0 tag 2024-04-28 15:05:54 +05:30
Kaartic Sivaraam
12ac709aba Rebrand v4.3.0 as v5.0.0
We have a huge quantum of changes since v4.2.1. So, it makes sense
to bump the major release. So, rebrand v4.3.0 as v5.0.0.
2024-04-28 14:46:47 +05:30
1311 changed files with 77570 additions and 61976 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

@ -12,17 +12,17 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Set up JDK
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Cache packages
id: cache-packages
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
@ -37,7 +37,7 @@ jobs:
- name: AVD cache
if: github.event_name != 'pull_request'
uses: actions/cache@v3
uses: actions/cache@v4
id: avd-cache
with:
path: |
@ -89,7 +89,7 @@ jobs:
run: bash ./gradlew assembleBetaDebug --stacktrace
- name: Upload betaDebug APK
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: betaDebugAPK
path: app/build/outputs/apk/beta/debug/app-*.apk
@ -98,7 +98,17 @@ jobs:
run: bash ./gradlew assembleProdDebug --stacktrace
- name: Upload prodDebug APK
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: prodDebugAPK
path: app/build/outputs/apk/prod/debug/app-*.apk
- name: Create and PR number artifact
run: |
echo "{\"pr_number\": ${{ github.event.pull_request.number || 'null' }}}" > pr_number.json
- name: Upload PR number artifact
uses: actions/upload-artifact@v4
with:
name: pr_number
path: ./pr_number.json

41
.github/workflows/build-beta.yml vendored Normal file
View file

@ -0,0 +1,41 @@
name: Build beta only
on: [workflow_dispatch]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: gradle
- name: Access test login credentials
run: |
echo "TEST_USER_NAME=${{ secrets.TEST_USER_NAME }}" >> local.properties
echo "TEST_USER_PASSWORD=${{ secrets.TEST_USER_PASSWORD }}" >> local.properties
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Set env
run: echo "COMMIT_SHA=$(git log -n 1 --format='%h')" >> $GITHUB_ENV
- name: Generate betaDebug APK
run: ./gradlew assembleBetaDebug --stacktrace
- name: Rename betaDebug APK
run: mv app/build/outputs/apk/beta/debug/app-*.apk app/build/outputs/apk/beta/debug/apps-android-commons-betaDebug-$COMMIT_SHA.apk
- name: Upload betaDebug APK
uses: actions/upload-artifact@v4
with:
name: apps-android-commons-betaDebugAPK-${{ env.COMMIT_SHA }}
path: app/build/outputs/apk/beta/debug/*.apk
retention-days: 30

View file

@ -0,0 +1,96 @@
name: Comment Artifacts on PR
on:
workflow_run:
workflows: [ "Android CI" ]
types: [ completed ]
permissions:
pull-requests: write
contents: read
concurrency:
group: comment-${{ github.event.workflow_run.id }}
cancel-in-progress: true
jobs:
comment:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request' }}
steps:
- name: Download and process artifacts
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const fs = require('fs');
const runId = context.payload.workflow_run.id;
const allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: runId,
});
const prNumberArtifact = allArtifacts.data.artifacts.find(artifact => artifact.name === "pr_number");
if (!prNumberArtifact) {
console.log("pr_number artifact not found.");
return;
}
const download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: prNumberArtifact.id,
archive_format: 'zip',
});
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/pr_number.zip`, Buffer.from(download.data));
const { execSync } = require('child_process');
execSync('unzip -q pr_number.zip -d ./pr_number/');
fs.unlinkSync('pr_number.zip');
const prData = JSON.parse(fs.readFileSync('./pr_number/pr_number.json', 'utf8'));
const prNumber = prData.pr_number;
if (!prNumber || prNumber === 'null') {
console.log("No valid PR number found in pr_number.json. Skipping.");
return;
}
const artifactsToLink = allArtifacts.data.artifacts.filter(artifact => artifact.name !== "pr_number");
if (artifactsToLink.length === 0) {
console.log("No artifacts to link found.");
return;
}
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: Number(prNumber),
});
const oldComments = comments.data.filter(comment =>
comment.body.startsWith("✅ Generated APK variants!")
);
for (const comment of oldComments) {
await github.rest.issues.deleteComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: comment.id,
});
console.log(`Deleted old comment ID: ${comment.id}`);
};
const commentBody = `✅ Generated APK variants!\n` +
artifactsToLink.map(artifact => {
const artifactUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}/artifacts/${artifact.id}`;
return `- 🤖 [Download ${artifact.name}](${artifactUrl})`;
}).join('\n');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: Number(prNumber),
body: commentBody
});

1
.gitignore vendored
View file

@ -47,3 +47,4 @@ captures/*
# Test and other output
app/jacoco.exec
app/CommonsContributions
app/.*

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,16 +1,36 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="AndroidLintNewerVersionAvailable" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AutoCloseableResource" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ClassWithOnlyPrivateConstructors" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="ConfusingElse" enabled="true" level="WARNING" enabled_by_default="true">
<option name="reportWhenNoStatementFollow" value="true" />
</inspection_tool>
<inspection_tool class="ControlFlowStatementWithoutBraces" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="DefaultNotLastCaseInSwitch" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ExplicitThis" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="FieldMayBeFinal" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="LocalCanBeFinal" enabled="true" level="WARNING" enabled_by_default="true">
<option name="REPORT_VARIABLES" value="true" />
<option name="REPORT_PARAMETERS" value="true" />
@ -24,14 +44,33 @@
<inspection_tool class="OverlyStrongTypeCast" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoreInMatchingInstanceof" value="false" />
</inspection_tool>
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="ProblematicWhitespace" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ProtectedMemberInFinalClass" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="RedundantFieldInitialization" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="RedundantImplements" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoreSerializable" value="false" />
<option name="ignoreCloneable" value="false" />
</inspection_tool>
<inspection_tool class="RedundantMethodOverride" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="SimplifiableEqualsExpression" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="TypeParameterExtendsFinalClass" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UnnecessarilyQualifiedStaticUsage" enabled="true" level="WARNING" enabled_by_default="true">
@ -47,6 +86,5 @@
<inspection_tool class="UnnecessaryQualifierForThis" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UnnecessarySuperConstructor" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UnnecessaryThis" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UnnecessaryToStringCall" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

View file

@ -1,5 +1,279 @@
# Wikimedia Commons for Android
## v6.0.2
### What's changed
* Addressed a bug that prevented the keyboard from appearing in various text fields, such as on the upload wizard
* Links in the "File usages" list are now clickable and will take you to the correct page.
* Titles for file usages are now clearer and easier to understand
* Bug fixes and stability improvements
## v6.0.1
### What's changed
* The app now supports Android 15 with an improved user interface
* Enhanced Nearby with robust and more reliable labels
* Bug fixes and stability improvements
## v5.6.1
### What's changed
* The app no longer uploads images to Wikidata if one exists already for a given item
* File usage displays correctly now
* No more infinite circular progress bar on nominating an image for deletion
* Enhanced location updates while using GPS
* Author/uploader names are now available in Media Details for Commons licensing compliance
* Improved usage of popups in Nearby
* Bug fixes and stability improvements
## v5.5.0
### What's changed
* Explore images will now be shown based on the map location and not at your current location
* Enhanced Wikidata feedback message
* Green labels in Explore map will no longer be hidden by other pins thumbnails
* Upload wizard's language drop-down now reflects the language used in the pin label
* Users can now pick only one image at a time while using the custom selector
* Bug fixes and stability improvements
## v5.4.1
### What's changed
* Custom picker now detects images that are already available on Commons
* Improve credit line in image list
* Show place cards with loaded names only in the Nearby list
* Fix the error that occurs while loading images in Explore
## v5.3.0
### What's changed
* Enable EmailAuth support
* Explore map images no longer show "Unknown"
* Fix crash when removing last two images of multiupload
* Mark ❌ for closed locations (P3999) in Nearby
* Fix two pin labels staying visible at the same time in Explore map
* Refactoring and minor UI improvements
## v5.2.0
v5.2.0 boasts several new functionalities like:
* A new refresh button lets you quickly reload the Nearby map
* Bookmarks now support categories
* Improved feedback and consistency in the user interface
* Bug fixes and performance improvements
### What's changed
* Implement "Refresh" button to clear the cache and reload the Nearby map.
* `CommonsApplication` migrate to kotlin & some lint fixes.
* Revert back to MainScope for database and UI updates and make database operations thread safe.
* Hide edit options for logged-out users in Explore screen.
* Introduced a button to delete the current folder in custom selector.
* Improve Unique File Name Search.
* Migration of several modules from Java to Kotlin.
* Fix modification on bottom sheet's data when coming from Nearby Banner and clicked on other pins.
* Bug fixes and enhancement of Achievements screen.
* Show where file is being used on Commons and other wikis.
* Migrate android.media.ExifInterface to androidx.exifinterface.media.ExifInterface as android.media.ExifInterface had security flaws on older devices.
* Make dialogs modal and always show the upload icon.
* Fix unintentional deletion of subfolders and non-images by custom selector.
* Bookmark categories.
* Add pull down to refresh in the Contributions screen.
* Fix race condition and lag when loading pin details, faster overlay management.
* Show cached pins in Nearby even when internet is unavailable
Full changelog with the list of contributors: [`v5.1.2...v5.2.0`](https://github.com/commons-app/apps-android-commons/compare/v5.1.2...v5.2.0).
## v5.1.2
### What's changed
* Fix the broken category search in the explore screen
## v5.1.1
### What's changed
* Use Android's new EXIF interface to mitigate security issues in old
EXIF interface.
* Make the icon that helps view the upload queue always visible as it ensures
that the queue accessible at all times.
## v5.1.0
### What's Changed
* Enhanced **upload queue management** in the Commons app for smoother, sequential
processing, clearer progress tracking, prevention of stuck or duplicate
uploads. As part of this improvement, the "Limited Connection mode" has been
removed.
* Added an option in "Nearby" feature enabling users to **provide feedback on
Wikidata items**. Users can report if an item doesnt exist, is at a different
location, or has other issues, with submissions tagged for easy tracking and
updates.
* Improved the "Nearby" feature by splitting the query into two parts for faster
loading and **better performance, especially in areas with dense amount of
places**. This update also resolves issues with pins overlapping place names.
* Upgraded AGP and **target/compile SDK to 34** and make necessary adjustments to
the app such as adding **"Partial Access" support**. Also includes some minor
refactoring, and replacement of deprecated circular progress bars.
* Fixed an **UI issue where the 'Subcategories' and 'Parent Categories' tabs
appeared blank** in the Category Details screen. Resolved by optimizing view
binding handling in the parent fragments.
* Fixed an issue where editing depictions removed all other structured data from
images. Now, **only depictions are updated, preserving other associated data**.
* Fixed **map centering** in the image upload flow to **use GPS EXIF tag location**
from pictures and ensured "Show in map app" accurately reflects this location.
* Fixed navigation **after uploading via Nearby by directing users to the Uploads
activity** instead of returning to Nearby, preventing confusion about needing to
upload again.
### Bug fixes and various changes
* Improved the "Nearby" feature to fetch labels based on the user's preferred
language instead of defaulting to English.
* Added a legend to the "Nearby" feature indicating pin statuses: red for items
without pictures, green for those with pictures, and grey for items being
checked. A floating action button now allows users to toggle the legend's
visibility.
* Fixed an issue where the "Nominate for deletion" option is shown to logged out
users, preventing app errors and crashes.
* Updated the regex pattern that filters categories with an year in it to also
filter the 2020s.
* Fix an issue where past depictions were not shown as suggestions, despite
being saved correctly.
* Fixed an issue in custom image picker where exiting the media preview showed
only the first image and cleared selections. Now, previously selected images
are restored correctly after exiting the preview. This was contributed.
* Fixed an issue in custom image picker where scrolling behavior did not
maintain position after exiting fullscreen preview, ensuring users remain at
the same point in their image roll unless actioned images are filtered. This
was contributed.
* Fixed Nearby map not showing new pins on map move by removing the 2000m scroll
threshold and adding an 800ms debounce for smoother pin updates when the map
is moved. Queued searches are now canceled on fragment destruction.
* Revised author information retrieval to emphasize the custom author name from
the metadata instead of the default registered username.
* Enhanced notification classification to properly identify "email" type
notifications and prompting users to check their e-mail inbox when such
notifications are clicked.
* Resolved a bug in the language chooser that incorrectly greyed-out previously
selected languages, ensuring only the current language is non-selectable during
image upload.
* Resolved pin color update issue in "Nearby" feature where the pin colour
failed to be updated after a successful image upload.
What's listed here is only a subset of all the changes. Check the full-list of
the changes in [this link](https://github.com/commons-app/apps-android-commons/compare/v5.0.2...v5.1.0).
Alternatively, checkout [this release on GitHub releases page](https://github.com/commons-app/apps-android-commons/releases/tag/v5.1.0)
for an exhaustive list of changes and the various contributors who contributed the same.
## v5.0.2
- Enhanced multi-upload functionality with user prompts to clarify that all images would share the
same category and depictions.
- Show Wikidata description on currently active Nearby pin to provide more useful information.
- Improve the visibility of map markers by dynamically adjusting their colors based on the app's
theme. The map markers will now appear lighter when the app is in dark mode and darker when the
app is in light mode. This change aims to enhance marker visibility and improve the overall user
experience.
- Added information on where user feedback is posted, helping users track existing feedback and
monitor their own submissions.
- Enhanced the edit location screen of the upload screen by centering the map on the picture's
location from metadata when editing, or on the device's GPS location if metadata is unavailable,
improving accuracy and user experience.
- Ensured the 'Add Location' button is renamed to 'Edit Location' when copying the location of a
recently uploaded image, enhancing clarity and user experience.
- Added a ProgressBar to the media detail screen to indicate image loading status, enhancing user
experience by showing loading progress until the image is fully loaded.
- Fixed an issue where caption and description fields would intermittently disappear when using
voice input, ensuring text remains visible and stable across all entries.
- Fixed a crash that occurred when attempting to remove multiple instances of caption/description
fields after initially adding them.
- Improve the text in the prompt shown when skipping login to sound more natural.
- Modified feedback addition logic to append new sections at the bottom of the page, ensuring
auto-archiving of sections functions correctly on the feedback page.
- Resolved issue where the app failed to clear cookies upon logout.
## v5.0.1
Same as v5.0.0 except this fixes some R8 rules to ensure that the release
variants of the app work as intended.
## v5.0.0
### What's Changed
- Redesigned the map feature to **replace Mapbox with the osmdroid library**.
Key elements like pin visualization and user-centered display are still
included in this redesign. This is done to guard against possible misuse of
the Mapbox token and, more crucially, to keep the app from becoming dependent
on a service that charges for usage but offers a free tier.
With this change, the app retrieves the map tiles from [Wikimedia maps](https://maps.wikimedia.org).
- Add the ability to **export locations of nearby missing pictures in GPX and
KML formats**. This allows users to browse the locations with desired radius
for offline use in their favourite map apps like OsmAnd or Maps.me, enhancing
accessibility and offline functionality.
- **Limited the uploads via the custom image picker** to a maximum of 20.
- Added two menu choices for **transparent image backgrounds**, giving users the
option of either a black or white background, increasing adaptability to
various theme settings.
User customization option has been provided with the
ability to save background color selections permanently on a per image basis.
- Implemented functionality to **automatically resume uploads** that become
stuck due to app termination or device reboot.
- Added a **compass arrow in the Nearby banner** shown in the "Contributions"
screen to guide users towards the nearest item, thus providing the missing
directional cues. The arrow dynamically adjusts based on device rotation,
aligning with the calculated bearing towards the target location. Further,
the distance and direction are updated as the user moves.
- Implemented **voice input feature** for caption and description fields,
enabling users to dictate text directly into these fields.
- Improved various flows in the app to **redirect users to the login page** and
display a persistent message **if their session becomes invalid** due to a
password change, enhancing user guidance and security measures.
### Revamps and refactorings
- **Revamped initial upload screen layout and the description edit screen layout**
for enhanced user experience and ensuring better symmetry in the design.
- **Replaced Butterknife with ViewBinding** in various places of the app.
- Transferred essential code from **the redundant data-client module** to the
main Commons app code, enabling its integration and facilitating the removal
of the redundant module. Further, convert various parts of the code to Kotlin.
- **Revamped the various location permission flows** to ensure consistency for
the sake of a better user experience.
### Bug fixes and various changes
- Resolved an issue where paused uploads that were subsequently cancelled were
still being uploaded.
- Fixed an issue where some user information such as upload count were not
displayed in the "Contributions" and "Profile" screens.
- Fixed the long-standing broken *"Picture of the Day" widget* to restore its
usability.
- Resolved an issue where some categories were hidden at the top of Upload
Wizard suggestions.
- Resolved an issue where there was a grey empty screen at Upload wizard when
the app was denied the files permission.
- Implemented logic to bypass media in Peer Review if the current reviewer is
also the user who uploaded the media.
- Corrected arrow image behaviour in the first upload screen: now displays down
arrow when details card is fully visible, aligning with expected user
interaction.
- Updated app icon to improve visibility and recognition on F-Droid.
- Fixed issue causing all pictures to disappear and activity to reload fully in
the custom image selector after marking a picture as 'not for upload', now
ensuring only the selected picture is removed as expected.
What's listed here is only a subset of all the changes. Check the full-list of
the changes in [this link](https://github.com/commons-app/apps-android-commons/compare/v4.2.1...v5.0.0).
Alternatively, checkout [this release on GitHub releases page](https://github.com/commons-app/apps-android-commons/releases/tag/v5.0.0)
for an exhaustive list of changes and the various contributors who contributed the same.
## v4.2.1
- Provide the ability to edit an image to losslessly rotate it while uploading

View file

@ -1,6 +1,6 @@
# Wikimedia Commons Android app
![GitHub issue custom search](https://img.shields.io/github/issues-search?label=%22good%20first%20issue%22%20issues&query=repo%3Acommons-app%2Fapps-android-commons%20is%3Aissue%20is%3Aopen%20label%3A%22good%20first%20issue%22)
[![Build status](https://github.com/commons-app/apps-android-commons/actions/workflows/android.yml/badge.svg?branch=master)](https://github.com/commons-app/apps-android-commons/actions?query=branch%3Amaster)
[![Build status](https://github.com/commons-app/apps-android-commons/actions/workflows/android.yml/badge.svg?branch=main)](https://github.com/commons-app/apps-android-commons/actions?query=branch%3Amain)
[![Preview the app](https://img.shields.io/badge/Preview-Appetize.io-orange.svg)](https://appetize.io/app/8ywtpe9f8tb8h6bey11c92vkcw)
[![codecov](https://codecov.io/gh/commons-app/apps-android-commons/branch/master/graph/badge.svg)](https://codecov.io/gh/commons-app/apps-android-commons)
@ -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).
@ -45,7 +46,7 @@ This software is open source, licensed under the [Apache License 2.0][10].
[1]: https://play.google.com/store/apps/details?id=fr.free.nrw.commons
[2]: https://commons-app.github.io/
[3]: https://github.com/commons-app/apps-android-commons/issues
[3]: https://github.com/commons-app/apps-android-commons/issues?q=is%3Aopen+is%3Aissue+no%3Aassignee+-label%3Adebated+label%3Abug+-label%3A%22low+priority%22+-label%3Aupstream
[4]: https://github.com/commons-app/commons-app-documentation/blob/master/android/README.md#-android-documentation
[5]: https://github.com/commons-app/commons-app-documentation/blob/master/android/README.md#-user-documentation

View file

@ -1,398 +0,0 @@
plugins {
id 'com.github.triplet.play' version '2.7.2' apply false
}
apply from: '../gitutils.gradle'
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-parcelize'
apply from: "$rootDir/jacoco.gradle"
def isRunningOnTravisAndIsNotPRBuild = System.getenv("CI") == "true" && file('../play.p12').exists()
if (isRunningOnTravisAndIsNotPRBuild) {
apply plugin: 'com.github.triplet.play'
}
dependencies {
// Utils
implementation 'in.yuvi:http.fluent:1.3'
implementation 'com.google.code.gson:gson:2.8.5'
implementation ("com.squareup.okhttp3:okhttp:$OKHTTP_VERSION!!"){
// Forcing dependency versions using force = true on a first-level dependency has been deprecated.
// Ref: https://docs.gradle.org/7.5/userguide/upgrading_version_5.html#forced_dependencies
//force = true //API 19 support
}
implementation 'com.squareup.retrofit2:retrofit:2.8.1'
implementation "com.squareup.retrofit2:converter-gson:2.8.1"
implementation "com.squareup.retrofit2:adapter-rxjava2:2.8.1"
implementation 'com.squareup.okio:okio:2.2.2'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
implementation 'io.reactivex.rxjava2:rxjava:2.2.3'
implementation 'com.jakewharton.rxbinding2:rxbinding:2.1.1'
implementation 'com.jakewharton.rxbinding3:rxbinding-appcompat:3.0.0'
implementation 'com.jakewharton.rxbinding2:rxbinding-support-v4:2.1.1'
implementation 'com.jakewharton.rxbinding2:rxbinding-appcompat-v7:2.1.1'
implementation 'com.jakewharton.rxbinding2:rxbinding-design:2.1.1'
implementation 'com.facebook.fresco:fresco:1.13.0'
implementation 'org.apache.commons:commons-lang3:3.8.1'
// UI
implementation 'fr.avianey.com.viewpagerindicator:library:2.4.1.1@aar'
implementation 'com.github.chrisbanes:PhotoView:2.0.0'
implementation 'com.github.pedrovgs:renderers:3.3.3'
implementation "org.maplibre.gl:android-sdk:$MAPLIBRE_VERSION"
implementation 'org.maplibre.gl:android-plugin-scalebar-v9:1.0.0'
implementation 'com.jakewharton.timber:timber:4.7.1'
implementation 'com.github.deano2390:MaterialShowcaseView:1.2.0'
implementation 'com.dinuscxj:circleprogressbar:1.1.1'
implementation 'com.karumi:dexter:5.0.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation "com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:$ADAPTER_DELEGATES_VERSION"
implementation "com.hannesdorfmann:adapterdelegates4-pagination:$ADAPTER_DELEGATES_VERSION"
implementation "androidx.paging:paging-runtime-ktx:$PAGING_VERSION"
testImplementation "androidx.paging:paging-common-ktx:$PAGING_VERSION"
implementation "androidx.paging:paging-rxjava2-ktx:$PAGING_VERSION"
implementation "androidx.recyclerview:recyclerview:1.2.0-alpha02"
implementation "com.squareup.okhttp3:okhttp-ws:$OKHTTP_VERSION"
// Logging
implementation 'ch.acra:acra-dialog:5.8.4'
implementation 'ch.acra:acra-mail:5.8.4'
implementation 'org.slf4j:slf4j-api:1.7.25'
api('com.github.tony19:logback-android-classic:1.1.1-6') {
exclude group: 'com.google.android', module: 'android'
}
implementation "com.squareup.okhttp3:logging-interceptor:$OKHTTP_VERSION"
// Dependency injector
implementation "com.google.dagger:dagger-android:$DAGGER_VERSION"
implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION"
kapt "com.google.dagger:dagger-android-processor:$DAGGER_VERSION"
kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
annotationProcessor "com.google.dagger:dagger-android-processor:$DAGGER_VERSION"
implementation "org.jetbrains.kotlin:kotlin-reflect:$KOTLIN_VERSION"
//Mocking
testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0'
testImplementation 'org.mockito:mockito-inline:5.2.0'
testImplementation 'org.mockito:mockito-core:5.6.0'
testImplementation "org.powermock:powermock-module-junit4:2.0.9"
testImplementation "org.powermock:powermock-api-mockito2:2.0.9"
// Unit testing
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.robolectric:robolectric:4.11.1'
testImplementation 'androidx.test:core:1.5.0'
testImplementation "androidx.test:runner:1.5.2"
testImplementation 'androidx.test.ext:junit:1.1.5'
testImplementation "androidx.test:rules:1.5.0"
testImplementation "com.squareup.okhttp3:mockwebserver:$OKHTTP_VERSION"
testImplementation "com.jraska.livedata:testing-ktx:1.1.2"
testImplementation "androidx.arch.core:core-testing:2.2.0"
testImplementation "org.junit.jupiter:junit-jupiter-api:5.10.0"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.10.0"
testImplementation 'com.facebook.soloader:soloader:0.10.5'
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3"
debugImplementation("androidx.fragment:fragment-testing:1.6.2")
testImplementation "commons-io:commons-io:2.6"
// Android testing
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0-alpha04'
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.4.0'
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.5.0-alpha04'
androidTestImplementation 'androidx.test:runner:1.4.0'
androidTestImplementation 'androidx.test:rules:1.4.1-alpha04'
androidTestImplementation 'androidx.test:core:1.4.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.annotation:annotation:1.3.0'
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.8.0'
androidTestImplementation "androidx.test.uiautomator:uiautomator:2.2.0"
androidTestUtil 'androidx.test:orchestrator:1.4.1'
// Debugging
debugImplementation "com.squareup.leakcanary:leakcanary-android:$LEAK_CANARY_VERSION"
// Support libraries
implementation "com.google.android.material:material:1.1.0-alpha04"
implementation "androidx.browser:browser:1.3.0"
implementation "androidx.cardview:cardview:1.0.0"
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation "androidx.exifinterface:exifinterface:1.3.2"
implementation "androidx.core:core-ktx:$CORE_KTX_VERSION"
implementation 'com.simplecityapps:recyclerview-fastscroll:2.0.1'
//swipe_layout
implementation 'com.daimajia.swipelayout:library:1.2.0@aar'
//Room
implementation "androidx.room:room-runtime:$ROOM_VERSION"
implementation "androidx.room:room-ktx:$ROOM_VERSION"
implementation "androidx.room:room-rxjava2:$ROOM_VERSION"
kapt "androidx.room:room-compiler:$ROOM_VERSION"
// For Kotlin use kapt instead of annotationProcessor
testImplementation "androidx.arch.core:core-testing:2.1.0"
// Pref
// Java language implementation
implementation "androidx.preference:preference:$PREFERENCE_VERSION"
// Kotlin
implementation "androidx.preference:preference-ktx:$PREFERENCE_VERSION"
//Android Media
implementation 'com.github.juanitobananas:AndroidMediaUtil:v1.0-1'
implementation "androidx.multidex:multidex:$MULTIDEX_VERSION"
def work_version = "2.8.1"
// Kotlin + coroutines
implementation "androidx.work:work-runtime-ktx:$work_version"
implementation("androidx.work:work-runtime:$work_version")
testImplementation "androidx.work:work-testing:$work_version"
//Glide
implementation 'com.github.bumptech.glide:glide:4.12.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
kaptTest "androidx.databinding:databinding-compiler:8.0.2"
kaptAndroidTest "androidx.databinding:databinding-compiler:8.0.2"
implementation("io.github.coordinates2country:coordinates2country-android:1.3") { exclude group: 'com.google.android', module: 'android' }
//OSMDroid
implementation ("org.osmdroid:osmdroid-android:$OSMDROID_VERSION")
constraints {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.0") {
because("kotlin-stdlib-jdk7 is now a part of kotlin-stdlib")
}
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.0") {
because("kotlin-stdlib-jdk8 is now a part of kotlin-stdlib")
}
}
}
task disableAnimations(type: Exec) {
def adb = "$System.env.ANDROID_HOME/platform-tools/adb"
commandLine "$adb", 'shell', 'settings', 'put', 'global', 'window_animation_scale', '0'
commandLine "$adb", 'shell', 'settings', 'put', 'global', 'transition_animation_scale', '0'
commandLine "$adb", 'shell', 'settings', 'put', 'global', 'animator_duration_scale', '0'
}
project.gradle.taskGraph.whenReady {
connectedBetaDebugAndroidTest.dependsOn disableAnimations
connectedProdDebugAndroidTest.dependsOn disableAnimations
}
android {
compileSdkVersion 33
defaultConfig {
//applicationId 'fr.free.nrw.commons'
versionCode 1037
versionName '4.3.0'
setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName())
minSdkVersion 21
targetSdkVersion 33
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments clearPackageData: 'true'
multiDexEnabled true
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
}
packagingOptions {
jniLibs {
excludes += ['META-INF/androidx.*']
}
resources {
excludes += ['META-INF/androidx.*', 'META-INF/proguard/androidx-annotations.pro']
}
}
testOptions {
animationsDisabled true
unitTests {
returnDefaultValues = true
includeAndroidResources = true
}
unitTests.all {
jvmArgs '-noverify'
}
}
sourceSets {
// use kotlin only in tests (for now)
test.java.srcDirs += 'src/test/kotlin'
// use main assets and resources in test
test.assets.srcDirs += 'src/main/assets'
test.resources.srcDirs += 'src/main/resoures'
}
signingConfigs {
release
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
testProguardFile 'test-proguard-rules.txt'
signingConfig signingConfigs.debug
if (isRunningOnTravisAndIsNotPRBuild) {
signingConfig signingConfigs.release
}
}
debug {
testCoverageEnabled true
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
testProguardFile 'test-proguard-rules.txt'
versionNameSuffix "-debug-" + getBranchName()
}
}
if (isRunningOnTravisAndIsNotPRBuild) {
// configure keystore based on env vars in Travis for automated alpha builds
signingConfigs.release.storeFile = file("../nr-commons.keystore")
signingConfigs.release.storePassword = System.getenv("keystore_password")
signingConfigs.release.keyAlias = System.getenv("key_alias")
signingConfigs.release.keyPassword = System.getenv("key_password")
}
configurations.all {
resolutionStrategy.force 'androidx.annotation:annotation:1.1.0'
resolutionStrategy.force 'com.jakewharton.timber:timber:4.7.1'
resolutionStrategy.force 'androidx.fragment:fragment:1.3.6'
exclude module: 'okhttp-ws'
}
flavorDimensions 'tier'
productFlavors {
prod {
applicationId 'fr.free.nrw.commons'
buildConfigField "String", "WIKIMEDIA_API_POTD", "\"https://commons.wikimedia.org/w/api.php?action=featuredfeed&feed=potd&feedformat=rss&language=en\""
buildConfigField "String", "WIKIMEDIA_API_HOST", "\"https://commons.wikimedia.org/w/api.php\""
buildConfigField "String", "WIKIDATA_API_HOST", "\"https://www.wikidata.org/w/api.php\""
buildConfigField "String", "WIKIDATA_URL", "\"https://www.wikidata.org\""
buildConfigField "String", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\""
buildConfigField "String", "WIKIMEDIA_CAMPAIGNS_URL", "\"https://raw.githubusercontent.com/commons-app/campaigns/master/campaigns.json\""
buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.wikimedia.org/wikipedia/commons\""
buildConfigField "String", "HOME_URL", "\"https://commons.wikimedia.org/wiki/\""
buildConfigField "String", "COMMONS_URL", "\"https://commons.wikimedia.org\""
buildConfigField "String", "WIKIDATA_URL", "\"https://www.wikidata.org\""
buildConfigField "String", "MOBILE_HOME_URL", "\"https://commons.m.wikimedia.org/wiki/\""
buildConfigField "String", "SIGNUP_LANDING_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Special:CreateAccount&returnto=Main+Page&returntoquery=welcome%3Dyes\""
buildConfigField "String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Main_Page&welcome=yes\""
buildConfigField "String", "FORGOT_PASSWORD_URL", "\"https://commons.wikimedia.org/wiki/Special:PasswordReset\""
buildConfigField "String", "PRIVACY_POLICY_URL", "\"https://github.com/commons-app/commons-app-documentation/blob/master/android/Privacy-policy.md\""
buildConfigField "String", "ACCOUNT_TYPE", "\"fr.free.nrw.commons\""
buildConfigField "String", "CONTRIBUTION_AUTHORITY", "\"fr.free.nrw.commons.contributions.contentprovider\""
buildConfigField "String", "MODIFICATION_AUTHORITY", "\"fr.free.nrw.commons.modifications.contentprovider\""
buildConfigField "String", "CATEGORY_AUTHORITY", "\"fr.free.nrw.commons.categories.contentprovider\""
buildConfigField "String", "RECENT_SEARCH_AUTHORITY", "\"fr.free.nrw.commons.explore.recentsearches.contentprovider\""
buildConfigField "String", "RECENT_LANGUAGE_AUTHORITY", "\"fr.free.nrw.commons.recentlanguages.contentprovider\""
buildConfigField "String", "BOOKMARK_AUTHORITY", "\"fr.free.nrw.commons.bookmarks.contentprovider\""
buildConfigField "String", "BOOKMARK_LOCATIONS_AUTHORITY", "\"fr.free.nrw.commons.bookmarks.locations.contentprovider\""
buildConfigField "String", "BOOKMARK_ITEMS_AUTHORITY", "\"fr.free.nrw.commons.bookmarks.items.contentprovider\""
buildConfigField "String", "COMMIT_SHA", "\"" + getBuildVersion().toString() + "\""
buildConfigField "String", "TEST_USERNAME", "\"" + getTestUserName() + "\""
buildConfigField "String", "TEST_PASSWORD", "\"" + getTestPassword() + "\""
buildConfigField "String", "DEPICTS_PROPERTY", "\"P180\""
dimension 'tier'
}
beta {
applicationId 'fr.free.nrw.commons.beta'
// What values do we need to hit the BETA versions of the site / api ?
buildConfigField "String", "WIKIMEDIA_API_POTD", "\"https://commons.wikimedia.org/w/api.php?action=featuredfeed&feed=potd&feedformat=rss&language=en\""
buildConfigField "String", "WIKIMEDIA_API_HOST", "\"https://commons.wikimedia.beta.wmflabs.org/w/api.php\""
buildConfigField "String", "WIKIDATA_API_HOST", "\"https://www.wikidata.org/w/api.php\""
buildConfigField "String", "WIKIDATA_URL", "\"https://www.wikidata.org\""
buildConfigField "String", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\""
buildConfigField "String", "WIKIMEDIA_CAMPAIGNS_URL", "\"https://raw.githubusercontent.com/commons-app/campaigns/master/campaigns_beta_active.json\""
buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.beta.wmflabs.org/wikipedia/commons\""
buildConfigField "String", "HOME_URL", "\"https://commons.wikimedia.beta.wmflabs.org/wiki/\""
buildConfigField "String", "COMMONS_URL", "\"https://commons.wikimedia.beta.wmflabs.org\""
buildConfigField "String", "WIKIDATA_URL", "\"https://www.wikidata.org\""
buildConfigField "String", "MOBILE_HOME_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/wiki/\""
buildConfigField "String", "SIGNUP_LANDING_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Special:CreateAccount&returnto=Main+Page&returntoquery=welcome%3Dyes\""
buildConfigField "String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Main_Page&welcome=yes\""
buildConfigField "String", "FORGOT_PASSWORD_URL", "\"https://commons.wikimedia.beta.wmflabs.org/wiki/Special:PasswordReset\""
buildConfigField "String", "PRIVACY_POLICY_URL", "\"https://github.com/commons-app/commons-app-documentation/blob/master/android/Privacy-policy.md\""
buildConfigField "String", "ACCOUNT_TYPE", "\"fr.free.nrw.commons.beta\""
buildConfigField "String", "CONTRIBUTION_AUTHORITY", "\"fr.free.nrw.commons.beta.contributions.contentprovider\""
buildConfigField "String", "MODIFICATION_AUTHORITY", "\"fr.free.nrw.commons.beta.modifications.contentprovider\""
buildConfigField "String", "CATEGORY_AUTHORITY", "\"fr.free.nrw.commons.beta.categories.contentprovider\""
buildConfigField "String", "RECENT_SEARCH_AUTHORITY", "\"fr.free.nrw.commons.beta.explore.recentsearches.contentprovider\""
buildConfigField "String", "RECENT_LANGUAGE_AUTHORITY", "\"fr.free.nrw.commons.beta.recentlanguages.contentprovider\""
buildConfigField "String", "BOOKMARK_AUTHORITY", "\"fr.free.nrw.commons.beta.bookmarks.contentprovider\""
buildConfigField "String", "BOOKMARK_LOCATIONS_AUTHORITY", "\"fr.free.nrw.commons.beta.bookmarks.locations.contentprovider\""
buildConfigField "String", "BOOKMARK_ITEMS_AUTHORITY", "\"fr.free.nrw.commons.beta.bookmarks.items.contentprovider\""
buildConfigField "String", "COMMIT_SHA", "\"" + getBuildVersion().toString() + "\""
buildConfigField "String", "TEST_USERNAME", "\"" + getTestUserName() + "\""
buildConfigField "String", "TEST_PASSWORD", "\"" + getTestPassword() + "\""
buildConfigField "String", "DEPICTS_PROPERTY", "\"P245962\""
dimension 'tier'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildToolsVersion buildToolsVersion
buildFeatures {
viewBinding true
}
namespace 'fr.free.nrw.commons'
lint {
abortOnError false
disable 'MissingTranslation', 'ExtraTranslation'
}
}
String getTestUserName() {
def propFile = rootProject.file("./local.properties")
def properties = new Properties()
properties.load(new FileInputStream(propFile))
return properties['TEST_USER_NAME']
}
String getTestPassword() {
def propFile = rootProject.file("./local.properties")
def properties = new Properties()
properties.load(new FileInputStream(propFile))
return properties['TEST_USER_PASSWORD']
}
if (isRunningOnTravisAndIsNotPRBuild) {
play {
track = "alpha"
userFraction = 1
serviceAccountEmail = System.getenv("SERVICE_ACCOUNT_NAME")
serviceAccountCredentials = file("../play.p12")
resolutionStrategy = "auto"
outputProcessor { // this: ApkVariantOutput
versionNameOverride = "$versionNameOverride.$versionCode"
}
}
}

447
app/build.gradle.kts Normal file
View file

@ -0,0 +1,447 @@
import java.util.Properties
import java.io.ByteArrayOutputStream
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.jetbrains.kotlin.android)
alias(libs.plugins.kotlin.kapt)
alias(libs.plugins.kotlin.parcelize)
}
apply(from = "$rootDir/jacoco.gradle")
val isRunningOnTravisAndIsNotPRBuild = System.getenv("CI") == "true" && file("../play.p12").exists()
if (isRunningOnTravisAndIsNotPRBuild) {
apply(plugin = "com.github.triplet.play")
}
android {
namespace = "fr.free.nrw.commons"
compileSdk = 35
defaultConfig {
applicationId = "fr.free.nrw.commons"
minSdk = 21
targetSdk = 35
versionCode = 1059
versionName = "6.1.0"
setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName())
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments["clearPackageData"] = "true"
multiDexEnabled = true
vectorDrawables {
useSupportLibrary = true
}
}
sourceSets {
getByName("test") {
// Use kotlin only in tests (for now)
java.srcDirs("src/test/kotlin")
// Use main assets and resources in test
assets.srcDirs("src/main/assets")
resources.srcDirs("src/main/resources")
}
}
signingConfigs {
create("release") {
// Configure keystore based on env vars in Travis for automated alpha builds
if(isRunningOnTravisAndIsNotPRBuild) {
storeFile = file("../nr-commons.keystore")
storePassword = System.getenv("keystore_password")
keyAlias = System.getenv("key_alias")
keyPassword = System.getenv("key_password")
}
}
}
buildTypes {
release {
isMinifyEnabled = true
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.txt")
testProguardFile("test-proguard-rules.txt")
signingConfig = signingConfigs.getByName("debug")
if (isRunningOnTravisAndIsNotPRBuild) {
signingConfig = signingConfigs.getByName("release")
}
}
debug {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.txt")
testProguardFile("test-proguard-rules.txt")
versionNameSuffix = "-debug-" + getBranchName()
enableUnitTestCoverage = true
enableAndroidTestCoverage = true
}
}
configurations.all {
resolutionStrategy {
force("androidx.annotation:annotation:1.1.0")
force("com.jakewharton.timber:timber:4.7.1")
force("androidx.fragment:fragment:1.3.6")
}
exclude(module = "okhttp-ws")
}
flavorDimensions += "tier"
productFlavors {
create("prod") {
dimension = "tier"
applicationId = "fr.free.nrw.commons"
buildConfigField("String", "WIKIMEDIA_API_POTD", "\"https://commons.wikimedia.org/w/api.php?action=featuredfeed&feed=potd&feedformat=rss&language=en\"")
buildConfigField("String", "WIKIMEDIA_API_HOST", "\"https://commons.wikimedia.org/w/api.php\"")
buildConfigField("String", "WIKIDATA_API_HOST", "\"https://www.wikidata.org/w/api.php\"")
buildConfigField("String", "WIKIDATA_URL", "\"https://www.wikidata.org\"")
buildConfigField("String", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\"")
buildConfigField("String", "WIKIMEDIA_CAMPAIGNS_URL", "\"https://raw.githubusercontent.com/commons-app/campaigns/master/campaigns.json\"")
buildConfigField("String", "IMAGE_URL_BASE", "\"https://upload.wikimedia.org/wikipedia/commons\"")
buildConfigField("String", "HOME_URL", "\"https://commons.wikimedia.org/wiki/\"")
buildConfigField("String", "COMMONS_URL", "\"https://commons.wikimedia.org\"")
buildConfigField("String", "WIKIDATA_URL", "\"https://www.wikidata.org\"")
buildConfigField("String", "MOBILE_HOME_URL", "\"https://commons.m.wikimedia.org/wiki/\"")
buildConfigField("String", "MOBILE_META_URL", "\"https://meta.m.wikimedia.org/wiki/\"")
buildConfigField("String", "SIGNUP_LANDING_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Special:CreateAccount&returnto=Main+Page&returntoquery=welcome%3Dyes\"")
buildConfigField("String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Main_Page&welcome=yes\"")
buildConfigField("String", "FORGOT_PASSWORD_URL", "\"https://commons.wikimedia.org/wiki/Special:PasswordReset\"")
buildConfigField("String", "PRIVACY_POLICY_URL", "\"https://github.com/commons-app/commons-app-documentation/blob/master/android/Privacy-policy.md\"")
buildConfigField("String", "FILE_USAGES_BASE_URL", "\"https://commons.wikimedia.org/w/api.php?action=query&format=json&formatversion=2\"")
buildConfigField("String", "ACCOUNT_TYPE", "\"fr.free.nrw.commons\"")
buildConfigField("String", "CONTRIBUTION_AUTHORITY", "\"fr.free.nrw.commons.contributions.contentprovider\"")
buildConfigField("String", "MODIFICATION_AUTHORITY", "\"fr.free.nrw.commons.modifications.contentprovider\"")
buildConfigField("String", "CATEGORY_AUTHORITY", "\"fr.free.nrw.commons.categories.contentprovider\"")
buildConfigField("String", "RECENT_SEARCH_AUTHORITY", "\"fr.free.nrw.commons.explore.recentsearches.contentprovider\"")
buildConfigField("String", "RECENT_LANGUAGE_AUTHORITY", "\"fr.free.nrw.commons.recentlanguages.contentprovider\"")
buildConfigField("String", "BOOKMARK_AUTHORITY", "\"fr.free.nrw.commons.bookmarks.contentprovider\"")
buildConfigField("String", "BOOKMARK_LOCATIONS_AUTHORITY", "\"fr.free.nrw.commons.bookmarks.locations.contentprovider\"")
buildConfigField("String", "BOOKMARK_ITEMS_AUTHORITY", "\"fr.free.nrw.commons.bookmarks.items.contentprovider\"")
buildConfigField("String", "COMMIT_SHA", "\"" + getBuildVersion().toString() + "\"")
buildConfigField("String", "TEST_USERNAME", "\"" + getTestUserName() + "\"")
buildConfigField("String", "TEST_PASSWORD", "\"" + getTestPassword() + "\"")
buildConfigField("String", "DEPICTS_PROPERTY", "\"P180\"")
buildConfigField("String", "CREATOR_PROPERTY", "\"P170\"")
}
create("beta") {
dimension = "tier"
applicationId = "fr.free.nrw.commons.beta"
// What values do we need to hit the BETA versions of the site / api ?
buildConfigField("String", "WIKIMEDIA_API_POTD", "\"https://commons.wikimedia.org/w/api.php?action=featuredfeed&feed=potd&feedformat=rss&language=en\"")
buildConfigField("String", "WIKIMEDIA_API_HOST", "\"https://commons.wikimedia.beta.wmflabs.org/w/api.php\"")
buildConfigField("String", "WIKIDATA_API_HOST", "\"https://www.wikidata.org/w/api.php\"")
buildConfigField("String", "WIKIDATA_URL", "\"https://www.wikidata.org\"")
buildConfigField("String", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\"")
buildConfigField("String", "WIKIMEDIA_CAMPAIGNS_URL", "\"https://raw.githubusercontent.com/commons-app/campaigns/master/campaigns_beta_active.json\"")
buildConfigField("String", "IMAGE_URL_BASE", "\"https://upload.beta.wmflabs.org/wikipedia/commons\"")
buildConfigField("String", "HOME_URL", "\"https://commons.wikimedia.beta.wmflabs.org/wiki/\"")
buildConfigField("String", "COMMONS_URL", "\"https://commons.wikimedia.beta.wmflabs.org\"")
buildConfigField("String", "WIKIDATA_URL", "\"https://www.wikidata.org\"")
buildConfigField("String", "MOBILE_HOME_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/wiki/\"")
buildConfigField("String", "MOBILE_META_URL", "\"https://meta.m.wikimedia.beta.wmflabs.org/wiki/\"")
buildConfigField("String", "SIGNUP_LANDING_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Special:CreateAccount&returnto=Main+Page&returntoquery=welcome%3Dyes\"")
buildConfigField("String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Main_Page&welcome=yes\"")
buildConfigField("String", "FORGOT_PASSWORD_URL", "\"https://commons.wikimedia.beta.wmflabs.org/wiki/Special:PasswordReset\"")
buildConfigField("String", "PRIVACY_POLICY_URL", "\"https://github.com/commons-app/commons-app-documentation/blob/master/android/Privacy-policy.md\"")
buildConfigField("String", "FILE_USAGES_BASE_URL", "\"https://commons.wikimedia.org/w/api.php?action=query&format=json&formatversion=2\"")
buildConfigField("String", "ACCOUNT_TYPE", "\"fr.free.nrw.commons.beta\"")
buildConfigField("String", "CONTRIBUTION_AUTHORITY", "\"fr.free.nrw.commons.beta.contributions.contentprovider\"")
buildConfigField("String", "MODIFICATION_AUTHORITY", "\"fr.free.nrw.commons.beta.modifications.contentprovider\"")
buildConfigField("String", "CATEGORY_AUTHORITY", "\"fr.free.nrw.commons.beta.categories.contentprovider\"")
buildConfigField("String", "RECENT_SEARCH_AUTHORITY", "\"fr.free.nrw.commons.beta.explore.recentsearches.contentprovider\"")
buildConfigField("String", "RECENT_LANGUAGE_AUTHORITY", "\"fr.free.nrw.commons.beta.recentlanguages.contentprovider\"")
buildConfigField("String", "BOOKMARK_AUTHORITY", "\"fr.free.nrw.commons.beta.bookmarks.contentprovider\"")
buildConfigField("String", "BOOKMARK_LOCATIONS_AUTHORITY", "\"fr.free.nrw.commons.beta.bookmarks.locations.contentprovider\"")
buildConfigField("String", "BOOKMARK_ITEMS_AUTHORITY", "\"fr.free.nrw.commons.beta.bookmarks.items.contentprovider\"")
buildConfigField("String", "COMMIT_SHA", "\"" + getBuildVersion().toString() + "\"")
buildConfigField("String", "TEST_USERNAME", "\"" + getTestUserName() + "\"")
buildConfigField("String", "TEST_PASSWORD", "\"" + getTestPassword() + "\"")
buildConfigField("String", "DEPICTS_PROPERTY", "\"P245962\"")
buildConfigField("String", "CREATOR_PROPERTY", "\"P253075\"")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
buildConfig = true
viewBinding = true
compose = true
}
buildToolsVersion = buildToolsVersion
composeOptions {
kotlinCompilerExtensionVersion = "1.5.8"
}
packaging {
jniLibs {
excludes += listOf("META-INF/androidx.*")
}
resources {
excludes += listOf(
"META-INF/androidx.*",
"META-INF/proguard/androidx-annotations.pro",
"/META-INF/LICENSE.md",
"/META-INF/LICENSE-notice.md"
)
}
}
testOptions {
animationsDisabled = true
unitTests {
isReturnDefaultValues = true
isIncludeAndroidResources = true
}
unitTests.all {
it.jvmArgs("-noverify")
}
}
lint {
abortOnError = false
disable += listOf("MissingTranslation", "ExtraTranslation")
}
}
dependencies {
// Utils
implementation(libs.gson)
implementation(libs.okhttp)
implementation(libs.retrofit)
implementation(libs.retrofit.converter.gson)
implementation(libs.retrofit.adapter.rxjava)
implementation(libs.rxandroid)
implementation(libs.rxjava)
implementation(libs.rxbinding)
implementation(libs.rxbinding.appcompat)
implementation(libs.facebook.fresco)
implementation(libs.facebook.fresco.middleware)
implementation(libs.apache.commons.lang3)
// UI
implementation("${libs.viewpagerindicator.library.get()}@aar")
implementation(libs.photoview)
implementation(libs.android.sdk)
implementation(libs.android.plugin.scalebar)
implementation(libs.timber)
implementation(libs.android.material)
implementation(libs.dexter)
// Jetpack Compose
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.runtime)
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.ui.viewbinding)
implementation(libs.androidx.material3)
implementation(libs.androidx.foundation)
implementation(libs.androidx.foundation.layout)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
implementation(libs.adapterdelegates4.kotlin.dsl.viewbinding)
implementation(libs.adapterdelegates4.pagination)
implementation(libs.androidx.paging.runtime.ktx)
testImplementation(libs.androidx.paging.common.ktx)
implementation(libs.androidx.paging.rxjava2.ktx)
implementation(libs.androidx.recyclerview)
// Logging
implementation(libs.acra.dialog)
implementation(libs.acra.mail)
implementation(libs.slf4j.api)
implementation(libs.logback.android.classic) {
exclude(group = "com.google.android", module = "android")
}
implementation(libs.logging.interceptor)
// Dependency injector
implementation(libs.dagger.android)
implementation(libs.dagger.android.support)
kapt(libs.dagger.android.processor)
kapt(libs.dagger.compiler)
annotationProcessor(libs.dagger.android.processor)
implementation(libs.kotlin.reflect)
//Mocking
testImplementation(libs.mockito.kotlin)
testImplementation(libs.mockito.core)
testImplementation(libs.powermock.module.junit)
testImplementation(libs.powermock.api.mockito)
testImplementation(libs.mockk)
// Unit testing
testImplementation(libs.junit)
testImplementation(libs.robolectric)
testImplementation(libs.androidx.test.core)
testImplementation(libs.androidx.runner)
testImplementation(libs.androidx.test.ext.junit)
testImplementation(libs.androidx.test.rules)
testImplementation(libs.mockwebserver)
testImplementation(libs.livedata.testing.ktx)
testImplementation(libs.androidx.core.testing)
testImplementation(libs.junit.jupiter.api)
testRuntimeOnly(libs.junit.jupiter.engine)
testImplementation(libs.soloader)
testImplementation(libs.kotlinx.coroutines.test)
debugImplementation(libs.androidx.fragment.testing)
testImplementation(libs.commons.io)
// Android testing
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(libs.androidx.espresso.intents)
androidTestImplementation(libs.androidx.espresso.contrib)
androidTestImplementation(libs.androidx.runner)
androidTestImplementation(libs.androidx.test.rules)
androidTestImplementation(libs.androidx.test.core)
androidTestImplementation(libs.androidx.test.ext.junit)
androidTestImplementation(libs.androidx.annotation)
androidTestImplementation(libs.mockwebserver)
androidTestImplementation(libs.androidx.uiautomator)
// Debugging
debugImplementation(libs.leakcanary.android)
// Support libraries
implementation(libs.androidx.browser)
implementation(libs.androidx.cardview)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.exifinterface)
implementation(libs.recyclerview.fastscroll)
//swipe_layout
implementation(libs.swipelayout.library)
//Room
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.room.ktx)
implementation(libs.androidx.room.rxjava)
kapt(libs.androidx.room.compiler)
// Preferences
implementation(libs.androidx.preference)
implementation(libs.androidx.preference.ktx)
//Android Media
implementation(libs.juanitobananas.androidDmediaUtil)
implementation(libs.androidx.multidex)
// Kotlin + coroutines
implementation(libs.androidx.work.runtime.ktx)
implementation(libs.androidx.work.runtime)
implementation(libs.kotlinx.coroutines.rx2)
testImplementation(libs.androidx.work.testing)
//Glide
implementation(libs.glide)
annotationProcessor(libs.glide.compiler)
kaptTest(libs.androidx.databinding.compiler)
kaptAndroidTest(libs.androidx.databinding.compiler)
implementation(libs.coordinates2country.android) {
exclude(group = "com.google.android", module = "android")
}
//OSMDroid
implementation(libs.osmdroid.android)
constraints {
implementation(libs.kotlin.stdlib.jdk7) {
because("kotlin-stdlib-jdk7 is now a part of kotlin-stdlib")
}
implementation(libs.kotlin.stdlib.jdk8) {
because("kotlin-stdlib-jdk8 is now a part of kotlin-stdlib")
}
}
}
tasks.register<Exec>("disableAnimations") {
val adb = "${System.getenv("ANDROID_HOME")}/platform-tools/adb"
commandLine(adb, "shell", "settings", "put", "global", "window_animation_scale", "0")
commandLine(adb, "shell", "settings", "put", "global", "transition_animation_scale", "0")
commandLine(adb, "shell", "settings", "put", "global", "animator_duration_scale", "0")
}
project.gradle.taskGraph.whenReady {
val connectedBetaDebugAndroidTest = tasks.named("connectedBetaDebugAndroidTest")
val connectedProdDebugAndroidTest = tasks.named("connectedProdDebugAndroidTest")
connectedBetaDebugAndroidTest.configure {
dependsOn("disableAnimations")
}
connectedProdDebugAndroidTest.configure {
dependsOn("disableAnimations")
}
}
fun getTestUserName(): String? {
val propFile = rootProject.file("./local.properties")
val properties = Properties()
propFile.inputStream().use { properties.load(it) }
return properties.getProperty("TEST_USER_NAME")
}
fun getTestPassword(): String? {
val propFile = rootProject.file("./local.properties")
val properties = Properties()
propFile.inputStream().use { properties.load(it) }
return properties.getProperty("TEST_USER_PASSWORD")
}
if (isRunningOnTravisAndIsNotPRBuild) {
configure<com.github.triplet.gradle.play.PlayPublisherExtension> {
track = "alpha"
userFraction = 1.0
serviceAccountEmail = System.getenv("SERVICE_ACCOUNT_NAME")
serviceAccountCredentials = file("../play.p12")
resolutionStrategy = "auto"
outputProcessor { // this: ApkVariantOutput
versionNameOverride = "$versionNameOverride.$versionCode"
}
}
}
fun getBuildVersion(): String? {
return try {
val stdout = ByteArrayOutputStream()
exec {
commandLine("git", "rev-parse", "--short", "HEAD")
standardOutput = stdout
}
stdout.toString().trim()
} catch (e: Exception) {
null
}
}
fun getBranchName(): String? {
return try {
val stdout = ByteArrayOutputStream()
exec {
commandLine("git", "rev-parse", "--abbrev-ref", "HEAD")
standardOutput = stdout
}
stdout.toString().trim()
} catch (e: Exception) {
null
}
}

View file

@ -32,13 +32,15 @@
# Retain declared checked exceptions for use by a Proxy instance.
-keepattributes Exceptions
# Application classes that will be serialized/deserialized over Gson
-keepclasseswithmembers class org.wikipedia.** { *; }
# Note: The model package right now seems to include some other classes that
# are not used for serialization / deserialization over Gson. Hopefully
# that's not a problem since it only prevents R8 from avoiding trimming
# of few more classes.
-keepclasseswithmembers class fr.free.nrw.commons.*.model.** { *; }
-keepclasseswithmembers class fr.free.nrw.commons.actions.** { *; }
-keepclasseswithmembers class fr.free.nrw.commons.auth.csrf.** { *; }
-keepclasseswithmembers class fr.free.nrw.commons.auth.login.** { *; }
-keepclasseswithmembers class fr.free.nrw.commons.wikidata.mwapi.** { *; }
# --- /Retrofit ---
@ -64,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

@ -25,7 +25,6 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class AboutActivityTest {
@get:Rule
var activityRule: ActivityTestRule<*> = ActivityTestRule(AboutActivity::class.java)
@ -36,7 +35,8 @@ class AboutActivityTest {
device.setOrientationNatural()
device.freezeRotation()
Intents.init()
Intents.intending(CoreMatchers.not(IntentMatchers.isInternal()))
Intents
.intending(CoreMatchers.not(IntentMatchers.isInternal()))
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
}
@ -47,11 +47,12 @@ class AboutActivityTest {
@Test
fun testBuildNumber() {
Espresso.onView(ViewMatchers.withId(R.id.about_version))
Espresso
.onView(ViewMatchers.withId(R.id.about_version))
.check(
ViewAssertions.matches(
withText(getApplicationContext<CommonsApplication>().getVersionNameWithSha())
)
withText(getApplicationContext<CommonsApplication>().getVersionNameWithSha()),
),
)
}
@ -61,8 +62,8 @@ class AboutActivityTest {
Intents.intended(
CoreMatchers.allOf(
IntentMatchers.hasAction(Intent.ACTION_VIEW),
IntentMatchers.hasData(Urls.WEBSITE_URL)
)
IntentMatchers.hasData(Urls.WEBSITE_URL),
),
)
}
@ -73,8 +74,8 @@ class AboutActivityTest {
CoreMatchers.anyOf(
IntentMatchers.hasAction(Intent.ACTION_VIEW),
IntentMatchers.hasData(Urls.FACEBOOK_WEB_URL),
IntentMatchers.hasPackage(Urls.FACEBOOK_PACKAGE_NAME)
)
IntentMatchers.hasPackage(Urls.FACEBOOK_PACKAGE_NAME),
),
)
}
@ -84,8 +85,8 @@ class AboutActivityTest {
Intents.intended(
CoreMatchers.allOf(
IntentMatchers.hasAction(Intent.ACTION_VIEW),
IntentMatchers.hasData(Urls.GITHUB_REPO_URL)
)
IntentMatchers.hasData(Urls.GITHUB_REPO_URL),
),
)
}
@ -95,8 +96,8 @@ class AboutActivityTest {
Intents.intended(
CoreMatchers.allOf(
IntentMatchers.hasAction(Intent.ACTION_VIEW),
IntentMatchers.hasData(BuildConfig.PRIVACY_POLICY_URL)
)
IntentMatchers.hasData(BuildConfig.PRIVACY_POLICY_URL),
),
)
}
@ -104,12 +105,12 @@ class AboutActivityTest {
fun testLaunchTranslate() {
Espresso.onView(ViewMatchers.withId(R.id.about_translate)).perform(ViewActions.click())
Espresso.onView(ViewMatchers.withId(android.R.id.button1)).perform(ViewActions.click())
val langCode = CommonsApplication.getInstance().languageLookUpTable.codes[0]
val langCode = CommonsApplication.instance.languageLookUpTable!!.getCodes()[0]
Intents.intended(
CoreMatchers.allOf(
IntentMatchers.hasAction(Intent.ACTION_VIEW),
IntentMatchers.hasData("${Urls.TRANSLATE_WIKI_URL}$langCode")
)
IntentMatchers.hasData("${Urls.TRANSLATE_WIKI_URL}$langCode"),
),
)
}
@ -119,27 +120,30 @@ class AboutActivityTest {
Intents.intended(
CoreMatchers.allOf(
IntentMatchers.hasAction(Intent.ACTION_VIEW),
IntentMatchers.hasData(Urls.CREDITS_URL)
)
IntentMatchers.hasData(Urls.CREDITS_URL),
),
)
}
@Test
fun testLaunchUserGuide() {
Espresso.onView(ViewMatchers.withId(R.id.about_user_guide)).perform(ViewActions.click())
Intents.intended(CoreMatchers.allOf(IntentMatchers.hasAction(Intent.ACTION_VIEW),
IntentMatchers.hasData(Urls.USER_GUIDE_URL)))
Intents.intended(
CoreMatchers.allOf(
IntentMatchers.hasAction(Intent.ACTION_VIEW),
IntentMatchers.hasData(Urls.USER_GUIDE_URL),
),
)
}
@Test
fun testLaunchAboutFaq() {
Espresso.onView(ViewMatchers.withId(R.id.about_faq)).perform(ViewActions.click())
Intents.intended(
CoreMatchers.allOf(
IntentMatchers.hasAction(Intent.ACTION_VIEW),
IntentMatchers.hasData(Urls.FAQ_URL)
)
IntentMatchers.hasData(Urls.FAQ_URL),
),
)
}
}

View file

@ -18,12 +18,14 @@ import fr.free.nrw.commons.auth.LoginActivity
import fr.free.nrw.commons.auth.SignupActivity
import org.hamcrest.CoreMatchers
import org.hamcrest.CoreMatchers.not
import org.junit.*
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class LoginActivityTest {
@get:Rule
var activityRule = ActivityTestRule(LoginActivity::class.java)
@ -49,8 +51,8 @@ class LoginActivityTest {
Intents.intended(
CoreMatchers.allOf(
IntentMatchers.hasAction(Intent.ACTION_VIEW),
IntentMatchers.hasData(BuildConfig.FORGOT_PASSWORD_URL)
)
IntentMatchers.hasData(BuildConfig.FORGOT_PASSWORD_URL),
),
)
}

View file

@ -21,20 +21,23 @@ import fr.free.nrw.commons.kvstore.JsonKvStore
import fr.free.nrw.commons.notification.NotificationActivity
import org.hamcrest.CoreMatchers
import org.hamcrest.Matchers
import org.junit.*
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@LargeTest
@RunWith(AndroidJUnit4::class)
class MainActivityTest {
@get:Rule
var activityRule: ActivityTestRule<*> = ActivityTestRule(LoginActivity::class.java)
@get:Rule
var mGrantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(
"android.permission.ACCESS_FINE_LOCATION"
)
var mGrantPermissionRule: GrantPermissionRule =
GrantPermissionRule.grant(
"android.permission.ACCESS_FINE_LOCATION",
)
private val device: UiDevice =
UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
@ -48,7 +51,8 @@ class MainActivityTest {
UITestHelper.loginUser()
UITestHelper.skipWelcome()
Intents.init()
Intents.intending(CoreMatchers.not(IntentMatchers.isInternal()))
Intents
.intending(CoreMatchers.not(IntentMatchers.isInternal()))
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
val context = InstrumentationRegistry.getInstrumentation().targetContext
val storeName = context.packageName + "_preferences"
@ -62,169 +66,149 @@ class MainActivityTest {
@Test
fun testNearby() {
Espresso.onView(
Matchers.allOf(
childAtPosition(
Espresso
.onView(
Matchers.allOf(
childAtPosition(
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
0
childAtPosition(
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
0,
),
1,
),
1
ViewMatchers.isDisplayed(),
),
ViewMatchers.isDisplayed()
)
).perform(ViewActions.click())
Espresso.onView(ViewMatchers.withId(R.id.fragmentContainer))
).perform(ViewActions.click())
Espresso
.onView(ViewMatchers.withId(R.id.fragmentContainer))
.check(matches(ViewMatchers.isDisplayed()))
UITestHelper.sleep(10000)
val actionMenuItemView2 = Espresso.onView(
Matchers.allOf(
ViewMatchers.withId(R.id.list_sheet), ViewMatchers.withContentDescription("List"),
childAtPosition(
val actionMenuItemView2 =
Espresso.onView(
Matchers.allOf(
ViewMatchers.withId(R.id.list_sheet),
ViewMatchers.withContentDescription("List"),
childAtPosition(
ViewMatchers.withId(R.id.toolbar),
1
childAtPosition(
ViewMatchers.withId(R.id.toolbar),
1,
),
0,
),
0
ViewMatchers.isDisplayed(),
),
ViewMatchers.isDisplayed()
)
)
actionMenuItemView2.perform(ViewActions.click())
UITestHelper.sleep(1000)
}
@Test
fun testExplore() {
Espresso.onView(
Matchers.allOf(
childAtPosition(
Espresso
.onView(
Matchers.allOf(
childAtPosition(
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
0
childAtPosition(
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
0,
),
2,
),
2
ViewMatchers.isDisplayed(),
),
ViewMatchers.isDisplayed()
)
).perform(ViewActions.click())
Espresso.onView(ViewMatchers.withId(R.id.fragmentContainer))
).perform(ViewActions.click())
Espresso
.onView(ViewMatchers.withId(R.id.fragmentContainer))
.check(matches(ViewMatchers.isDisplayed()))
UITestHelper.sleep(1000)
}
@Test
fun testContributions() {
Espresso.onView(
Matchers.allOf(
childAtPosition(
Espresso
.onView(
Matchers.allOf(
childAtPosition(
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
0
childAtPosition(
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
0,
),
0,
),
0
ViewMatchers.isDisplayed(),
),
ViewMatchers.isDisplayed()
)
).perform(ViewActions.click())
Espresso.onView(ViewMatchers.withId(R.id.fragmentContainer))
).perform(ViewActions.click())
Espresso
.onView(ViewMatchers.withId(R.id.fragmentContainer))
.check(matches(ViewMatchers.isDisplayed()))
Espresso.onView(
Matchers.allOf(
ViewMatchers.withId(R.id.contributionImage),
childAtPosition(
Espresso
.onView(
Matchers.allOf(
ViewMatchers.withId(R.id.contributionImage),
childAtPosition(
ViewMatchers.withId(R.id.contributionsList),
0
childAtPosition(
ViewMatchers.withId(R.id.contributionsList),
0,
),
1,
),
1
ViewMatchers.isDisplayed(),
),
ViewMatchers.isDisplayed()
)
).perform(ViewActions.click())
val actionMenuItemView = Espresso.onView(
Matchers.allOf(
ViewMatchers.withId(R.id.menu_bookmark_current_image),
childAtPosition(
).perform(ViewActions.click())
val actionMenuItemView =
Espresso.onView(
Matchers.allOf(
ViewMatchers.withId(R.id.menu_bookmark_current_image),
childAtPosition(
ViewMatchers.withId(R.id.toolbar),
1
childAtPosition(
ViewMatchers.withId(R.id.toolbar),
1,
),
0,
),
0
ViewMatchers.isDisplayed(),
),
ViewMatchers.isDisplayed()
)
)
actionMenuItemView.perform(ViewActions.click())
UITestHelper.sleep(3000)
}
@Test
fun testBookmarks() {
Espresso.onView(
Matchers.allOf(
childAtPosition(
Espresso
.onView(
Matchers.allOf(
childAtPosition(
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
0
childAtPosition(
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
0,
),
3,
),
3
ViewMatchers.isDisplayed(),
),
ViewMatchers.isDisplayed()
)
).perform(ViewActions.click())
).perform(ViewActions.click())
UITestHelper.sleep(1000)
}
@Test
fun testNotifications() {
Espresso.onView(
Matchers.allOf(
ViewMatchers.withId(R.id.notifications),
childAtPosition(
Espresso
.onView(
Matchers.allOf(
ViewMatchers.withId(R.id.notifications),
childAtPosition(
ViewMatchers.withId(R.id.toolbar),
1
childAtPosition(
ViewMatchers.withId(R.id.toolbar),
1,
),
1,
),
1
ViewMatchers.isDisplayed(),
),
ViewMatchers.isDisplayed()
)
).perform(ViewActions.click())
).perform(ViewActions.click())
Intents.intended(IntentMatchers.hasComponent(NotificationActivity::class.java.name))
Espresso.pressBack()
UITestHelper.sleep(1000)
}
@Test
fun testLimitedConnectionModeToggle() {
val isEnabled = defaultKvStore
.getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false)
Espresso.onView(
Matchers.allOf(
ViewMatchers.withId(R.id.toggle_limited_connection_mode),
childAtPosition(
childAtPosition(
ViewMatchers.withId(R.id.toolbar),
1
),
0
),
ViewMatchers.isDisplayed()
)
).perform(ViewActions.click())
UITestHelper.sleep(1000)
if (isEnabled) {
Assert.assertFalse(
defaultKvStore
.getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false)
)
} else {
Assert.assertTrue(
defaultKvStore
.getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false)
)
}
}
}

View file

@ -4,7 +4,6 @@ import android.app.Activity
import android.app.Instrumentation
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.swipeRight
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.matcher.IntentMatchers
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
@ -26,7 +25,6 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ProfileActivityTest {
@get:Rule
var activityRule = IntentsTestRule(LoginActivity::class.java)
@ -38,7 +36,8 @@ class ProfileActivityTest {
device.freezeRotation()
UITestHelper.loginUser()
UITestHelper.skipWelcome()
Intents.intending(CoreMatchers.not(IntentMatchers.isInternal()))
Intents
.intending(CoreMatchers.not(IntentMatchers.isInternal()))
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
}
@ -50,20 +49,19 @@ class ProfileActivityTest {
childAtPosition(
childAtPosition(
withId(R.id.fragment_main_nav_tab_layout),
0
0,
),
4
4,
),
ViewMatchers.isDisplayed()
)
ViewMatchers.isDisplayed(),
),
).perform(ViewActions.click())
onView(Matchers.allOf(withId(R.id.more_profile))).perform(
ViewActions.scrollTo(),
ViewActions.click()
ViewActions.click(),
)
device.swipe(1033,1346,531,1346,20)
device.swipe(1033, 1346, 531, 1346, 20)
UITestHelper.sleep(5000)
Intents.intended(hasComponent(ProfileActivity::class.java.name))
}
}

View file

@ -9,7 +9,6 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ReviewActivityTest {
@get:Rule
var activityRule: ActivityTestRule<*> = ActivityTestRule(ReviewActivity::class.java)
@ -17,5 +16,4 @@ class ReviewActivityTest {
fun orientationChange() {
UITestHelper.changeOrientation(activityRule)
}
}

View file

@ -16,7 +16,6 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class SearchActivityTest {
@get:Rule
var activityRule = ActivityTestRule(SearchActivity::class.java)
@ -31,21 +30,22 @@ class SearchActivityTest {
@Test
fun exploreActivityTest() {
val searchAutoComplete = Espresso.onView(
Matchers.allOf(
UITestHelper.childAtPosition(
Matchers.allOf(
ViewMatchers.withClassName(Matchers.`is`("android.widget.LinearLayout")),
UITestHelper.childAtPosition(
val searchAutoComplete =
Espresso.onView(
Matchers.allOf(
UITestHelper.childAtPosition(
Matchers.allOf(
ViewMatchers.withClassName(Matchers.`is`("android.widget.LinearLayout")),
1
)
UITestHelper.childAtPosition(
ViewMatchers.withClassName(Matchers.`is`("android.widget.LinearLayout")),
1,
),
),
0,
),
0
ViewMatchers.isDisplayed(),
),
ViewMatchers.isDisplayed()
)
)
searchAutoComplete.perform(ViewActions.replaceText("cat"), ViewActions.closeSoftKeyboard())
UITestHelper.sleep(5000)
device.swipe(1000, 1400, 500, 1400, 20)

View file

@ -22,7 +22,6 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class SettingsActivityLoggedInTest {
@get:Rule
var activityRule: ActivityTestRule<*> = ActivityTestRule(LoginActivity::class.java)
@ -35,31 +34,32 @@ class SettingsActivityLoggedInTest {
device.freezeRotation()
UITestHelper.loginUser()
UITestHelper.skipWelcome()
Intents.intending(CoreMatchers.not(IntentMatchers.isInternal()))
Intents
.intending(CoreMatchers.not(IntentMatchers.isInternal()))
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
}
@Test
fun testSettings() {
Espresso.onView(
Matchers.allOf(
ViewMatchers.withContentDescription("More"),
UITestHelper.childAtPosition(
Espresso
.onView(
Matchers.allOf(
ViewMatchers.withContentDescription("More"),
UITestHelper.childAtPosition(
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
0
UITestHelper.childAtPosition(
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
0,
),
4,
),
4
ViewMatchers.isDisplayed(),
),
ViewMatchers.isDisplayed()
)
).perform(ViewActions.click())
).perform(ViewActions.click())
Espresso.onView(Matchers.allOf(ViewMatchers.withId(R.id.more_settings))).perform(
ViewActions.scrollTo(),
ViewActions.click()
ViewActions.click(),
)
Intents.intended(IntentMatchers.hasComponent(SettingsActivity::class.java.name))
UITestHelper.sleep(1000)
}
}

View file

@ -23,7 +23,6 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class SettingsActivityTest {
private lateinit var defaultKvStore: JsonKvStore
@get:Rule
@ -44,22 +43,24 @@ class SettingsActivityTest {
fun useAuthorNameTogglesOn() {
// Turn on "Use author name" preference if currently off
if (!defaultKvStore.getBoolean("useAuthorName", false)) {
Espresso.onView(
allOf(
withId(R.id.recycler_view),
childAtPosition(withId(android.R.id.list_container), 0)
Espresso
.onView(
allOf(
withId(R.id.recycler_view),
childAtPosition(withId(android.R.id.list_container), 0),
),
).perform(
RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(6, click()),
)
).perform(
RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(6, click())
)
}
// Check authorName preference is enabled
Espresso.onView(
allOf(
withId(R.id.recycler_view),
childAtPosition(withId(android.R.id.list_container), 0)
)
).check(matches(isEnabled()))
Espresso
.onView(
allOf(
withId(R.id.recycler_view),
childAtPosition(withId(android.R.id.list_container), 0),
),
).check(matches(isEnabled()))
}
@Test

View file

@ -10,17 +10,20 @@ import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.rule.ActivityTestRule
import org.apache.commons.lang3.StringUtils
import org.hamcrest.*
import org.hamcrest.BaseMatcher
import org.hamcrest.Description
import org.hamcrest.Matcher
import org.hamcrest.Matchers
import org.hamcrest.TypeSafeMatcher
import timber.log.Timber
class UITestHelper {
companion object {
fun skipWelcome() {
try {
onView(ViewMatchers.withId(R.id.button_ok))
.perform(ViewActions.click())
//Skip tutorial
// Skip tutorial
onView(ViewMatchers.withId(R.id.finishTutorialButton))
.perform(ViewActions.click())
} catch (ignored: NoMatchingViewException) {
@ -29,27 +32,31 @@ class UITestHelper {
fun skipLogin() {
try {
//Skip Login
val htmlTextView = onView(
Matchers.allOf(
ViewMatchers.withId(R.id.skip_login), ViewMatchers.withText("Skip"),
ViewMatchers.isDisplayed()
// Skip Login
val htmlTextView =
onView(
Matchers.allOf(
ViewMatchers.withId(R.id.skip_login),
ViewMatchers.withText("Skip"),
ViewMatchers.isDisplayed(),
),
)
)
htmlTextView.perform(ViewActions.click())
val appCompatButton = onView(
Matchers.allOf(
ViewMatchers.withId(android.R.id.button1), ViewMatchers.withText("Yes"),
childAtPosition(
val appCompatButton =
onView(
Matchers.allOf(
ViewMatchers.withId(android.R.id.button1),
ViewMatchers.withText("Yes"),
childAtPosition(
ViewMatchers.withId(R.id.buttonPanel),
0
childAtPosition(
ViewMatchers.withId(R.id.buttonPanel),
0,
),
3,
),
3
)
),
)
)
appCompatButton.perform(ViewActions.scrollTo(), ViewActions.click())
} catch (ignored: NoMatchingViewException) {
}
@ -57,18 +64,18 @@ class UITestHelper {
fun loginUser() {
try {
//Perform Login
// Perform Login
sleep(3000)
onView(ViewMatchers.withId(R.id.login_username))
.perform(
ViewActions.replaceText(getTestUsername()),
ViewActions.closeSoftKeyboard()
ViewActions.closeSoftKeyboard(),
)
sleep(2000)
onView(ViewMatchers.withId(R.id.login_password))
.perform(
ViewActions.replaceText(getTestUserPassword()),
ViewActions.closeSoftKeyboard()
ViewActions.closeSoftKeyboard(),
)
sleep(2000)
onView(ViewMatchers.withId(R.id.login_button))
@ -76,7 +83,6 @@ class UITestHelper {
sleep(10000)
} catch (ignored: NoMatchingViewException) {
}
}
fun logoutUser() {
@ -87,36 +93,38 @@ class UITestHelper {
childAtPosition(
childAtPosition(
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
0
0,
),
4
4,
),
ViewMatchers.isDisplayed()
)
ViewMatchers.isDisplayed(),
),
).perform(ViewActions.click())
onView(
Matchers.allOf(
ViewMatchers.withId(R.id.more_logout), ViewMatchers.withText("Logout"),
ViewMatchers.withId(R.id.more_logout),
ViewMatchers.withText("Logout"),
childAtPosition(
childAtPosition(
ViewMatchers.withId(R.id.scroll_view_more_bottom_sheet),
0
0,
),
6
)
)
6,
),
),
).perform(ViewActions.scrollTo(), ViewActions.click())
onView(
Matchers.allOf(
ViewMatchers.withId(android.R.id.button1), ViewMatchers.withText("Yes"),
ViewMatchers.withId(android.R.id.button1),
ViewMatchers.withText("Yes"),
childAtPosition(
childAtPosition(
ViewMatchers.withId(R.id.buttonPanel),
0
0,
),
3
)
)
3,
),
),
).perform(ViewActions.scrollTo(), ViewActions.click())
sleep(5000)
} catch (ignored: NoMatchingViewException) {
@ -124,9 +132,9 @@ class UITestHelper {
}
fun childAtPosition(
parentMatcher: Matcher<View>, position: Int
parentMatcher: Matcher<View>,
position: Int,
): Matcher<View> {
return object : TypeSafeMatcher<View>() {
override fun describeTo(description: Description) {
description.appendText("Child at position $position in parent ")
@ -135,8 +143,9 @@ class UITestHelper {
public override fun matchesSafely(view: View): Boolean {
val parent = view.parent
return parent is ViewGroup && parentMatcher.matches(parent)
&& view == parent.getChildAt(position)
return parent is ViewGroup &&
parentMatcher.matches(parent) &&
view == parent.getChildAt(position)
}
}
}
@ -154,14 +163,18 @@ class UITestHelper {
val username = BuildConfig.TEST_USERNAME
if (StringUtils.isEmpty(username) || username == "null") {
throw NotImplementedError("Configure your beta account's username")
} else return username
} else {
return username
}
}
private fun getTestUserPassword(): String {
val password = BuildConfig.TEST_PASSWORD
if (StringUtils.isEmpty(password) || password == "null") {
throw NotImplementedError("Configure your beta account's password")
} else return password
} else {
return password
}
}
fun <T : Activity> changeOrientation(activityRule: ActivityTestRule<T>) {
@ -174,6 +187,7 @@ class UITestHelper {
fun <T> first(matcher: Matcher<T>): Matcher<T>? {
return object : BaseMatcher<T>() {
var isFirst = true
override fun matches(item: Any): Boolean {
if (isFirst && matcher.matches(item)) {
isFirst = false

View file

@ -4,7 +4,10 @@ import android.app.Activity
import android.app.Instrumentation
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.*
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
import androidx.test.espresso.action.ViewActions.replaceText
import androidx.test.espresso.action.ViewActions.scrollTo
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.matcher.IntentMatchers
@ -15,7 +18,7 @@ import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.ActivityTestRule
import androidx.test.rule.GrantPermissionRule
import androidx.test.uiautomator.UiDevice
import fr.free.nrw.commons.LocationPicker.LocationPickerActivity
import fr.free.nrw.commons.locationpicker.LocationPickerActivity
import fr.free.nrw.commons.UITestHelper.Companion.childAtPosition
import fr.free.nrw.commons.auth.LoginActivity
import org.hamcrest.CoreMatchers
@ -28,7 +31,6 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class UploadCancelledTest {
@Rule
@JvmField
var mActivityTestRule = ActivityTestRule(LoginActivity::class.java)
@ -37,7 +39,7 @@ class UploadCancelledTest {
@JvmField
var mGrantPermissionRule: GrantPermissionRule =
GrantPermissionRule.grant(
"android.permission.WRITE_EXTERNAL_STORAGE"
"android.permission.WRITE_EXTERNAL_STORAGE",
)
private val device: UiDevice =
@ -47,15 +49,15 @@ class UploadCancelledTest {
fun setup() {
try {
Intents.init()
} catch (ex: IllegalStateException) {
} catch (_: IllegalStateException) {
}
device.unfreezeRotation()
device.setOrientationNatural()
device.freezeRotation()
UITestHelper.loginUser()
UITestHelper.skipWelcome()
Intents.intending(CoreMatchers.not(IntentMatchers.isInternal()))
Intents
.intending(CoreMatchers.not(IntentMatchers.isInternal()))
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
}
@ -63,131 +65,138 @@ class UploadCancelledTest {
fun teardown() {
try {
Intents.release()
} catch (ex: IllegalStateException) {
} catch (_: IllegalStateException) {
}
}
@Test
fun uploadCancelledAfterLocationPickedTest() {
val bottomNavigationItemView = onView(
allOf(
childAtPosition(
val bottomNavigationItemView =
onView(
allOf(
childAtPosition(
withId(R.id.fragment_main_nav_tab_layout),
0
childAtPosition(
withId(R.id.fragment_main_nav_tab_layout),
0,
),
1,
),
1
isDisplayed(),
),
isDisplayed()
)
)
bottomNavigationItemView.perform(click())
UITestHelper.sleep(12000)
val actionMenuItemView = onView(
allOf(
withId(R.id.list_sheet),
childAtPosition(
val actionMenuItemView =
onView(
allOf(
withId(R.id.list_sheet),
childAtPosition(
withId(R.id.toolbar),
1
childAtPosition(
withId(R.id.toolbar),
1,
),
0,
),
0
isDisplayed(),
),
isDisplayed()
)
)
actionMenuItemView.perform(click())
val recyclerView = onView(
allOf(
withId(R.id.rv_nearby_list),
val recyclerView =
onView(
allOf(
withId(R.id.rv_nearby_list),
),
)
)
recyclerView.perform(
RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(
0,
click()
)
click(),
),
)
val linearLayout3 = onView(
allOf(
withId(R.id.cameraButton),
childAtPosition(
allOf(
withId(R.id.nearby_button_layout),
val linearLayout3 =
onView(
allOf(
withId(R.id.cameraButton),
childAtPosition(
allOf(
withId(R.id.nearby_button_layout),
),
0,
),
0
isDisplayed(),
),
isDisplayed()
)
)
linearLayout3.perform(click())
val pasteSensitiveTextInputEditText = onView(
allOf(
withId(R.id.caption_item_edit_text),
childAtPosition(
val pasteSensitiveTextInputEditText =
onView(
allOf(
withId(R.id.caption_item_edit_text),
childAtPosition(
withId(R.id.caption_item_edit_text_input_layout),
0
childAtPosition(
withId(R.id.caption_item_edit_text_input_layout),
0,
),
0,
),
0
isDisplayed(),
),
isDisplayed()
)
)
pasteSensitiveTextInputEditText.perform(replaceText("test"), closeSoftKeyboard())
val pasteSensitiveTextInputEditText2 = onView(
allOf(
withId(R.id.description_item_edit_text),
childAtPosition(
val pasteSensitiveTextInputEditText2 =
onView(
allOf(
withId(R.id.description_item_edit_text),
childAtPosition(
withId(R.id.description_item_edit_text_input_layout),
0
childAtPosition(
withId(R.id.description_item_edit_text_input_layout),
0,
),
0,
),
0
isDisplayed(),
),
isDisplayed()
)
)
pasteSensitiveTextInputEditText2.perform(replaceText("test"), closeSoftKeyboard())
val appCompatButton2 = onView(
allOf(
withId(R.id.btn_next),
childAtPosition(
val appCompatButton2 =
onView(
allOf(
withId(R.id.btn_next),
childAtPosition(
withId(R.id.ll_container_media_detail),
2
childAtPosition(
withId(R.id.ll_container_media_detail),
2,
),
1,
),
1
isDisplayed(),
),
isDisplayed()
)
)
appCompatButton2.perform(click())
val appCompatButton3 = onView(
allOf(
withId(android.R.id.button1),
val appCompatButton3 =
onView(
allOf(
withId(android.R.id.button1),
),
)
)
appCompatButton3.perform(scrollTo(), click())
Intents.intended(IntentMatchers.hasComponent(LocationPickerActivity::class.java.name))
val floatingActionButton3 = onView(
allOf(
withId(R.id.location_chosen_button),
isDisplayed()
val floatingActionButton3 =
onView(
allOf(
withId(R.id.location_chosen_button),
isDisplayed(),
),
)
)
UITestHelper.sleep(2000)
floatingActionButton3.perform(click())
}

View file

@ -19,7 +19,10 @@ import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.Intents.intending
import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
import androidx.test.espresso.intent.matcher.IntentMatchers.hasType
import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withParent
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.rule.ActivityTestRule
@ -29,21 +32,29 @@ import fr.free.nrw.commons.upload.UploadMediaDetailAdapter
import fr.free.nrw.commons.util.MyViewAction
import fr.free.nrw.commons.utils.ConfigUtils
import org.hamcrest.core.AllOf.allOf
import org.junit.*
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import timber.log.Timber
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.*
import java.util.Date
import java.util.Random
@LargeTest
@RunWith(AndroidJUnit4::class)
class UploadTest {
@get:Rule
var permissionRule = GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.ACCESS_FINE_LOCATION)!!
var permissionRule =
GrantPermissionRule.grant(
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.ACCESS_FINE_LOCATION,
)!!
@get:Rule
var activityRule = ActivityTestRule(LoginActivity::class.java)
@ -60,8 +71,7 @@ class UploadTest {
fun setup() {
try {
Intents.init()
} catch (ex: IllegalStateException) {
} catch (_: IllegalStateException) {
}
UITestHelper.loginUser()
UITestHelper.skipWelcome()
@ -94,14 +104,13 @@ class UploadTest {
dismissWarning("Yes")
onView(allOf<View>(isDisplayed(), withId(R.id.tv_title)))
.perform(replaceText(commonsFileName))
.perform(replaceText(commonsFileName))
onView(allOf<View>(isDisplayed(), withId(R.id.description_item_edit_text)))
.perform(replaceText(commonsFileName))
.perform(replaceText(commonsFileName))
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
.perform(click())
.perform(click())
UITestHelper.sleep(5000)
dismissWarning("Yes")
@ -109,29 +118,30 @@ class UploadTest {
UITestHelper.sleep(3000)
onView(allOf(isDisplayed(), withId(R.id.et_search)))
.perform(replaceText("Uploaded with Mobile/Android Tests"))
.perform(replaceText("Uploaded with Mobile/Android Tests"))
UITestHelper.sleep(3000)
try {
onView(allOf(isDisplayed(), UITestHelper.first(withParent(withId(R.id.rv_categories)))))
.perform(click())
.perform(click())
} catch (ignored: NoMatchingViewException) {
}
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
.perform(click())
.perform(click())
dismissWarning("Yes, Submit")
UITestHelper.sleep(500)
onView(allOf(isDisplayed(), withId(R.id.btn_submit)))
.perform(click())
.perform(click())
UITestHelper.sleep(10000)
val fileUrl = "https://commons.wikimedia.beta.wmflabs.org/wiki/File:" +
val fileUrl =
"https://commons.wikimedia.beta.wmflabs.org/wiki/File:" +
commonsFileName.replace(' ', '_') + ".jpg"
Timber.i("File should be uploaded to $fileUrl")
}
@ -139,8 +149,8 @@ class UploadTest {
private fun dismissWarning(warningText: String) {
try {
onView(withText(warningText))
.check(matches(isDisplayed()))
.perform(click())
.check(matches(isDisplayed()))
.perform(click())
} catch (ignored: NoMatchingViewException) {
}
}
@ -167,10 +177,10 @@ class UploadTest {
dismissWarning("Yes")
onView(allOf<View>(isDisplayed(), withId(R.id.tv_title)))
.perform(replaceText(commonsFileName))
.perform(replaceText(commonsFileName))
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
.perform(click())
.perform(click())
UITestHelper.sleep(10000)
dismissWarning("Yes")
@ -178,29 +188,30 @@ class UploadTest {
UITestHelper.sleep(3000)
onView(allOf(isDisplayed(), withId(R.id.et_search)))
.perform(replaceText("Test"))
.perform(replaceText("Test"))
UITestHelper.sleep(3000)
try {
onView(allOf(isDisplayed(), UITestHelper.first(withParent(withId(R.id.rv_categories)))))
.perform(click())
.perform(click())
} catch (ignored: NoMatchingViewException) {
}
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
.perform(click())
.perform(click())
dismissWarning("Yes, Submit")
UITestHelper.sleep(500)
onView(allOf(isDisplayed(), withId(R.id.btn_submit)))
.perform(click())
.perform(click())
UITestHelper.sleep(10000)
val fileUrl = "https://commons.wikimedia.beta.wmflabs.org/wiki/File:" +
val fileUrl =
"https://commons.wikimedia.beta.wmflabs.org/wiki/File:" +
commonsFileName.replace(' ', '_') + ".jpg"
Timber.i("File should be uploaded to $fileUrl")
}
@ -227,23 +238,29 @@ class UploadTest {
dismissWarningDialog()
onView(allOf<View>(isDisplayed(), withId(R.id.tv_title)))
.perform(replaceText(commonsFileName))
.perform(replaceText(commonsFileName))
onView(withId(R.id.rv_descriptions)).perform(
RecyclerViewActions
.actionOnItemAtPosition<UploadMediaDetailAdapter.ViewHolder>(0,
MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Test description")))
RecyclerViewActions
.actionOnItemAtPosition<UploadMediaDetailAdapter.ViewHolder>(
0,
MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Test description"),
),
)
onView(withId(R.id.btn_add))
.perform(click())
.perform(click())
onView(withId(R.id.rv_descriptions)).perform(
RecyclerViewActions
.actionOnItemAtPosition<UploadMediaDetailAdapter.ViewHolder>(1,
MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Description")))
RecyclerViewActions
.actionOnItemAtPosition<UploadMediaDetailAdapter.ViewHolder>(
1,
MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Description"),
),
)
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
.perform(click())
.perform(click())
UITestHelper.sleep(5000)
dismissWarning("Yes")
@ -251,29 +268,30 @@ class UploadTest {
UITestHelper.sleep(3000)
onView(allOf(isDisplayed(), withId(R.id.et_search)))
.perform(replaceText("Test"))
.perform(replaceText("Test"))
UITestHelper.sleep(3000)
try {
onView(allOf(isDisplayed(), UITestHelper.first(withParent(withId(R.id.rv_categories)))))
.perform(click())
.perform(click())
} catch (ignored: NoMatchingViewException) {
}
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
.perform(click())
.perform(click())
dismissWarning("Yes, Submit")
UITestHelper.sleep(500)
onView(allOf(isDisplayed(), withId(R.id.btn_submit)))
.perform(click())
.perform(click())
UITestHelper.sleep(10000)
val fileUrl = "https://commons.wikimedia.beta.wmflabs.org/wiki/File:" +
val fileUrl =
"https://commons.wikimedia.beta.wmflabs.org/wiki/File:" +
commonsFileName.replace(' ', '_') + ".jpg"
Timber.i("File should be uploaded to $fileUrl")
}
@ -306,7 +324,6 @@ class UploadTest {
} catch (e: IOException) {
e.printStackTrace()
}
}
}
@ -328,8 +345,8 @@ class UploadTest {
private fun dismissWarningDialog() {
try {
onView(withText("Yes"))
.check(matches(isDisplayed()))
.perform(click())
.check(matches(isDisplayed()))
.perform(click())
} catch (ignored: NoMatchingViewException) {
}
}
@ -337,10 +354,10 @@ class UploadTest {
private fun openGallery() {
// Open FAB
onView(allOf<View>(withId(R.id.fab_plus), isDisplayed()))
.perform(click())
.perform(click())
// Click gallery
onView(allOf<View>(withId(R.id.fab_gallery), isDisplayed()))
.perform(click())
.perform(click())
}
}

View file

@ -3,7 +3,6 @@ package fr.free.nrw.commons
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.runners.AndroidJUnit4
@ -18,11 +17,12 @@ import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.CoreMatchers.equalTo
@LargeTest
@RunWith(AndroidJUnit4::class)
class WelcomeActivityTest {
@get:Rule
var activityRule: ActivityTestRule<*> = ActivityTestRule(WelcomeActivity::class.java)
@ -61,7 +61,7 @@ class WelcomeActivityTest {
.perform(ViewActions.click())
onView(withId(R.id.finishTutorialButton))
.perform(ViewActions.click())
assert(activityRule.activity.isDestroyed)
assertThat(activityRule.activity.isDestroyed, equalTo(true))
}
}
@ -71,10 +71,10 @@ class WelcomeActivityTest {
.perform(ViewActions.click())
onView(withId(R.id.welcomePager))
.perform(ViewActions.swipeLeft())
assert(true)
assertThat(true, equalTo(true))
onView(withId(R.id.welcomePager))
.perform(ViewActions.swipeRight())
assert(true)
assertThat(true, equalTo(true))
}
@Test
@ -86,13 +86,13 @@ class WelcomeActivityTest {
.perform(ViewActions.swipeLeft())
.perform(ViewActions.swipeLeft())
.perform(ViewActions.swipeLeft())
assert(true)
assertThat(true, equalTo(true))
onView(withId(R.id.welcomePager))
.perform(ViewActions.swipeRight())
.perform(ViewActions.swipeRight())
.perform(ViewActions.swipeRight())
.perform(ViewActions.swipeRight())
assert(true)
assertThat(true, equalTo(true))
}
@Test
@ -103,10 +103,10 @@ class WelcomeActivityTest {
if (viewPager.currentItem == 3) {
onView(withId(R.id.welcomePager))
.perform(ViewActions.swipeLeft())
assert(true)
assertThat(true, equalTo(true))
onView(withId(R.id.welcomePager))
.perform(ViewActions.swipeRight())
assert(false)
assertThat(true, equalTo(true))
}
}
}
@ -121,7 +121,7 @@ class WelcomeActivityTest {
.perform(ViewActions.click())
onView(withId(R.id.finishTutorialButton))
.perform(ViewActions.click())
assert(activityRule.activity.isDestroyed)
assertThat(activityRule.activity.isDestroyed, equalTo(true))
}
}
}

View file

@ -30,7 +30,6 @@ import java.lang.reflect.Method
@Config(sdk = [21], application = TestCommonsApplication::class)
@LooperMode(LooperMode.Mode.PAUSED)
class ContributionsListFragmentUnitTests {
private lateinit var scenario: FragmentScenario<ContributionsListFragment>
private lateinit var fragment: ContributionsListFragment
@ -43,17 +42,19 @@ class ContributionsListFragmentUnitTests {
fun setUp() {
OkHttpConnectionFactory.CLIENT = createTestClient()
scenario = launchFragmentInContainer(
initialState = Lifecycle.State.RESUMED,
themeResId = R.style.LightAppTheme
) {
ContributionsListFragment().apply {
contributionsListPresenter = mock()
callback = mock()
}.also {
fragment = it
scenario =
launchFragmentInContainer(
initialState = Lifecycle.State.RESUMED,
themeResId = R.style.LightAppTheme,
) {
ContributionsListFragment()
.apply {
contributionsListPresenter = mock()
callback = mock()
}.also {
fragment = it
}
}
}
scenario.onFragment {
it.adapter = adapter
@ -88,7 +89,7 @@ class ContributionsListFragmentUnitTests {
Shadows.shadowOf(Looper.getMainLooper()).idle()
fragment.rvContributionsList = mock()
fragment.scrollToTop()
verify(fragment.rvContributionsList).smoothScrollToPosition(0)
verify(fragment.rvContributionsList)?.smoothScrollToPosition(0)
}
@Test
@ -129,28 +130,15 @@ class ContributionsListFragmentUnitTests {
@Throws(Exception::class)
fun testShowAddImageToWikipediaInstructions() {
Shadows.shadowOf(Looper.getMainLooper()).idle()
val method: Method = ContributionsListFragment::class.java.getDeclaredMethod(
"showAddImageToWikipediaInstructions",
Contribution::class.java
)
val method: Method =
ContributionsListFragment::class.java.getDeclaredMethod(
"showAddImageToWikipediaInstructions",
Contribution::class.java,
)
method.isAccessible = true
method.invoke(fragment, contribution)
}
@Test
@Throws(Exception::class)
fun testResumeUpload() {
Shadows.shadowOf(Looper.getMainLooper()).idle()
fragment.resumeUpload(contribution)
}
@Test
@Throws(Exception::class)
fun testPauseUpload() {
Shadows.shadowOf(Looper.getMainLooper()).idle()
fragment.pauseUpload(contribution)
}
@Test
@Throws(Exception::class)
fun testAddImageToWikipedia() {
@ -165,20 +153,6 @@ class ContributionsListFragmentUnitTests {
fragment.openMediaDetail(0, true)
}
@Test
@Throws(Exception::class)
fun testDeleteUpload() {
Shadows.shadowOf(Looper.getMainLooper()).idle()
fragment.deleteUpload(contribution)
}
@Test
@Throws(Exception::class)
fun testRetryUpload() {
Shadows.shadowOf(Looper.getMainLooper()).idle()
fragment.retryUpload(contribution)
}
@Test
@Throws(Exception::class)
fun testOnViewStateRestored() {
@ -221,10 +195,11 @@ class ContributionsListFragmentUnitTests {
scenario.onFragment {
it.requireView().findViewById<FloatingActionButton>(R.id.fab_plus).hide()
}
val method: Method = ContributionsListFragment::class.java.getDeclaredMethod(
"animateFAB",
Boolean::class.java
)
val method: Method =
ContributionsListFragment::class.java.getDeclaredMethod(
"animateFAB",
Boolean::class.java,
)
method.isAccessible = true
method.invoke(fragment, true)
}
@ -236,10 +211,11 @@ class ContributionsListFragmentUnitTests {
scenario.onFragment {
it.requireView().findViewById<FloatingActionButton>(R.id.fab_plus).show()
}
val method: Method = ContributionsListFragment::class.java.getDeclaredMethod(
"animateFAB",
Boolean::class.java
)
val method: Method =
ContributionsListFragment::class.java.getDeclaredMethod(
"animateFAB",
Boolean::class.java,
)
method.isAccessible = true
method.invoke(fragment, true)
}
@ -251,10 +227,11 @@ class ContributionsListFragmentUnitTests {
scenario.onFragment {
it.requireView().findViewById<FloatingActionButton>(R.id.fab_plus).show()
}
val method: Method = ContributionsListFragment::class.java.getDeclaredMethod(
"animateFAB",
Boolean::class.java
)
val method: Method =
ContributionsListFragment::class.java.getDeclaredMethod(
"animateFAB",
Boolean::class.java,
)
method.isAccessible = true
method.invoke(fragment, false)
}
@ -263,9 +240,10 @@ class ContributionsListFragmentUnitTests {
@Throws(Exception::class)
fun testSetListeners() {
Shadows.shadowOf(Looper.getMainLooper()).idle()
val method: Method = ContributionsListFragment::class.java.getDeclaredMethod(
"setListeners"
)
val method: Method =
ContributionsListFragment::class.java.getDeclaredMethod(
"setListeners",
)
method.isAccessible = true
method.invoke(fragment)
}
@ -274,9 +252,10 @@ class ContributionsListFragmentUnitTests {
@Throws(Exception::class)
fun testInitializeAnimations() {
Shadows.shadowOf(Looper.getMainLooper()).idle()
val method: Method = ContributionsListFragment::class.java.getDeclaredMethod(
"initializeAnimations"
)
val method: Method =
ContributionsListFragment::class.java.getDeclaredMethod(
"initializeAnimations",
)
method.isAccessible = true
method.invoke(fragment)
}

View file

@ -5,6 +5,7 @@ import androidx.fragment.app.testing.FragmentScenario
import androidx.fragment.app.testing.launchFragmentInContainer
import androidx.lifecycle.Lifecycle
import androidx.test.ext.junit.runners.AndroidJUnit4
import fr.free.nrw.commons.R
import fr.free.nrw.commons.TestCommonsApplication
import org.junit.Before
import org.junit.Test
@ -12,7 +13,6 @@ import org.junit.runner.RunWith
import org.robolectric.Shadows
import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode
import fr.free.nrw.commons.R
@RunWith(AndroidJUnit4::class)
@Config(sdk = [21], application = TestCommonsApplication::class)
@ -22,12 +22,13 @@ class MoreBottomSheetLoggedOutFragmentUnitTests {
@Before
fun setUp() {
scenario = launchFragmentInContainer(
initialState = Lifecycle.State.RESUMED,
themeResId = R.style.LightAppTheme
) {
MoreBottomSheetLoggedOutFragment()
}
scenario =
launchFragmentInContainer(
initialState = Lifecycle.State.RESUMED,
themeResId = R.style.LightAppTheme,
) {
MoreBottomSheetLoggedOutFragment()
}
}
@Test

View file

@ -11,21 +11,24 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class PasteSensitiveTextInputEditTextTest {
private var context: Context? = null
private var textView: PasteSensitiveTextInputEditText? = null
@Before
fun setup() {
context = ApplicationProvider.getApplicationContext()
textView = PasteSensitiveTextInputEditText(context)
textView = PasteSensitiveTextInputEditText(context!!)
}
// this test has no real value, just % for test code coverage
@Test
fun extractFormattingAttributeSet(){
val methodExtractFormattingAttribute = textView!!.javaClass.getDeclaredMethod(
"extractFormattingAttribute", Context::class.java, AttributeSet::class.java)
fun extractFormattingAttributeSet() {
val methodExtractFormattingAttribute =
textView!!.javaClass.getDeclaredMethod(
"extractFormattingAttribute",
Context::class.java,
AttributeSet::class.java,
)
methodExtractFormattingAttribute.isAccessible = true
methodExtractFormattingAttribute.invoke(textView, context, null)
}

View file

@ -9,56 +9,58 @@ import org.hamcrest.Matcher
class MyViewAction {
companion object {
fun typeTextInChildViewWithId(id: Int, textToBeTyped: String): ViewAction {
return object : ViewAction {
override fun getConstraints(): Matcher<View>? {
return null
}
fun typeTextInChildViewWithId(
id: Int,
textToBeTyped: String,
): ViewAction =
object : ViewAction {
override fun getConstraints(): Matcher<View>? = null
override fun getDescription(): String {
return "Click on a child view with specified id."
}
override fun getDescription(): String = "Click on a child view with specified id."
override fun perform(uiController: UiController, view: View) {
override fun perform(
uiController: UiController,
view: View,
) {
val v = view.findViewById<View>(id) as EditText
v.setText(textToBeTyped)
}
}
}
fun selectSpinnerItemInChildViewWithId(id: Int, position: Int): ViewAction {
return object : ViewAction {
override fun getConstraints(): Matcher<View>? {
return null
}
fun selectSpinnerItemInChildViewWithId(
id: Int,
position: Int,
): ViewAction =
object : ViewAction {
override fun getConstraints(): Matcher<View>? = null
override fun getDescription(): String {
return "Click on a child view with specified id."
}
override fun getDescription(): String = "Click on a child view with specified id."
override fun perform(uiController: UiController, view: View) {
override fun perform(
uiController: UiController,
view: View,
) {
val v = view.findViewById<View>(id) as AppCompatSpinner
v.setSelection(position)
}
}
}
fun clickItemWithId(id: Int, position: Int): ViewAction {
return object : ViewAction {
override fun getConstraints(): Matcher<View>? {
return null
}
fun clickItemWithId(
id: Int,
position: Int,
): ViewAction =
object : ViewAction {
override fun getConstraints(): Matcher<View>? = null
override fun getDescription(): String {
return "Click on a child view with specified id."
}
override fun getDescription(): String = "Click on a child view with specified id."
override fun perform(uiController: UiController, view: View) {
override fun perform(
uiController: UiController,
view: View,
) {
val v = view.findViewById<View>(id) as View
v.performClick()
}
}
}
}
}

View file

@ -1,262 +1,259 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.READ_SYNC_STATS" />
<uses-permission android:name="android.permission.REORDER_TASKS" />
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="com.google.android.apps.photos.permission.GOOGLE_PHOTOS" />
<uses-permission android:name="android.permission.SET_WALLPAPER"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"/>
xmlns:tools="http://schemas.android.com/tools">
<queries>
<!-- Browser -->
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
</intent>
<!-- Google Maps -->
<package android:name="com.google.android.apps.maps" />
</queries>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.READ_SYNC_STATS" />
<uses-permission android:name="android.permission.REORDER_TASKS" />
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
<!-- Permission needed up to Android 5.1, see https://github.com/commons-app/apps-android-commons/pull/5863 -->
<uses-permission android:name="android.permission.GET_ACCOUNTS"
android:maxSdkVersion="22"/>
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"
android:minSdkVersion="33"/>
<uses-permission android:name="com.google.android.apps.photos.permission.GOOGLE_PHOTOS" />
<uses-permission android:name="android.permission.SET_WALLPAPER" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"/>
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED"
android:minSdkVersion="34"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<queries>
<!-- Needed only if your app targets Android 5.0 (API level 21) or higher. -->
<uses-feature android:name="android.hardware.location.gps" />
<!-- Browser -->
<intent>
<action android:name="android.intent.action.VIEW" />
<application
android:name=".CommonsApplication"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/LightAppTheme"
android:largeHeap="true"
android:supportsRtl="true"
tools:replace="android:appComponentFactory"
android:appComponentFactory="commons"
android:requestLegacyExternalStorage = "true"
tools:ignore="GoogleAppIndexingWarning">
<category android:name="android.intent.category.BROWSABLE" />
<activity
android:theme="@style/EditActivityTheme"
android:name=".description.DescriptionEditActivity"
android:exported="true" />
<data android:scheme="https" />
</intent>
<!-- Google Maps -->
<package android:name="com.google.android.apps.maps" />
</queries> <!-- Needed only if your app targets Android 5.0 (API level 21) or higher. -->
<uses-feature android:name="android.hardware.location.gps" />
<activity
android:name=".edit.EditActivity"
android:exported="false" />
<application
android:name=".CommonsApplication"
android:appComponentFactory="commons"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:largeHeap="true"
android:requestLegacyExternalStorage="true"
android:supportsRtl="true"
android:theme="@style/LightAppTheme"
tools:ignore="GoogleAppIndexingWarning"
tools:replace="android:appComponentFactory">
<activity
android:name=".activity.SingleWebViewActivity"
android:exported="false" />
<activity
android:name=".nearby.WikidataFeedback"
android:exported="false" />
<activity
android:name=".upload.UploadProgressActivity"
android:exported="false" />
<activity
android:name=".description.DescriptionEditActivity"
android:exported="true"
android:theme="@style/EditActivityTheme" />
<activity
android:name=".edit.EditActivity"
android:exported="false" />
<activity
android:name="org.acra.dialog.CrashReportDialog"
android:excludeFromRecents="true"
android:finishOnTaskLaunch="true"
android:launchMode="singleInstance"
android:process=":acra" />
<activity
android:name=".media.ZoomableActivity"
android:configChanges="screenSize|keyboard|orientation"
android:label="Zoomable Activity"
android:parentActivityName=".customselector.ui.selector.CustomSelectorActivity" />
<activity
android:name=".auth.LoginActivity"
android:windowSoftInputMode="adjustPan"
android:exported="true">
<intent-filter>
<category android:name="android.intent.category.LAUNCHER" />
<activity android:name="org.acra.dialog.CrashReportDialog"
android:process=":acra"
android:launchMode="singleInstance"
android:excludeFromRecents="true"
android:finishOnTaskLaunch="true" />
<action android:name="android.intent.action.MAIN" />
</intent-filter>
<activity
android:name=".media.ZoomableActivity"
android:label="Zoomable Activity"
android:configChanges="screenSize|keyboard|orientation"
android:parentActivityName=".customselector.ui.selector.CustomSelectorActivity" />
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity>
<activity android:name=".WelcomeActivity" />
<activity
android:name=".upload.UploadActivity"
android:configChanges="orientation|screenSize|keyboard"
android:exported="true"
android:hardwareAccelerated="false"
android:icon="@mipmap/ic_launcher"
android:windowSoftInputMode="adjustPan">
<intent-filter android:label="@string/intent_share_upload_label">
<action android:name="android.intent.action.SEND" />
<activity android:name=".auth.LoginActivity"
android:exported="true">
<intent-filter>
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.DEFAULT" />
<action android:name="android.intent.action.MAIN" />
</intent-filter>
<data android:mimeType="image/*" />
<data android:mimeType="audio/ogg" />
</intent-filter>
<intent-filter android:label="@string/intent_share_upload_label">
<action android:name="android.intent.action.SEND_MULTIPLE" />
<meta-data android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
<category android:name="android.intent.category.DEFAULT" />
</activity>
<activity android:name=".WelcomeActivity" />
<data android:mimeType="image/*" />
<data android:mimeType="audio/ogg" />
</intent-filter>
</activity>
<activity
android:name=".contributions.MainActivity"
android:configChanges="screenSize|keyboard|orientation"
android:icon="@mipmap/ic_launcher"
/>
<activity
android:name=".settings.SettingsActivity"
android:label="@string/title_activity_settings" />
<activity
android:name=".AboutActivity"
android:label="@string/title_activity_about"
android:parentActivityName=".contributions.MainActivity" />
<activity
android:name=".auth.SignupActivity"
android:configChanges="orientation|screenLayout|screenSize"
android:label="@string/title_activity_signup" />
<activity
android:name=".notification.NotificationActivity"
android:label="@string/navigation_item_notification" />
<activity
android:name=".quiz.QuizActivity"
android:label="@string/quiz" />
<activity
android:name=".quiz.QuizResultActivity"
android:label="@string/result" />
<activity
android:name=".customselector.ui.selector.CustomSelectorActivity"
android:configChanges="screenSize|keyboard|orientation"
android:label="@string/title_activity_custom_selector"
android:parentActivityName=".contributions.MainActivity" />
<activity
android:name=".category.CategoryDetailsActivity"
android:configChanges="screenSize|keyboard|orientation"
android:label="@string/title_activity_featured_images"
android:parentActivityName=".contributions.MainActivity" />
<activity
android:name=".explore.depictions.WikidataItemDetailsActivity"
android:configChanges="screenSize|keyboard|orientation"
android:label="@string/title_activity_featured_images"
android:parentActivityName=".contributions.MainActivity" />
<activity
android:name=".explore.SearchActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:label="@string/title_activity_search"
android:launchMode="singleTop"
android:parentActivityName=".contributions.MainActivity" />
<activity
android:name=".profile.ProfileActivity"
android:configChanges="orientation|screenSize|keyboard"
android:label="@string/Profile" />
<activity
android:name=".review.ReviewActivity"
android:label="@string/title_activity_review" />
<activity
android:name=".locationpicker.LocationPickerActivity"
android:label="Location Picker" />
<activity
android:hardwareAccelerated="false"
android:name=".upload.UploadActivity"
android:exported="true"
android:configChanges="orientation|screenSize|keyboard"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:windowSoftInputMode="adjustResize"
>
<intent-filter android:label="@string/intent_share_upload_label">
<action android:name="android.intent.action.SEND" />
<service
android:name=".auth.WikiAccountAuthenticatorService"
android:exported="true"
android:process=":auth">
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator" />
</intent-filter>
<category android:name="android.intent.category.DEFAULT" />
<meta-data
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/authenticator" />
</service>
<service
android:name="org.acra.sender.SenderService"
android:exported="false"
android:process=":acra" />
<data android:mimeType="image/*" />
<data android:mimeType="audio/ogg" />
</intent-filter>
<intent-filter android:label="@string/intent_share_upload_label">
<action android:name="android.intent.action.SEND_MULTIPLE" />
<service
android:name="androidx.work.impl.foreground.SystemForegroundService"
android:foregroundServiceType="dataSync" />
<category android:name="android.intent.category.DEFAULT" />
<provider
android:name=".filepicker.ExtendedFileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
<provider
android:name=".category.CategoryContentProvider"
android:authorities="${applicationId}.categories.contentprovider"
android:exported="false"
android:label="@string/provider_categories"
android:syncable="false" />
<provider
android:name=".explore.recentsearches.RecentSearchesContentProvider"
android:authorities="${applicationId}.explore.recentsearches.contentprovider"
android:exported="false"
android:label="@string/provider_searches"
android:syncable="false" />
<provider
android:name=".recentlanguages.RecentLanguagesContentProvider"
android:authorities="${applicationId}.recentlanguages.contentprovider"
android:exported="false"
android:label="@string/provider_recent_languages"
android:syncable="false" />
<provider
android:name=".bookmarks.pictures.BookmarkPicturesContentProvider"
android:authorities="${applicationId}.bookmarks.contentprovider"
android:exported="false"
android:label="@string/provider_bookmarks"
android:syncable="false" />
<provider
android:name=".bookmarks.items.BookmarkItemsContentProvider"
android:authorities="${applicationId}.bookmarks.items.contentprovider"
android:exported="false"
android:label="@string/provider_bookmarks_location"
android:syncable="false" />
<data android:mimeType="image/*" />
<data android:mimeType="audio/ogg" />
</intent-filter>
</activity>
<activity
android:name=".contributions.MainActivity"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:configChanges="screenSize|keyboard|orientation" />
<activity
android:name=".settings.SettingsActivity"
android:label="@string/title_activity_settings" />
<activity
android:name=".AboutActivity"
android:label="@string/title_activity_about"
android:parentActivityName=".contributions.MainActivity" />
<receiver
android:name=".widget.PicOfDayAppWidget"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<activity
android:name=".auth.SignupActivity"
android:configChanges="orientation|screenLayout|screenSize"
android:label="@string/title_activity_signup" />
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/pic_of_day_app_widget_info" />
</receiver>
<activity
android:name=".notification.NotificationActivity"
android:label="@string/navigation_item_notification" />
<activity android:name=".quiz.QuizActivity"
android:label="@string/quiz"/>
<activity android:name=".quiz.QuizResultActivity"
android:label="@string/result"/>
<activity
android:name=".customselector.ui.selector.CustomSelectorActivity"
android:label="@string/title_activity_custom_selector"
android:configChanges="screenSize|keyboard|orientation"
android:parentActivityName=".contributions.MainActivity" />
<activity
android:name=".category.CategoryDetailsActivity"
android:label="@string/title_activity_featured_images"
android:configChanges="screenSize|keyboard|orientation"
android:parentActivityName=".contributions.MainActivity" />
<activity
android:name=".explore.depictions.WikidataItemDetailsActivity"
android:label="@string/title_activity_featured_images"
android:configChanges="screenSize|keyboard|orientation"
android:parentActivityName=".contributions.MainActivity" />
<activity
android:name=".explore.SearchActivity"
android:label="@string/title_activity_search"
android:launchMode="singleTop"
android:configChanges="orientation|keyboardHidden|screenSize"
android:parentActivityName=".contributions.MainActivity"
/>
<activity
android:name=".profile.ProfileActivity"
android:configChanges="orientation|screenSize|keyboard"
android:label="@string/Profile" />
<activity
android:name=".review.ReviewActivity"
android:label="@string/title_activity_review" />
<activity
android:name=".LocationPicker.LocationPickerActivity"
android:label="Location Picker" />
<service
android:name=".auth.WikiAccountAuthenticatorService"
android:exported="true"
android:process=":auth">
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator" />
</intent-filter>
<meta-data
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/authenticator" />
</service>
<service
android:name="org.acra.sender.SenderService"
android:exported="false"
android:process=":acra" />
<provider
android:name=".filepicker.ExtendedFileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
<provider
android:name=".category.CategoryContentProvider"
android:authorities="${applicationId}.categories.contentprovider"
android:exported="false"
android:label="@string/provider_categories"
android:syncable="false" />
<provider
android:name=".explore.recentsearches.RecentSearchesContentProvider"
android:authorities="${applicationId}.explore.recentsearches.contentprovider"
android:exported="false"
android:label="@string/provider_searches"
android:syncable="false" />
<provider
android:name=".recentlanguages.RecentLanguagesContentProvider"
android:authorities="${applicationId}.recentlanguages.contentprovider"
android:exported="false"
android:label="@string/provider_recent_languages"
android:syncable="false" />
<provider
android:name=".bookmarks.pictures.BookmarkPicturesContentProvider"
android:authorities="${applicationId}.bookmarks.contentprovider"
android:exported="false"
android:label="@string/provider_bookmarks"
android:syncable="false" />
<provider
android:name=".bookmarks.locations.BookmarkLocationsContentProvider"
android:authorities="${applicationId}.bookmarks.locations.contentprovider"
android:exported="false"
android:label="@string/provider_bookmarks_location"
android:syncable="false" />
<provider
android:name=".bookmarks.items.BookmarkItemsContentProvider"
android:authorities="${applicationId}.bookmarks.items.contentprovider"
android:exported="false"
android:label="@string/provider_bookmarks_location"
android:syncable="false" />
<receiver android:name=".widget.PicOfDayAppWidget"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/pic_of_day_app_widget_info" />
</receiver>
<uses-library android:name="org.apache.http.legacy" android:required="false" />
</application>
<uses-library
android:name="org.apache.http.legacy"
android:required="false" />
</application>
</manifest>

View file

@ -1,187 +0,0 @@
package fr.free.nrw.commons;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.LinearLayout;
import android.widget.Spinner;
import androidx.annotation.NonNull;
import fr.free.nrw.commons.databinding.ActivityAboutBinding;
import fr.free.nrw.commons.theme.BaseActivity;
import fr.free.nrw.commons.utils.ConfigUtils;
import fr.free.nrw.commons.utils.DialogUtil;
import java.util.Collections;
import java.util.List;
/**
* Represents about screen of this app
*/
public class AboutActivity extends BaseActivity {
/*
This View Binding class is auto-generated for each xml file. The format is usually the name
of the file with PascalCasing (The underscore characters will be ignored).
More information is available at https://developer.android.com/topic/libraries/view-binding
*/
private ActivityAboutBinding binding;
/**
* This method helps in the creation About screen
*
* @param savedInstanceState Data bundle
*/
@Override
@SuppressLint("StringFormatInvalid")
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
/*
Instead of just setting the view with the xml file. We need to use View Binding class.
*/
binding = ActivityAboutBinding.inflate(getLayoutInflater());
final View view = binding.getRoot();
setContentView(view);
setSupportActionBar(binding.toolbarBinding.toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
final String aboutText = getString(R.string.about_license);
/*
We can then access all the views by just using the id names like this.
camelCasing is used with underscore characters being ignored.
*/
binding.aboutLicense.setHtmlText(aboutText);
@SuppressLint("StringFormatMatches")
String improveText = String.format(getString(R.string.about_improve), Urls.NEW_ISSUE_URL);
binding.aboutImprove.setHtmlText(improveText);
binding.aboutVersion.setText(ConfigUtils.getVersionNameWithSha(getApplicationContext()));
Utils.setUnderlinedText(binding.aboutFaq, R.string.about_faq, getApplicationContext());
Utils.setUnderlinedText(binding.aboutRateUs, R.string.about_rate_us, getApplicationContext());
Utils.setUnderlinedText(binding.aboutUserGuide, R.string.user_guide, getApplicationContext());
Utils.setUnderlinedText(binding.aboutPrivacyPolicy, R.string.about_privacy_policy, getApplicationContext());
Utils.setUnderlinedText(binding.aboutTranslate, R.string.about_translate, getApplicationContext());
Utils.setUnderlinedText(binding.aboutCredits, R.string.about_credits, getApplicationContext());
/*
To set listeners, we can create a separate method and use lambda syntax.
*/
binding.facebookLaunchIcon.setOnClickListener(this::launchFacebook);
binding.githubLaunchIcon.setOnClickListener(this::launchGithub);
binding.websiteLaunchIcon.setOnClickListener(this::launchWebsite);
binding.aboutRateUs.setOnClickListener(this::launchRatings);
binding.aboutCredits.setOnClickListener(this::launchCredits);
binding.aboutPrivacyPolicy.setOnClickListener(this::launchPrivacyPolicy);
binding.aboutUserGuide.setOnClickListener(this::launchUserGuide);
binding.aboutFaq.setOnClickListener(this::launchFrequentlyAskedQuesions);
binding.aboutTranslate.setOnClickListener(this::launchTranslate);
}
@Override
public boolean onSupportNavigateUp() {
onBackPressed();
return true;
}
public void launchFacebook(View view) {
Intent intent;
try {
intent = new Intent(Intent.ACTION_VIEW, Uri.parse(Urls.FACEBOOK_APP_URL));
intent.setPackage(Urls.FACEBOOK_PACKAGE_NAME);
startActivity(intent);
} catch (Exception e) {
Utils.handleWebUrl(this, Uri.parse(Urls.FACEBOOK_WEB_URL));
}
}
public void launchGithub(View view) {
Intent intent;
try {
intent = new Intent(Intent.ACTION_VIEW, Uri.parse(Urls.GITHUB_REPO_URL));
intent.setPackage(Urls.GITHUB_PACKAGE_NAME);
startActivity(intent);
} catch (Exception e) {
Utils.handleWebUrl(this, Uri.parse(Urls.GITHUB_REPO_URL));
}
}
public void launchWebsite(View view) {
Utils.handleWebUrl(this, Uri.parse(Urls.WEBSITE_URL));
}
public void launchRatings(View view){
Utils.rateApp(this);
}
public void launchCredits(View view) {
Utils.handleWebUrl(this, Uri.parse(Urls.CREDITS_URL));
}
public void launchUserGuide(View view) {
Utils.handleWebUrl(this, Uri.parse(Urls.USER_GUIDE_URL));
}
public void launchPrivacyPolicy(View view) {
Utils.handleWebUrl(this, Uri.parse(BuildConfig.PRIVACY_POLICY_URL));
}
public void launchFrequentlyAskedQuesions(View view) {
Utils.handleWebUrl(this, Uri.parse(Urls.FAQ_URL));
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_about, menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.share_app_icon:
String shareText = String.format(getString(R.string.share_text), Urls.PLAY_STORE_URL_PREFIX + this.getPackageName());
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT, shareText);
sendIntent.setType("text/plain");
startActivity(Intent.createChooser(sendIntent, getString(R.string.share_via)));
return true;
default:
return super.onOptionsItemSelected(item);
}
}
public void launchTranslate(View view) {
@NonNull List<String> sortedLocalizedNamesRef = CommonsApplication.getInstance().getLanguageLookUpTable().getCanonicalNames();
Collections.sort(sortedLocalizedNamesRef);
final ArrayAdapter<String> languageAdapter = new ArrayAdapter<>(AboutActivity.this,
android.R.layout.simple_spinner_dropdown_item, sortedLocalizedNamesRef);
final Spinner spinner = new Spinner(AboutActivity.this);
spinner.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT));
spinner.setAdapter(languageAdapter);
spinner.setGravity(17);
spinner.setPadding(50,0,0,0);
Runnable positiveButtonRunnable = () -> {
String langCode = CommonsApplication.getInstance().getLanguageLookUpTable().getCodes().get(spinner.getSelectedItemPosition());
Utils.handleWebUrl(AboutActivity.this, Uri.parse(Urls.TRANSLATE_WIKI_URL + langCode));
};
DialogUtil.showAlertDialog(this,
getString(R.string.about_translate_title),
getString(R.string.about_translate_message),
getString(R.string.about_translate_proceed),
getString(R.string.about_translate_cancel),
positiveButtonRunnable,
() -> {},
spinner,
true);
}
}

View file

@ -0,0 +1,207 @@
package fr.free.nrw.commons
import android.annotation.SuppressLint
import android.content.ActivityNotFoundException
import android.content.Intent
import android.content.Intent.ACTION_VIEW
import android.net.Uri
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.ArrayAdapter
import android.widget.LinearLayout
import android.widget.Spinner
import fr.free.nrw.commons.CommonsApplication.Companion.instance
import fr.free.nrw.commons.databinding.ActivityAboutBinding
import fr.free.nrw.commons.theme.BaseActivity
import fr.free.nrw.commons.utils.ConfigUtils.getVersionNameWithSha
import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog
import java.util.Collections
import androidx.core.net.toUri
import fr.free.nrw.commons.utils.applyEdgeToEdgeTopInsets
import fr.free.nrw.commons.utils.handleWebUrl
import fr.free.nrw.commons.utils.setUnderlinedText
/**
* Represents about screen of this app
*/
class AboutActivity : BaseActivity() {
/*
This View Binding class is auto-generated for each xml file. The format is usually the name
of the file with PascalCasing (The underscore characters will be ignored).
More information is available at https://developer.android.com/topic/libraries/view-binding
*/
private var binding: ActivityAboutBinding? = null
/**
* This method helps in the creation About screen
*
* @param savedInstanceState Data bundle
*/
@SuppressLint("StringFormatInvalid") //TODO:
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
/*
Instead of just setting the view with the xml file. We need to use View Binding class.
*/
binding = ActivityAboutBinding.inflate(layoutInflater)
val view: View = binding!!.root
applyEdgeToEdgeTopInsets(binding!!.toolbarLayout)
setContentView(view)
setSupportActionBar(binding!!.toolbarBinding.toolbar)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
val aboutText = getString(R.string.about_license)
/*
We can then access all the views by just using the id names like this.
camelCasing is used with underscore characters being ignored.
*/
binding!!.aboutLicense.setHtmlText(aboutText)
@SuppressLint("StringFormatMatches") // TODO:
val improveText =
String.format(getString(R.string.about_improve), Urls.NEW_ISSUE_URL)
binding!!.aboutImprove.setHtmlText(improveText)
binding!!.aboutVersion.text = applicationContext.getVersionNameWithSha()
binding!!.aboutFaq.setUnderlinedText(R.string.about_faq)
binding!!.aboutRateUs.setUnderlinedText(R.string.about_rate_us)
binding!!.aboutUserGuide.setUnderlinedText(R.string.user_guide)
binding!!.aboutPrivacyPolicy.setUnderlinedText(R.string.about_privacy_policy)
binding!!.aboutTranslate.setUnderlinedText(R.string.about_translate)
binding!!.aboutCredits.setUnderlinedText(R.string.about_credits)
/*
To set listeners, we can create a separate method and use lambda syntax.
*/
binding!!.facebookLaunchIcon.setOnClickListener(::launchFacebook)
binding!!.githubLaunchIcon.setOnClickListener(::launchGithub)
binding!!.websiteLaunchIcon.setOnClickListener(::launchWebsite)
binding!!.aboutRateUs.setOnClickListener(::launchRatings)
binding!!.aboutCredits.setOnClickListener(::launchCredits)
binding!!.aboutPrivacyPolicy.setOnClickListener(::launchPrivacyPolicy)
binding!!.aboutUserGuide.setOnClickListener(::launchUserGuide)
binding!!.aboutFaq.setOnClickListener(::launchFrequentlyAskedQuesions)
binding!!.aboutTranslate.setOnClickListener(::launchTranslate)
}
override fun onSupportNavigateUp(): Boolean {
onBackPressed()
return true
}
fun launchFacebook(view: View?) {
val intent: Intent
try {
intent = Intent(ACTION_VIEW, Urls.FACEBOOK_APP_URL.toUri())
intent.setPackage(Urls.FACEBOOK_PACKAGE_NAME)
startActivity(intent)
} catch (e: Exception) {
handleWebUrl(this, Urls.FACEBOOK_WEB_URL.toUri())
}
}
fun launchGithub(view: View?) {
val intent: Intent
try {
intent = Intent(ACTION_VIEW, Urls.GITHUB_REPO_URL.toUri())
intent.setPackage(Urls.GITHUB_PACKAGE_NAME)
startActivity(intent)
} catch (e: Exception) {
handleWebUrl(this, Urls.GITHUB_REPO_URL.toUri())
}
}
fun launchWebsite(view: View?) {
handleWebUrl(this, Urls.WEBSITE_URL.toUri())
}
fun launchRatings(view: View?) {
try {
startActivity(
Intent(
ACTION_VIEW,
(Urls.PLAY_STORE_PREFIX + packageName).toUri()
)
)
} catch (_: ActivityNotFoundException) {
handleWebUrl(this, (Urls.PLAY_STORE_URL_PREFIX + packageName).toUri())
}
}
fun launchCredits(view: View?) {
handleWebUrl(this, Urls.CREDITS_URL.toUri())
}
fun launchUserGuide(view: View?) {
handleWebUrl(this, Urls.USER_GUIDE_URL.toUri())
}
fun launchPrivacyPolicy(view: View?) {
handleWebUrl(this, BuildConfig.PRIVACY_POLICY_URL.toUri())
}
fun launchFrequentlyAskedQuesions(view: View?) {
handleWebUrl(this, Urls.FAQ_URL.toUri())
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
val inflater = menuInflater
inflater.inflate(R.menu.menu_about, menu)
return super.onCreateOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.share_app_icon -> {
val shareText = String.format(
getString(R.string.share_text),
Urls.PLAY_STORE_URL_PREFIX + this.packageName
)
val sendIntent = Intent()
sendIntent.setAction(Intent.ACTION_SEND)
sendIntent.putExtra(Intent.EXTRA_TEXT, shareText)
sendIntent.setType("text/plain")
startActivity(Intent.createChooser(sendIntent, getString(R.string.share_via)))
return true
}
else -> return super.onOptionsItemSelected(item)
}
}
fun launchTranslate(view: View?) {
val sortedLocalizedNamesRef = instance.languageLookUpTable!!.getCanonicalNames()
Collections.sort(sortedLocalizedNamesRef)
val languageAdapter = ArrayAdapter(
this@AboutActivity,
android.R.layout.simple_spinner_dropdown_item, sortedLocalizedNamesRef
)
val spinner = Spinner(this@AboutActivity)
spinner.layoutParams =
LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
spinner.adapter = languageAdapter
spinner.gravity = 17
spinner.setPadding(50, 0, 0, 0)
val positiveButtonRunnable = Runnable {
val langCode = instance.languageLookUpTable!!.getCodes()[spinner.selectedItemPosition]
handleWebUrl(this@AboutActivity, (Urls.TRANSLATE_WIKI_URL + langCode).toUri())
}
showAlertDialog(
this,
getString(R.string.about_translate_title),
getString(R.string.about_translate_message),
getString(R.string.about_translate_proceed),
getString(R.string.about_translate_cancel),
positiveButtonRunnable,
{},
spinner
)
}
}

View file

@ -39,26 +39,25 @@ class BaseMarker {
constructor() {
}
fun fromResource(context: Context, drawableResId: Int) {
fun fromResource(
context: Context,
drawableResId: Int,
) {
val drawable: Drawable = context.resources.getDrawable(drawableResId)
icon = if (drawable is BitmapDrawable) {
(drawable as BitmapDrawable).bitmap
} else {
val bitmap = Bitmap.createBitmap(
drawable.intrinsicWidth,
drawable.intrinsicHeight, Bitmap.Config.ARGB_8888
)
val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, canvas.width, canvas.height)
drawable.draw(canvas)
bitmap
}
icon =
if (drawable is BitmapDrawable) {
drawable.bitmap
} else {
val bitmap =
Bitmap.createBitmap(
drawable.intrinsicWidth,
drawable.intrinsicHeight,
Bitmap.Config.ARGB_8888,
)
val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, canvas.width, canvas.height)
drawable.draw(canvas)
bitmap
}
}
}

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

@ -10,6 +10,7 @@ object BetaConstants {
* production server where beta server does not work
*/
const val COMMONS_URL = "https://commons.wikimedia.org/"
/**
* Commons production's depicts property which is used in beta for some specific GET calls on
* production server where beta server does not work

View file

@ -3,31 +3,31 @@ package fr.free.nrw.commons
import android.os.Parcel
import android.os.Parcelable
class CameraPosition(val latitude: Double, val longitude: Double, val zoom: Double) : Parcelable {
class CameraPosition(
val latitude: Double,
val longitude: Double,
val zoom: Double,
) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.readDouble(),
parcel.readDouble(),
parcel.readDouble()
parcel.readDouble(),
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
override fun writeToParcel(
parcel: Parcel,
flags: Int,
) {
parcel.writeDouble(latitude)
parcel.writeDouble(longitude)
parcel.writeDouble(zoom)
}
override fun describeContents(): Int {
return 0
}
override fun describeContents(): Int = 0
companion object CREATOR : Parcelable.Creator<CameraPosition> {
override fun createFromParcel(parcel: Parcel): CameraPosition {
return CameraPosition(parcel)
}
override fun createFromParcel(parcel: Parcel): CameraPosition = CameraPosition(parcel)
override fun newArray(size: Int): Array<CameraPosition?> {
return arrayOfNulls(size)
}
override fun newArray(size: Int): Array<CameraPosition?> = arrayOfNulls(size)
}
}

View file

@ -1,440 +0,0 @@
package fr.free.nrw.commons;
import static fr.free.nrw.commons.data.DBOpenHelper.CONTRIBUTIONS_TABLE;
import static org.acra.ReportField.ANDROID_VERSION;
import static org.acra.ReportField.APP_VERSION_CODE;
import static org.acra.ReportField.APP_VERSION_NAME;
import static org.acra.ReportField.PHONE_MODEL;
import static org.acra.ReportField.STACK_TRACE;
import static org.acra.ReportField.USER_COMMENT;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.content.Intent;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.os.Build;
import android.os.Process;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.multidex.MultiDexApplication;
import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.imagepipeline.core.ImagePipeline;
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.Table;
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao;
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao;
import fr.free.nrw.commons.category.CategoryDao;
import fr.free.nrw.commons.concurrency.BackgroundPoolExceptionHandler;
import fr.free.nrw.commons.concurrency.ThreadPoolService;
import fr.free.nrw.commons.contributions.ContributionDao;
import fr.free.nrw.commons.data.DBOpenHelper;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.language.AppLanguageLookUpTable;
import fr.free.nrw.commons.logging.FileLoggingTree;
import fr.free.nrw.commons.logging.LogUtils;
import fr.free.nrw.commons.media.CustomOkHttpNetworkFetcher;
import fr.free.nrw.commons.settings.Prefs;
import fr.free.nrw.commons.upload.FileUtils;
import fr.free.nrw.commons.utils.ConfigUtils;
import fr.free.nrw.commons.wikidata.cookies.CommonsCookieJar;
import io.reactivex.Completable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.internal.functions.Functions;
import io.reactivex.plugins.RxJavaPlugins;
import io.reactivex.schedulers.Schedulers;
import java.io.File;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Named;
import org.acra.ACRA;
import org.acra.annotation.AcraCore;
import org.acra.annotation.AcraDialog;
import org.acra.annotation.AcraMailSender;
import org.acra.data.StringFormat;
import timber.log.Timber;
@AcraCore(
buildConfigClass = BuildConfig.class,
resReportSendSuccessToast = R.string.crash_dialog_ok_toast,
reportFormat = StringFormat.KEY_VALUE_LIST,
reportContent = {USER_COMMENT, APP_VERSION_CODE, APP_VERSION_NAME, ANDROID_VERSION, PHONE_MODEL,
STACK_TRACE}
)
@AcraMailSender(
mailTo = "commons-app-android-private@googlegroups.com",
reportAsFile = false
)
@AcraDialog(
resTheme = R.style.Theme_AppCompat_Dialog,
resText = R.string.crash_dialog_text,
resTitle = R.string.crash_dialog_title,
resCommentPrompt = R.string.crash_dialog_comment_prompt
)
public class CommonsApplication extends MultiDexApplication {
public static final String loginMessageIntentKey = "loginMessage";
public static final String loginUsernameIntentKey = "loginUsername";
public static final String IS_LIMITED_CONNECTION_MODE_ENABLED = "is_limited_connection_mode_enabled";
@Inject
SessionManager sessionManager;
@Inject
DBOpenHelper dbOpenHelper;
@Inject
@Named("default_preferences")
JsonKvStore defaultPrefs;
@Inject
CommonsCookieJar cookieJar;
@Inject
CustomOkHttpNetworkFetcher customOkHttpNetworkFetcher;
/**
* Constants begin
*/
public static final int OPEN_APPLICATION_DETAIL_SETTINGS = 1001;
public static final String DEFAULT_EDIT_SUMMARY = "Uploaded using [[COM:MOA|Commons Mobile App]]";
public static final String FEEDBACK_EMAIL = "commons-app-android@googlegroups.com";
public static final String FEEDBACK_EMAIL_SUBJECT = "Commons Android App Feedback";
public static final String REPORT_EMAIL = "commons-app-android-private@googlegroups.com";
public static final String REPORT_EMAIL_SUBJECT = "Report a violation";
public static final String NOTIFICATION_CHANNEL_ID_ALL = "CommonsNotificationAll";
public static final String FEEDBACK_EMAIL_TEMPLATE_HEADER = "-- Technical information --";
/**
* Constants End
*/
private static CommonsApplication INSTANCE;
public static CommonsApplication getInstance() {
return INSTANCE;
}
private AppLanguageLookUpTable languageLookUpTable;
public AppLanguageLookUpTable getLanguageLookUpTable() {
return languageLookUpTable;
}
@Inject
ContributionDao contributionDao;
/**
* In-memory list of contributions whose uploads have been paused by the user
*/
public static Map<String, Boolean> pauseUploads = new HashMap<>();
/**
* In-memory list of uploads that have been cancelled by the user
*/
public static HashSet<String> cancelledUploads = new HashSet<>();
/**
* Used to declare and initialize various components and dependencies
*/
@Override
public void onCreate() {
super.onCreate();
INSTANCE = this;
ACRA.init(this);
ApplicationlessInjection
.getInstance(this)
.getCommonsApplicationComponent()
.inject(this);
initTimber();
if (!defaultPrefs.getBoolean("has_user_manually_removed_location")) {
Set<String> defaultExifTagsSet = defaultPrefs.getStringSet(Prefs.MANAGED_EXIF_TAGS);
if (null == defaultExifTagsSet) {
defaultExifTagsSet = new HashSet<>();
}
defaultExifTagsSet.add(getString(R.string.exif_tag_location));
defaultPrefs.putStringSet(Prefs.MANAGED_EXIF_TAGS, defaultExifTagsSet);
}
// Set DownsampleEnabled to True to downsample the image in case it's heavy
ImagePipelineConfig config = ImagePipelineConfig.newBuilder(this)
.setNetworkFetcher(customOkHttpNetworkFetcher)
.setDownsampleEnabled(true)
.build();
try {
Fresco.initialize(this, config);
} catch (Exception e) {
Timber.e(e);
// TODO: Remove when we're able to initialize Fresco in test builds.
}
createNotificationChannel(this);
languageLookUpTable = new AppLanguageLookUpTable(this);
// This handler will catch exceptions thrown from Observables after they are disposed,
// or from Observables that are (deliberately or not) missing an onError handler.
RxJavaPlugins.setErrorHandler(Functions.emptyConsumer());
// Fire progress callbacks for every 3% of uploaded content
System.setProperty("in.yuvi.http.fluent.PROGRESS_TRIGGER_THRESHOLD", "3.0");
}
/**
* Plants debug and file logging tree. Timber lets you plant your own logging trees.
*/
private void initTimber() {
boolean isBeta = ConfigUtils.isBetaFlavour();
String logFileName =
isBeta ? "CommonsBetaAppLogs" : "CommonsAppLogs";
String logDirectory = LogUtils.getLogDirectory();
//Delete stale logs if they have exceeded the specified size
deleteStaleLogs(logFileName, logDirectory);
FileLoggingTree tree = new FileLoggingTree(
Log.VERBOSE,
logFileName,
logDirectory,
1000,
getFileLoggingThreadPool());
Timber.plant(tree);
Timber.plant(new Timber.DebugTree());
}
/**
* Deletes the logs zip file at the specified directory and file locations specified in the
* params
*
* @param logFileName
* @param logDirectory
*/
private void deleteStaleLogs(String logFileName, String logDirectory) {
try {
File file = new File(logDirectory + "/zip/" + logFileName + ".zip");
if (file.exists() && file.getTotalSpace() > 1000000) {// In Kbs
file.delete();
}
} catch (Exception e) {
Timber.e(e);
}
}
public static boolean isRoboUnitTest() {
return "robolectric".equals(Build.FINGERPRINT);
}
private ThreadPoolService getFileLoggingThreadPool() {
return new ThreadPoolService.Builder("file-logging-thread")
.setPriority(Process.THREAD_PRIORITY_LOWEST)
.setPoolSize(1)
.setExceptionHandler(new BackgroundPoolExceptionHandler())
.build();
}
public static void createNotificationChannel(@NonNull Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationManager manager = (NotificationManager) context
.getSystemService(Context.NOTIFICATION_SERVICE);
NotificationChannel channel = manager
.getNotificationChannel(NOTIFICATION_CHANNEL_ID_ALL);
if (channel == null) {
channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID_ALL,
context.getString(R.string.notifications_channel_name_all),
NotificationManager.IMPORTANCE_DEFAULT);
manager.createNotificationChannel(channel);
}
}
}
public String getUserAgent() {
return "Commons/" + ConfigUtils.getVersionNameWithSha(this)
+ " (https://mediawiki.org/wiki/Apps/Commons) Android/" + Build.VERSION.RELEASE;
}
/**
* clears data of current application
*
* @param context Application context
* @param logoutListener Implementation of interface LogoutListener
*/
@SuppressLint("CheckResult")
public void clearApplicationData(Context context, LogoutListener logoutListener) {
File cacheDirectory = context.getCacheDir();
File applicationDirectory = new File(cacheDirectory.getParent());
if (applicationDirectory.exists()) {
String[] fileNames = applicationDirectory.list();
for (String fileName : fileNames) {
if (!fileName.equals("lib")) {
FileUtils.deleteFile(new File(applicationDirectory, fileName));
}
}
}
sessionManager.logout()
.andThen(Completable.fromAction(() -> {
Timber.d("All accounts have been removed");
clearImageCache();
//TODO: fix preference manager
defaultPrefs.clearAll();
defaultPrefs.putBoolean("firstrun", false);
updateAllDatabases();
}
))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(logoutListener::onLogoutComplete, Timber::e);
}
/**
* Clear all images cache held by Fresco
*/
private void clearImageCache() {
ImagePipeline imagePipeline = Fresco.getImagePipeline();
imagePipeline.clearCaches();
}
/**
* Deletes all tables and re-creates them.
*/
private void updateAllDatabases() {
dbOpenHelper.getReadableDatabase().close();
SQLiteDatabase db = dbOpenHelper.getWritableDatabase();
CategoryDao.Table.onDelete(db);
dbOpenHelper.deleteTable(db,
CONTRIBUTIONS_TABLE);//Delete the contributions table in the existing db on older versions
try {
contributionDao.deleteAll();
} catch (SQLiteException e) {
Timber.e(e);
}
BookmarkPicturesDao.Table.onDelete(db);
BookmarkLocationsDao.Table.onDelete(db);
Table.onDelete(db);
}
/**
* Interface used to get log-out events
*/
public interface LogoutListener {
void onLogoutComplete();
}
/**
* This listener is responsible for handling post-logout actions, specifically invoking the LoginActivity
* with relevant intent parameters. It does not perform the actual logout operation.
*/
public static class BaseLogoutListener implements CommonsApplication.LogoutListener {
Context ctx;
String loginMessage, userName;
/**
* Constructor for BaseLogoutListener.
*
* @param ctx Application context
*/
public BaseLogoutListener(final Context ctx) {
this.ctx = ctx;
}
/**
* Constructor for BaseLogoutListener
*
* @param ctx The application context, used for invoking the LoginActivity and passing relevant intent parameters as part of the post-logout process.
* @param loginMessage Message to be displayed on the login page
* @param loginUsername Username to be pre-filled on the login page
*/
public BaseLogoutListener(final Context ctx, final String loginMessage,
final String loginUsername) {
this.ctx = ctx;
this.loginMessage = loginMessage;
this.userName = loginUsername;
}
@Override
public void onLogoutComplete() {
Timber.d("Logout complete callback received.");
final Intent loginIntent = new Intent(ctx, LoginActivity.class);
loginIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (loginMessage != null) {
loginIntent.putExtra(loginMessageIntentKey, loginMessage);
}
if (userName != null) {
loginIntent.putExtra(loginUsernameIntentKey, userName);
}
ctx.startActivity(loginIntent);
}
}
/**
* This class is an extension of BaseLogoutListener, providing additional functionality or customization
* for the logout process. It includes specific actions to be taken during logout, such as handling redirection to the login screen.
*/
public static class ActivityLogoutListener extends BaseLogoutListener {
Activity activity;
/**
* Constructor for ActivityLogoutListener.
*
* @param activity The activity context from which the logout is initiated. Used to perform actions such as finishing the activity.
* @param ctx The application context, used for invoking the LoginActivity and passing relevant intent parameters as part of the post-logout process.
*/
public ActivityLogoutListener(final Activity activity, final Context ctx) {
super(ctx);
this.activity = activity;
}
/**
* Constructor for ActivityLogoutListener with additional parameters for the login screen.
*
* @param activity The activity context from which the logout is initiated. Used to perform actions such as finishing the activity.
* @param ctx The application context, used for invoking the LoginActivity and passing relevant intent parameters as part of the post-logout process.
* @param loginMessage Message to be displayed on the login page after logout.
* @param loginUsername Username to be pre-filled on the login page after logout.
*/
public ActivityLogoutListener(final Activity activity, final Context ctx,
final String loginMessage, final String loginUsername) {
super(activity, loginMessage, loginUsername);
this.activity = activity;
}
@Override
public void onLogoutComplete() {
super.onLogoutComplete();
activity.finish();
}
}
}

View file

@ -0,0 +1,417 @@
package fr.free.nrw.commons
import android.annotation.SuppressLint
import android.app.Activity
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import android.database.sqlite.SQLiteException
import android.os.Build
import android.os.Process
import android.util.Log
import androidx.multidex.MultiDexApplication
import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.imagepipeline.core.ImagePipelineConfig
import fr.free.nrw.commons.auth.LoginActivity
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable
import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable
import fr.free.nrw.commons.category.CategoryDao
import fr.free.nrw.commons.concurrency.BackgroundPoolExceptionHandler
import fr.free.nrw.commons.concurrency.ThreadPoolService
import fr.free.nrw.commons.contributions.ContributionDao
import fr.free.nrw.commons.data.DBOpenHelper
import fr.free.nrw.commons.di.ApplicationlessInjection
import fr.free.nrw.commons.kvstore.JsonKvStore
import fr.free.nrw.commons.language.AppLanguageLookUpTable
import fr.free.nrw.commons.logging.FileLoggingTree
import fr.free.nrw.commons.logging.LogUtils
import fr.free.nrw.commons.media.CustomOkHttpNetworkFetcher
import fr.free.nrw.commons.settings.Prefs
import fr.free.nrw.commons.upload.FileUtils
import fr.free.nrw.commons.utils.ConfigUtils.getVersionNameWithSha
import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour
import fr.free.nrw.commons.wikidata.cookies.CommonsCookieJar
import io.reactivex.Completable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.internal.functions.Functions
import io.reactivex.plugins.RxJavaPlugins
import io.reactivex.schedulers.Schedulers
import org.acra.ACRA.init
import org.acra.ReportField
import org.acra.annotation.AcraCore
import org.acra.annotation.AcraDialog
import org.acra.annotation.AcraMailSender
import org.acra.data.StringFormat
import timber.log.Timber
import timber.log.Timber.DebugTree
import java.io.File
import javax.inject.Inject
import javax.inject.Named
@AcraCore(
buildConfigClass = BuildConfig::class,
resReportSendSuccessToast = R.string.crash_dialog_ok_toast,
reportFormat = StringFormat.KEY_VALUE_LIST,
reportContent = [ReportField.USER_COMMENT, ReportField.APP_VERSION_CODE, ReportField.APP_VERSION_NAME, ReportField.ANDROID_VERSION, ReportField.PHONE_MODEL, ReportField.STACK_TRACE]
)
@AcraMailSender(mailTo = "commons-app-android-private@googlegroups.com", reportAsFile = false)
@AcraDialog(
resTheme = R.style.Theme_AppCompat_Dialog,
resText = R.string.crash_dialog_text,
resTitle = R.string.crash_dialog_title,
resCommentPrompt = R.string.crash_dialog_comment_prompt
)
class CommonsApplication : MultiDexApplication() {
@Inject
lateinit var sessionManager: SessionManager
@Inject
lateinit var dbOpenHelper: DBOpenHelper
@Inject
@field:Named("default_preferences")
lateinit var defaultPrefs: JsonKvStore
@Inject
lateinit var cookieJar: CommonsCookieJar
@Inject
lateinit var customOkHttpNetworkFetcher: CustomOkHttpNetworkFetcher
var languageLookUpTable: AppLanguageLookUpTable? = null
private set
@Inject
lateinit var contributionDao: ContributionDao
/**
* Used to declare and initialize various components and dependencies
*/
override fun onCreate() {
super.onCreate()
instance = this
init(this)
ApplicationlessInjection
.getInstance(this)
.commonsApplicationComponent
.inject(this)
initTimber()
if (!defaultPrefs.getBoolean("has_user_manually_removed_location")) {
var defaultExifTagsSet = defaultPrefs.getStringSet(Prefs.MANAGED_EXIF_TAGS)
if (null == defaultExifTagsSet) {
defaultExifTagsSet = HashSet()
}
defaultExifTagsSet.add(getString(R.string.exif_tag_location))
defaultPrefs.putStringSet(Prefs.MANAGED_EXIF_TAGS, defaultExifTagsSet)
}
// Set DownsampleEnabled to True to downsample the image in case it's heavy
val config = ImagePipelineConfig.newBuilder(this)
.setNetworkFetcher(customOkHttpNetworkFetcher)
.setDownsampleEnabled(true)
.build()
try {
Fresco.initialize(this, config)
} catch (e: Exception) {
Timber.e(e)
// TODO: Remove when we're able to initialize Fresco in test builds.
}
createNotificationChannel(this)
languageLookUpTable = AppLanguageLookUpTable(this)
// This handler will catch exceptions thrown from Observables after they are disposed,
// or from Observables that are (deliberately or not) missing an onError handler.
RxJavaPlugins.setErrorHandler(Functions.emptyConsumer())
// Fire progress callbacks for every 3% of uploaded content
System.setProperty("in.yuvi.http.fluent.PROGRESS_TRIGGER_THRESHOLD", "3.0")
}
/**
* Plants debug and file logging tree. Timber lets you plant your own logging trees.
*/
private fun initTimber() {
val isBeta = isBetaFlavour
val logFileName =
if (isBeta) "CommonsBetaAppLogs" else "CommonsAppLogs"
val logDirectory = LogUtils.getLogDirectory()
//Delete stale logs if they have exceeded the specified size
deleteStaleLogs(logFileName, logDirectory)
val tree = FileLoggingTree(
Log.VERBOSE,
logFileName,
logDirectory,
1000,
fileLoggingThreadPool
)
Timber.plant(tree)
Timber.plant(DebugTree())
}
/**
* Deletes the logs zip file at the specified directory and file locations specified in the
* params
*
* @param logFileName
* @param logDirectory
*/
private fun deleteStaleLogs(logFileName: String, logDirectory: String) {
try {
val file = File("$logDirectory/zip/$logFileName.zip")
if (file.exists() && file.totalSpace > 1000000) { // In Kbs
file.delete()
}
} catch (e: Exception) {
Timber.e(e)
}
}
private val fileLoggingThreadPool: ThreadPoolService
get() = ThreadPoolService.Builder("file-logging-thread")
.setPriority(Process.THREAD_PRIORITY_LOWEST)
.setPoolSize(1)
.setExceptionHandler(BackgroundPoolExceptionHandler())
.build()
val userAgent: String
get() = ("Commons/" + this.getVersionNameWithSha()
+ " (https://mediawiki.org/wiki/Apps/Commons) Android/" + Build.VERSION.RELEASE)
/**
* clears data of current application
*
* @param context Application context
* @param logoutListener Implementation of interface LogoutListener
*/
@SuppressLint("CheckResult")
fun clearApplicationData(context: Context, logoutListener: LogoutListener) {
val cacheDirectory = context.cacheDir
val applicationDirectory = File(cacheDirectory.parent)
if (applicationDirectory.exists()) {
val fileNames = applicationDirectory.list()
for (fileName in fileNames) {
if (fileName != "lib") {
FileUtils.deleteFile(File(applicationDirectory, fileName))
}
}
}
sessionManager.logout()
.andThen(Completable.fromAction { cookieJar.clear() })
.andThen(Completable.fromAction {
Timber.d("All accounts have been removed")
clearImageCache()
//TODO: fix preference manager
defaultPrefs.clearAll()
defaultPrefs.putBoolean("firstrun", false)
updateAllDatabases()
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ logoutListener.onLogoutComplete() }, { t: Throwable? -> Timber.e(t) })
}
/**
* Clear all images cache held by Fresco
*/
private fun clearImageCache() {
val imagePipeline = Fresco.getImagePipeline()
imagePipeline.clearCaches()
}
/**
* Deletes all tables and re-creates them.
*/
private fun updateAllDatabases() {
dbOpenHelper.readableDatabase.close()
val db = dbOpenHelper.writableDatabase
CategoryDao.Table.onDelete(db)
dbOpenHelper.deleteTable(
db,
DBOpenHelper.CONTRIBUTIONS_TABLE
) //Delete the contributions table in the existing db on older versions
dbOpenHelper.deleteTable(
db,
DBOpenHelper.BOOKMARKS_LOCATIONS
)
try {
contributionDao.deleteAll()
} catch (e: SQLiteException) {
Timber.e(e)
}
BookmarksTable.onDelete(db)
BookmarkItemsTable.onDelete(db)
}
/**
* Interface used to get log-out events
*/
interface LogoutListener {
fun onLogoutComplete()
}
/**
* This listener is responsible for handling post-logout actions, specifically invoking the LoginActivity
* with relevant intent parameters. It does not perform the actual logout operation.
*/
open class BaseLogoutListener : LogoutListener {
var ctx: Context
var loginMessage: String? = null
var userName: String? = null
/**
* Constructor for BaseLogoutListener.
*
* @param ctx Application context
*/
constructor(ctx: Context) {
this.ctx = ctx
}
/**
* Constructor for BaseLogoutListener
*
* @param ctx The application context, used for invoking the LoginActivity and passing relevant intent parameters as part of the post-logout process.
* @param loginMessage Message to be displayed on the login page
* @param loginUsername Username to be pre-filled on the login page
*/
constructor(
ctx: Context, loginMessage: String?,
loginUsername: String?
) {
this.ctx = ctx
this.loginMessage = loginMessage
this.userName = loginUsername
}
override fun onLogoutComplete() {
Timber.d("Logout complete callback received.")
val loginIntent = Intent(ctx, LoginActivity::class.java)
loginIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
if (loginMessage != null) {
loginIntent.putExtra(LOGIN_MESSAGE_INTENT_KEY, loginMessage)
}
if (userName != null) {
loginIntent.putExtra(LOGIN_USERNAME_INTENT_KEY, userName)
}
ctx.startActivity(loginIntent)
}
}
/**
* This class is an extension of BaseLogoutListener, providing additional functionality or customization
* for the logout process. It includes specific actions to be taken during logout, such as handling redirection to the login screen.
*/
class ActivityLogoutListener : BaseLogoutListener {
var activity: Activity
/**
* Constructor for ActivityLogoutListener.
*
* @param activity The activity context from which the logout is initiated. Used to perform actions such as finishing the activity.
* @param ctx The application context, used for invoking the LoginActivity and passing relevant intent parameters as part of the post-logout process.
*/
constructor(activity: Activity, ctx: Context) : super(ctx) {
this.activity = activity
}
/**
* Constructor for ActivityLogoutListener with additional parameters for the login screen.
*
* @param activity The activity context from which the logout is initiated. Used to perform actions such as finishing the activity.
* @param ctx The application context, used for invoking the LoginActivity and passing relevant intent parameters as part of the post-logout process.
* @param loginMessage Message to be displayed on the login page after logout.
* @param loginUsername Username to be pre-filled on the login page after logout.
*/
constructor(
activity: Activity, ctx: Context?,
loginMessage: String?, loginUsername: String?
) : super(activity, loginMessage, loginUsername) {
this.activity = activity
}
override fun onLogoutComplete() {
super.onLogoutComplete()
activity.finish()
}
}
companion object {
const val LOGIN_MESSAGE_INTENT_KEY: String = "loginMessage"
const val LOGIN_USERNAME_INTENT_KEY: String = "loginUsername"
const val IS_LIMITED_CONNECTION_MODE_ENABLED: String = "is_limited_connection_mode_enabled"
/**
* Constants begin
*/
const val OPEN_APPLICATION_DETAIL_SETTINGS: Int = 1001
const val DEFAULT_EDIT_SUMMARY: String = "Uploaded using [[COM:MOA|Commons Mobile App]]"
const val FEEDBACK_EMAIL: String = "commons-app-android@googlegroups.com"
const val FEEDBACK_EMAIL_SUBJECT: String = "Commons Android App Feedback"
const val REPORT_EMAIL: String = "commons-app-android-private@googlegroups.com"
const val REPORT_EMAIL_SUBJECT: String = "Report a violation"
const val NOTIFICATION_CHANNEL_ID_ALL: String = "CommonsNotificationAll"
const val FEEDBACK_EMAIL_TEMPLATE_HEADER: String = "-- Technical information --"
/**
* Constants End
*/
@JvmStatic
lateinit var instance: CommonsApplication
private set
@JvmField
var isPaused: Boolean = false
@JvmStatic
fun createNotificationChannel(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = context
.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
var channel = manager
.getNotificationChannel(NOTIFICATION_CHANNEL_ID_ALL)
if (channel == null) {
channel = NotificationChannel(
NOTIFICATION_CHANNEL_ID_ALL,
context.getString(R.string.notifications_channel_name_all),
NotificationManager.IMPORTANCE_DEFAULT
)
manager.createNotificationChannel(channel)
}
}
}
}
}

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,77 +0,0 @@
package fr.free.nrw.commons.LocationPicker;
import android.app.Activity;
import android.content.Intent;
import fr.free.nrw.commons.CameraPosition;
import fr.free.nrw.commons.Media;
/**
* Helper class for starting the activity
*/
public final class LocationPicker {
/**
* Getting camera position from the intent using constants
*
* @param data intent
* @return CameraPosition
*/
public static CameraPosition getCameraPosition(final Intent data) {
return data.getParcelableExtra(LocationPickerConstants.MAP_CAMERA_POSITION);
}
public static class IntentBuilder {
private final Intent intent;
/**
* Creates a new builder that creates an intent to launch the place picker activity.
*/
public IntentBuilder() {
intent = new Intent();
}
/**
* Gets and puts location in intent
* @param position CameraPosition
* @return LocationPicker.IntentBuilder
*/
public LocationPicker.IntentBuilder defaultLocation(
final CameraPosition position) {
intent.putExtra(LocationPickerConstants.MAP_CAMERA_POSITION, position);
return this;
}
/**
* Gets and puts activity name in intent
* @param activity activity key
* @return LocationPicker.IntentBuilder
*/
public LocationPicker.IntentBuilder activityKey(
final String activity) {
intent.putExtra(LocationPickerConstants.ACTIVITY_KEY, activity);
return this;
}
/**
* Gets and puts media in intent
* @param media Media
* @return LocationPicker.IntentBuilder
*/
public LocationPicker.IntentBuilder media(
final Media media) {
intent.putExtra(LocationPickerConstants.MEDIA, media);
return this;
}
/**
* Gets and sets the activity
* @param activity Activity
* @return Intent
*/
public Intent build(final Activity activity) {
intent.setClass(activity, LocationPickerActivity.class);
return intent;
}
}
}

View file

@ -1,623 +0,0 @@
package fr.free.nrw.commons.LocationPicker;
import static fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.LAST_LOCATION;
import static fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.LAST_ZOOM;
import static fr.free.nrw.commons.utils.MapUtils.ZOOM_LEVEL;
import android.Manifest.permission;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.location.LocationManager;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.text.Html;
import android.text.method.LinkMovementMethod;
import android.view.MotionEvent;
import android.view.View;
import android.view.Window;
import android.view.animation.OvershootInterpolator;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.AppCompatTextView;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import fr.free.nrw.commons.CameraPosition;
import fr.free.nrw.commons.CommonsApplication;
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.auth.csrf.CsrfTokenClient;
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException;
import fr.free.nrw.commons.coordinates.CoordinateEditHelper;
import fr.free.nrw.commons.filepicker.Constants;
import fr.free.nrw.commons.kvstore.BasicKvStore;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.location.LocationPermissionsHelper;
import fr.free.nrw.commons.location.LocationPermissionsHelper.LocationPermissionCallback;
import fr.free.nrw.commons.location.LocationServiceManager;
import fr.free.nrw.commons.theme.BaseActivity;
import fr.free.nrw.commons.utils.DialogUtil;
import fr.free.nrw.commons.utils.SystemThemeUtils;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import org.osmdroid.tileprovider.tilesource.TileSourceFactory;
import org.osmdroid.util.GeoPoint;
import org.osmdroid.util.constants.GeoConstants;
import org.osmdroid.views.CustomZoomButtonsController;
import org.osmdroid.views.overlay.Marker;
import org.osmdroid.views.overlay.Overlay;
import org.osmdroid.views.overlay.ScaleDiskOverlay;
import org.osmdroid.views.overlay.TilesOverlay;
import timber.log.Timber;
/**
* Helps to pick location and return the result with an intent
*/
public class LocationPickerActivity extends BaseActivity implements
LocationPermissionCallback {
/**
* coordinateEditHelper: helps to edit coordinates
*/
@Inject
CoordinateEditHelper coordinateEditHelper;
/**
* media : Media object
*/
private Media media;
/**
* cameraPosition : position of picker
*/
private CameraPosition cameraPosition;
/**
* markerImage : picker image
*/
private ImageView markerImage;
/**
* mapView : OSM Map
*/
private org.osmdroid.views.MapView mapView;
/**
* tvAttribution : credit
*/
private AppCompatTextView tvAttribution;
/**
* activity : activity key
*/
private String activity;
/**
* modifyLocationButton : button for start editing location
*/
Button modifyLocationButton;
/**
* removeLocationButton : button to remove location metadata
*/
Button removeLocationButton;
/**
* showInMapButton : button for showing in map
*/
TextView showInMapButton;
/**
* placeSelectedButton : fab for selecting location
*/
FloatingActionButton placeSelectedButton;
/**
* fabCenterOnLocation: button for center on location;
*/
FloatingActionButton fabCenterOnLocation;
/**
* shadow : imageview of shadow
*/
private ImageView shadow;
/**
* largeToolbarText : textView of shadow
*/
private TextView largeToolbarText;
/**
* smallToolbarText : textView of shadow
*/
private TextView smallToolbarText;
/**
* applicationKvStore : for storing values
*/
@Inject
@Named("default_preferences")
public
JsonKvStore applicationKvStore;
BasicKvStore store;
/**
* isDarkTheme: for keeping a track of the device theme and modifying the map theme accordingly
*/
@Inject
SystemThemeUtils systemThemeUtils;
private boolean isDarkTheme;
private boolean moveToCurrentLocation;
@Inject
LocationServiceManager locationManager;
LocationPermissionsHelper locationPermissionsHelper;
@Inject
SessionManager sessionManager;
/**
* Constants
*/
private static final String CAMERA_POS = "cameraPosition";
private static final String ACTIVITY = "activity";
@SuppressLint("ClickableViewAccessibility")
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState) {
getWindow().requestFeature(Window.FEATURE_ACTION_BAR);
super.onCreate(savedInstanceState);
isDarkTheme = systemThemeUtils.isDeviceInNightMode();
moveToCurrentLocation = false;
store = new BasicKvStore(this, "LocationPermissions");
getWindow().requestFeature(Window.FEATURE_ACTION_BAR);
final ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.hide();
}
setContentView(R.layout.activity_location_picker);
if (savedInstanceState == null) {
cameraPosition = getIntent()
.getParcelableExtra(LocationPickerConstants.MAP_CAMERA_POSITION);
activity = getIntent().getStringExtra(LocationPickerConstants.ACTIVITY_KEY);
media = getIntent().getParcelableExtra(LocationPickerConstants.MEDIA);
}else{
cameraPosition = savedInstanceState.getParcelable(CAMERA_POS);
activity = savedInstanceState.getString(ACTIVITY);
media = savedInstanceState.getParcelable("sMedia");
}
bindViews();
addBackButtonListener();
addPlaceSelectedButton();
addCredits();
getToolbarUI();
addCenterOnGPSButton();
org.osmdroid.config.Configuration.getInstance().load(getApplicationContext(),
PreferenceManager.getDefaultSharedPreferences(getApplicationContext()));
mapView.setTileSource(TileSourceFactory.WIKIMEDIA);
mapView.setTilesScaledToDpi(true);
mapView.setMultiTouchControls(true);
org.osmdroid.config.Configuration.getInstance().getAdditionalHttpRequestProperties().put(
"Referer", "http://maps.wikimedia.org/"
);
mapView.getZoomController().setVisibility(CustomZoomButtonsController.Visibility.NEVER);
mapView.getController().setZoom(ZOOM_LEVEL);
mapView.setOnTouchListener((v, event) -> {
if (event.getAction() == MotionEvent.ACTION_MOVE) {
if (markerImage.getTranslationY() == 0) {
markerImage.animate().translationY(-75)
.setInterpolator(new OvershootInterpolator()).setDuration(250).start();
}
} else if (event.getAction() == MotionEvent.ACTION_UP) {
markerImage.animate().translationY(0)
.setInterpolator(new OvershootInterpolator()).setDuration(250).start();
}
return false;
});
if ("UploadActivity".equals(activity)) {
placeSelectedButton.setVisibility(View.GONE);
modifyLocationButton.setVisibility(View.VISIBLE);
removeLocationButton.setVisibility(View.VISIBLE);
showInMapButton.setVisibility(View.VISIBLE);
largeToolbarText.setText(getResources().getString(R.string.image_location));
smallToolbarText.setText(getResources().
getString(R.string.check_whether_location_is_correct));
fabCenterOnLocation.setVisibility(View.GONE);
markerImage.setVisibility(View.GONE);
shadow.setVisibility(View.GONE);
assert cameraPosition != null;
showSelectedLocationMarker(new GeoPoint(cameraPosition.getLatitude(),
cameraPosition.getLongitude()));
}
setupMapView();
}
/**
* For showing credits
*/
private void addCredits() {
tvAttribution.setText(Html.fromHtml(getString(R.string.map_attribution)));
tvAttribution.setMovementMethod(LinkMovementMethod.getInstance());
}
/**
* For setting up Dark Theme
*/
private void darkThemeSetup() {
if (isDarkTheme) {
shadow.setColorFilter(Color.argb(255, 255, 255, 255));
mapView.getOverlayManager().getTilesOverlay()
.setColorFilter(TilesOverlay.INVERT_COLORS);
}
}
/**
* Clicking back button destroy locationPickerActivity
*/
private void addBackButtonListener() {
final ImageView backButton = findViewById(R.id.maplibre_place_picker_toolbar_back_button);
backButton.setOnClickListener(v -> {
finish();
});
}
/**
* Binds mapView and location picker icon
*/
private void bindViews() {
mapView = findViewById(R.id.map_view);
markerImage = findViewById(R.id.location_picker_image_view_marker);
tvAttribution = findViewById(R.id.tv_attribution);
modifyLocationButton = findViewById(R.id.modify_location);
removeLocationButton = findViewById(R.id.remove_location);
showInMapButton = findViewById(R.id.show_in_map);
showInMapButton.setText(getResources().getString(R.string.show_in_map_app).toUpperCase());
shadow = findViewById(R.id.location_picker_image_view_shadow);
}
/**
* Gets toolbar color
*/
private void getToolbarUI() {
final ConstraintLayout toolbar = findViewById(R.id.location_picker_toolbar);
largeToolbarText = findViewById(R.id.location_picker_toolbar_primary_text_view);
smallToolbarText = findViewById(R.id.location_picker_toolbar_secondary_text_view);
toolbar.setBackgroundColor(getResources().getColor(R.color.primaryColor));
}
private void setupMapView() {
adjustCameraBasedOnOptions();
modifyLocationButton.setOnClickListener(v -> onClickModifyLocation());
removeLocationButton.setOnClickListener(v -> onClickRemoveLocation());
showInMapButton.setOnClickListener(v -> showInMap());
darkThemeSetup();
requestLocationPermissions();
}
/**
* Handles onclick event of modifyLocationButton
*/
private void onClickModifyLocation() {
placeSelectedButton.setVisibility(View.VISIBLE);
modifyLocationButton.setVisibility(View.GONE);
removeLocationButton.setVisibility(View.GONE);
showInMapButton.setVisibility(View.GONE);
markerImage.setVisibility(View.VISIBLE);
shadow.setVisibility(View.VISIBLE);
largeToolbarText.setText(getResources().getString(R.string.choose_a_location));
smallToolbarText.setText(getResources().getString(R.string.pan_and_zoom_to_adjust));
fabCenterOnLocation.setVisibility(View.VISIBLE);
removeSelectedLocationMarker();
if (cameraPosition != null && mapView != null) {
if (mapView.getController() != null) {
mapView.getController().animateTo(new GeoPoint(cameraPosition.getLatitude(),
cameraPosition.getLongitude()));
}
}
}
/**
* Handles onclick event of removeLocationButton
*/
private void onClickRemoveLocation() {
DialogUtil.showAlertDialog(this,
getString(R.string.remove_location_warning_title),
getString(R.string.remove_location_warning_desc),
getString(R.string.continue_message),
getString(R.string.cancel), () -> removeLocationFromImage(), null);
}
/**
* Method to remove the location from the picture
*/
private void removeLocationFromImage() {
if (media != null) {
compositeDisposable.add(coordinateEditHelper.makeCoordinatesEdit(getApplicationContext()
, media, "0.0", "0.0", "0.0f")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(s -> {
Timber.d("Coordinates are removed from the image");
}));
}
final Intent returningIntent = new Intent();
setResult(AppCompatActivity.RESULT_OK, returningIntent);
finish();
}
/**
* Show the location in map app
*/
public void showInMap() {
Utils.handleGeoCoordinates(this,
new fr.free.nrw.commons.location.LatLng(mapView.getMapCenter().getLatitude(),
mapView.getMapCenter().getLongitude(), 0.0f));
}
/**
* move the location to the current media coordinates
*/
private void adjustCameraBasedOnOptions() {
if (cameraPosition != null) {
mapView.getController().setCenter(new GeoPoint(cameraPosition.getLatitude(),
cameraPosition.getLongitude()));
}
}
/**
* Select the preferable location
*/
private void addPlaceSelectedButton() {
placeSelectedButton = findViewById(R.id.location_chosen_button);
placeSelectedButton.setOnClickListener(view -> placeSelected());
}
/**
* Return the intent with required data
*/
void placeSelected() {
if (activity.equals("NoLocationUploadActivity")) {
applicationKvStore.putString(LAST_LOCATION,
mapView.getMapCenter().getLatitude()
+ ","
+ mapView.getMapCenter().getLongitude());
applicationKvStore.putString(LAST_ZOOM, mapView.getZoomLevel() + "");
}
if (media == null) {
final Intent returningIntent = new Intent();
returningIntent.putExtra(LocationPickerConstants.MAP_CAMERA_POSITION,
new CameraPosition(mapView.getMapCenter().getLatitude(),
mapView.getMapCenter().getLongitude(), 14.0));
setResult(AppCompatActivity.RESULT_OK, returningIntent);
} else {
updateCoordinates(String.valueOf(mapView.getMapCenter().getLatitude()),
String.valueOf(mapView.getMapCenter().getLongitude()),
String.valueOf(0.0f));
}
finish();
}
/**
* Fetched coordinates are replaced with existing coordinates by a POST API call.
* @param Latitude to be added
* @param Longitude to be added
* @param Accuracy to be added
*/
public void updateCoordinates(final String Latitude, final String Longitude,
final String Accuracy) {
if (media == null) {
return;
}
try {
compositeDisposable.add(
coordinateEditHelper.makeCoordinatesEdit(getApplicationContext(), media,
Latitude, Longitude, Accuracy)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(s -> {
Timber.d("Coordinates are added.");
}));
} catch (Exception e) {
if (e.getLocalizedMessage().equals(CsrfTokenClient.ANONYMOUS_TOKEN_MESSAGE)) {
final String username = sessionManager.getUserName();
final CommonsApplication.BaseLogoutListener logoutListener = new CommonsApplication.BaseLogoutListener(
this,
getString(R.string.invalid_login_message),
username
);
CommonsApplication.getInstance().clearApplicationData(
this, logoutListener);
}
}
}
/**
* Center the camera on the last saved location
*/
private void addCenterOnGPSButton() {
fabCenterOnLocation = findViewById(R.id.center_on_gps);
fabCenterOnLocation.setOnClickListener(view -> {
moveToCurrentLocation = true;
requestLocationPermissions();
});
}
/**
* Adds selected location marker on the map
*/
private void showSelectedLocationMarker(GeoPoint point) {
Drawable icon = ContextCompat.getDrawable(this, R.drawable.map_default_map_marker);
Marker marker = new Marker(mapView);
marker.setPosition(point);
marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM);
marker.setIcon(icon);
marker.setInfoWindow(null);
mapView.getOverlays().add(marker);
mapView.invalidate();
}
/**
* Removes selected location marker from the map
*/
private void removeSelectedLocationMarker() {
List<Overlay> overlays = mapView.getOverlays();
for (int i = 0; i < overlays.size(); i++) {
if (overlays.get(i) instanceof Marker) {
Marker item = (Marker) overlays.get(i);
if (cameraPosition.getLatitude() == item.getPosition().getLatitude()
&& cameraPosition.getLongitude() == item.getPosition().getLongitude()) {
mapView.getOverlays().remove(i);
mapView.invalidate();
break;
}
}
}
}
/**
* Center the map at user's current location
*/
private void requestLocationPermissions() {
locationPermissionsHelper = new LocationPermissionsHelper(
this, locationManager, this);
locationPermissionsHelper.requestForLocationAccess(R.string.location_permission_title,
R.string.upload_map_location_access);
}
@Override
public void onRequestPermissionsResult(final int requestCode,
@NonNull final String[] permissions,
@NonNull final int[] grantResults) {
if (requestCode == Constants.RequestCodes.LOCATION
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
onLocationPermissionGranted();
} else {
onLocationPermissionDenied(getString(R.string.upload_map_location_access));
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
@Override
protected void onResume() {
super.onResume();
mapView.onResume();
}
@Override
protected void onPause() {
super.onPause();
mapView.onPause();
}
@Override
public void onLocationPermissionDenied(String toastMessage) {
if (!ActivityCompat.shouldShowRequestPermissionRationale(this,
permission.ACCESS_FINE_LOCATION)) {
if (!locationPermissionsHelper.checkLocationPermission(this)) {
if (store.getBoolean("isPermissionDenied", false)) {
// means user has denied location permission twice or checked the "Don't show again"
locationPermissionsHelper.showAppSettingsDialog(this,
R.string.upload_map_location_access);
} else {
Toast.makeText(getBaseContext(), toastMessage, Toast.LENGTH_LONG).show();
}
store.putBoolean("isPermissionDenied", true);
}
} else {
Toast.makeText(getBaseContext(), toastMessage, Toast.LENGTH_LONG).show();
}
}
@Override
public void onLocationPermissionGranted() {
if (moveToCurrentLocation || !(activity.equals("MediaActivity"))) {
if (locationPermissionsHelper.isLocationAccessToAppsTurnedOn()) {
locationManager.requestLocationUpdatesFromProvider(
LocationManager.NETWORK_PROVIDER);
locationManager.requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER);
getLocation();
} else {
getLocation();
locationPermissionsHelper.showLocationOffDialog(this,
R.string.ask_to_turn_location_on_text);
}
}
}
/**
* Gets new location if locations services are on, else gets last location
*/
private void getLocation() {
fr.free.nrw.commons.location.LatLng currLocation = locationManager.getLastLocation();
if (currLocation != null) {
GeoPoint currLocationGeopoint = new GeoPoint(currLocation.getLatitude(),
currLocation.getLongitude());
addLocationMarker(currLocationGeopoint);
mapView.getController().setCenter(currLocationGeopoint);
mapView.getController().animateTo(currLocationGeopoint);
markerImage.setTranslationY(0);
}
}
private void addLocationMarker(GeoPoint geoPoint) {
if (moveToCurrentLocation) {
mapView.getOverlays().clear();
}
ScaleDiskOverlay diskOverlay =
new ScaleDiskOverlay(this,
geoPoint, 2000, GeoConstants.UnitOfMeasure.foot);
Paint circlePaint = new Paint();
circlePaint.setColor(Color.rgb(128, 128, 128));
circlePaint.setStyle(Paint.Style.STROKE);
circlePaint.setStrokeWidth(2f);
diskOverlay.setCirclePaint2(circlePaint);
Paint diskPaint = new Paint();
diskPaint.setColor(Color.argb(40, 128, 128, 128));
diskPaint.setStyle(Paint.Style.FILL_AND_STROKE);
diskOverlay.setCirclePaint1(diskPaint);
diskOverlay.setDisplaySizeMin(900);
diskOverlay.setDisplaySizeMax(1700);
mapView.getOverlays().add(diskOverlay);
org.osmdroid.views.overlay.Marker startMarker = new org.osmdroid.views.overlay.Marker(
mapView);
startMarker.setPosition(geoPoint);
startMarker.setAnchor(org.osmdroid.views.overlay.Marker.ANCHOR_CENTER,
org.osmdroid.views.overlay.Marker.ANCHOR_BOTTOM);
startMarker.setIcon(
ContextCompat.getDrawable(this, R.drawable.current_location_marker));
startMarker.setTitle("Your Location");
startMarker.setTextLabelFontSize(24);
mapView.getOverlays().add(startMarker);
}
/**
* Saves the state of the activity
* @param outState Bundle
*/
@Override
public void onSaveInstanceState(@NonNull final Bundle outState) {
super.onSaveInstanceState(outState);
if(cameraPosition!=null){
outState.putParcelable(CAMERA_POS, cameraPosition);
}
if(activity!=null){
outState.putString(ACTIVITY, activity);
}
if(media!=null){
outState.putParcelable("sMedia", media);
}
}
}

View file

@ -1,20 +0,0 @@
package fr.free.nrw.commons.LocationPicker;
/**
* Constants need for location picking
*/
public final class LocationPickerConstants {
public static final String ACTIVITY_KEY
= "location.picker.activity";
public static final String MAP_CAMERA_POSITION
= "location.picker.cameraPosition";
public static final String MEDIA
= "location.picker.media";
private LocationPickerConstants() {
}
}

View file

@ -1,63 +0,0 @@
package fr.free.nrw.commons.LocationPicker;
import android.app.Application;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.MutableLiveData;
import fr.free.nrw.commons.CameraPosition;
import org.jetbrains.annotations.NotNull;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import timber.log.Timber;
/**
* Observes live camera position data
*/
public class LocationPickerViewModel extends AndroidViewModel implements Callback<CameraPosition> {
/**
* Wrapping CameraPosition with MutableLiveData
*/
private final MutableLiveData<CameraPosition> result = new MutableLiveData<>();
/**
* Constructor for this class
*
* @param application Application
*/
public LocationPickerViewModel(@NonNull final Application application) {
super(application);
}
/**
* Responses on camera position changing
*
* @param call Call<CameraPosition>
* @param response Response<CameraPosition>
*/
@Override
public void onResponse(final @NotNull Call<CameraPosition> call,
final Response<CameraPosition> response) {
if (response.body() == null) {
result.setValue(null);
return;
}
result.setValue(response.body());
}
@Override
public void onFailure(final @NotNull Call<CameraPosition> call, final @NotNull Throwable t) {
Timber.e(t);
}
/**
* Gets live CameraPosition
*
* @return MutableLiveData<CameraPosition>
*/
public MutableLiveData<CameraPosition> getResult() {
return result;
}
}

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,10 +1,15 @@
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 kotlinx.parcelize.Parcelize
import fr.free.nrw.commons.wikidata.model.WikiSite
import fr.free.nrw.commons.wikidata.model.page.PageTitle
import java.util.*
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
import java.util.Date
import java.util.Locale
import java.util.UUID
@Parcelize
class Media constructor(
@ -14,7 +19,6 @@ class Media constructor(
*/
var pageId: String = UUID.randomUUID().toString(),
var thumbUrl: String? = null,
/**
* Gets image URL
* @return Image URL
@ -26,16 +30,9 @@ class Media constructor(
*/
var filename: String? = null,
/**
* Gets the file description.
* @return file description as a string
*/
// monolingual description on input...
/**
* Sets the file description.
* @param fallbackDescription the new description of the file
* The fallback description of the file, used if no other description is provided.
*/
var fallbackDescription: String? = null,
/**
* Gets the upload date of the file.
* Can be null.
@ -43,28 +40,25 @@ class Media constructor(
*/
var dateUploaded: Date? = null,
/**
* Gets the license name of the file.
* @return license as a String
*/
/**
* Sets the license name of the file.
*
* @param license license name as a String
* The license name of the file.
*/
var license: String? = null,
/**
* The URL corresponding to the license.
*/
var licenseUrl: String? = null,
/**
* Gets the name of the creator of the file.
* @return author name as a String
*/
/**
* Sets the author name of the file.
* @param author creator name as a string
* The name of the creator of the file.
*/
var author: String? = null,
var user:String?=null,
/**
* The username of the uploader.
*/
var user: String? = null,
/**
* The full name of the file's creator, if different from username.
*/
var creatorName: String? = null,
/**
* Gets the categories the file falls under.
* @return file categories as an ArrayList of Strings
@ -78,46 +72,111 @@ class Media constructor(
var captions: Map<String, String> = emptyMap(),
var descriptions: Map<String, String> = emptyMap(),
var depictionIds: List<String> = emptyList(),
var creatorIds: List<String> = emptyList(),
/**
* This field was added to find non-hidden categories
* Stores the mapping of category title to hidden attribute
* Example: "Mountains" => false, "CC-BY-SA-2.0" => true
*/
var categoriesHiddenStatus: Map<String, Boolean> = emptyMap()
var categoriesHiddenStatus: Map<String, Boolean> = emptyMap(),
) : Parcelable {
constructor(
captions: Map<String, String>,
categories: List<String>?,
filename: String?,
fallbackDescription: String?,
author: String?,
user: String?,
) : this(
filename = filename,
fallbackDescription = fallbackDescription,
dateUploaded = Date(),
author = author,
user = user,
categories = categories,
captions = captions,
)
constructor(
captions: Map<String, String>,
categories: List<String>?,
filename: String?,
fallbackDescription: String?,
author: String?, user:String?
author: String?,
user: String?,
dateUploaded: Date? = Date(),
license: String? = null,
licenseUrl: String? = null,
imageUrl: String? = null,
thumbUrl: String? = null,
coordinates: LatLng? = null,
descriptions: Map<String, String> = emptyMap(),
depictionIds: List<String> = emptyList(),
categoriesHiddenStatus: Map<String, Boolean> = emptyMap()
) : this(
pageId = UUID.randomUUID().toString(),
filename = filename,
fallbackDescription = fallbackDescription,
dateUploaded = Date(),
dateUploaded = dateUploaded,
author = author,
user=user,
user = user,
categories = categories,
captions = captions
captions = captions,
license = license,
licenseUrl = licenseUrl,
imageUrl = imageUrl,
thumbUrl = thumbUrl,
coordinates = coordinates,
descriptions = descriptions,
depictionIds = depictionIds,
categoriesHiddenStatus = categoriesHiddenStatus
)
/**
* Returns Author if it's not null or empty, otherwise
* returns user
* @return Author or User
*/
@Deprecated("Use user for uploader username. Use attributedAuthor() for attribution. Note that the uploader may not be the creator/author.")
fun getAuthorOrUser(): String? {
return if (!author.isNullOrEmpty()) {
author
} else{
user
}
}
/**
* Returns author if it's not null or empty, otherwise
* returns creator name
* @return name of author or creator
*/
fun getAttributedAuthor(): String? {
return if (!author.isNullOrEmpty()) {
author
} else{
creatorName
}
}
/**
* Gets media display title
* @return Media title
*/
val displayTitle: String
get() =
if (filename != null)
if (filename != null) {
pageTitle.displayTextWithoutNamespace.replaceFirst("[.][^.]+$".toRegex(), "")
else
} else {
""
}
/**
* 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
@ -127,17 +186,21 @@ class Media constructor(
get() = String.format("[[%s|thumb|%s]]", filename, mostRelevantCaption)
val mostRelevantCaption: String
get() = captions[Locale.getDefault().language]
?: captions.values.firstOrNull()
?: displayTitle
get() =
captions[Locale.getDefault().language]
?: captions.values.firstOrNull()
?: displayTitle
/**
* Gets the categories the file falls under.
* @return file categories as an ArrayList of Strings
*/
@IgnoredOnParcel
var addedCategories: List<String>? = null
// TODO added categories should be removed. It is added for a short fix. On category update,
// categories should be re-fetched instead
get() = field // getter
set(value) { field = value } // setter
get() = field // getter
set(value) {
field = value
} // setter
}

View file

@ -1,9 +1,9 @@
package fr.free.nrw.commons
import androidx.core.text.HtmlCompat
import fr.free.nrw.commons.media.PAGE_ID_PREFIX
import fr.free.nrw.commons.media.IdAndCaptions
import fr.free.nrw.commons.media.IdAndLabels
import fr.free.nrw.commons.media.MediaClient
import fr.free.nrw.commons.media.PAGE_ID_PREFIX
import io.reactivex.Single
import timber.log.Timber
import javax.inject.Inject
@ -17,42 +17,56 @@ import javax.inject.Singleton
* to the media and may change due to editing.
*/
@Singleton
class MediaDataExtractor @Inject constructor(private val mediaClient: MediaClient) {
class MediaDataExtractor
@Inject
constructor(
private val mediaClient: MediaClient,
) {
fun fetchDepictionIdsAndLabels(media: Media) =
mediaClient
.getEntities(media.depictionIds)
.map {
it
.entities()
.mapValues { entry -> entry.value.labels().mapValues { it.value.value() } }
}.map { it.map { (key, value) -> IdAndLabels(key, value) } }
.onErrorReturn { emptyList() }
fun fetchDepictionIdsAndLabels(media: Media) =
mediaClient.getEntities(media.depictionIds)
.map {
it.entities()
.mapValues { entry -> entry.value.labels().mapValues { it.value.value() } }
}
.map { it.map { (key, value) -> IdAndCaptions(key, value) } }
.onErrorReturn { emptyList() }
fun fetchCreatorIdsAndLabels(media: Media) =
mediaClient
.getEntities(media.creatorIds)
.map {
it
.entities()
.mapValues { entry -> entry.value.labels().mapValues { it.value.value() } }
}.map { it.map { (key, value) -> IdAndLabels(key, value) } }
.onErrorReturn { emptyList() }
fun checkDeletionRequestExists(media: Media) =
mediaClient.checkPageExistsUsingTitle("Commons:Deletion_requests/" + media.filename)
fun checkDeletionRequestExists(media: Media) = mediaClient.checkPageExistsUsingTitle("Commons:Deletion_requests/" + media.filename)
fun fetchDiscussion(media: Media) =
mediaClient.getPageHtml(media.filename!!.replace("File", "File talk"))
.map { HtmlCompat.fromHtml(it, HtmlCompat.FROM_HTML_MODE_LEGACY).toString() }
.onErrorReturn {
Timber.d("Error occurred while fetching discussion")
""
}
fun fetchDiscussion(media: Media) =
mediaClient
.getPageHtml(media.filename!!.replace("File", "File talk"))
.map { HtmlCompat.fromHtml(it, HtmlCompat.FROM_HTML_MODE_LEGACY).toString() }
.onErrorReturn {
Timber.d("Error occurred while fetching discussion")
""
}
fun refresh(media: Media): Single<Media> {
return Single.ambArray(
mediaClient.getMediaById(PAGE_ID_PREFIX + media.pageId)
.onErrorResumeNext { Single.never() },
mediaClient.getMediaSuppressingErrors(media.filename)
.onErrorResumeNext { Single.never() }
)
fun refresh(media: Media): Single<Media> =
Single.ambArray(
mediaClient
.getMediaById(PAGE_ID_PREFIX + media.pageId)
.onErrorResumeNext { Single.never() },
mediaClient
.getMediaSuppressingErrors(media.filename)
.onErrorResumeNext { Single.never() },
)
fun getHtmlOfPage(title: String) = mediaClient.getPageHtml(title)
/**
* Fetches wikitext from mediaClient
*/
fun getCurrentWikiText(title: String) = mediaClient.getCurrentWikiText(title)
}
fun getHtmlOfPage(title: String) = mediaClient.getPageHtml(title);
/**
* Fetches wikitext from mediaClient
*/
fun getCurrentWikiText(title: String) = mediaClient.getCurrentWikiText(title);
}

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

@ -10,7 +10,9 @@ internal object Urls {
const val FAQ_URL = "https://github.com/commons-app/commons-app-documentation/blob/master/android/Frequently-Asked-Questions.md"
const val PLAY_STORE_PREFIX = "market://details?id="
const val PLAY_STORE_URL_PREFIX = "https://play.google.com/store/apps/details?id="
const val TRANSLATE_WIKI_URL = "https://translatewiki.net/w/i.php?title=Special:Translate&group=commons-android-strings&filter=%21translated&action=translate&language="
const val TRANSLATE_WIKI_URL =
"https://translatewiki.net/w/i.php?title=Special:Translate" +
"&group=commons-android-strings&filter=%21translated&action=translate&language="
const val FACEBOOK_WEB_URL = "https://www.facebook.com/1921335171459985"
const val FACEBOOK_APP_URL = "fb://page/1921335171459985"
const val FACEBOOK_PACKAGE_NAME = "com.facebook.katana"

View file

@ -1,250 +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
* @param latLng
*/
public static void handleGeoCoordinates(Context context, LatLng latLng) {
Intent mapIntent = new Intent(Intent.ACTION_VIEW, latLng.getGmmIntentUri());
if (mapIntent.resolveActivity(context.getPackageManager()) != null) {
context.startActivity(mapIntent);
} else {
ViewUtil.showShortToast(context, context.getString(R.string.map_application_missing));
}
}
/**
* To take screenshot of the screen and return it in Bitmap format
*
* @param view
* @return
*/
public static Bitmap getScreenShot(View view) {
View screenView = view.getRootView();
screenView.setDrawingCacheEnabled(true);
Bitmap drawingCache = screenView.getDrawingCache();
if (drawingCache != null) {
Bitmap bitmap = Bitmap.createBitmap(drawingCache);
screenView.setDrawingCacheEnabled(false);
return bitmap;
}
return null;
}
/*
*Copies the content to the clipboard
*
*/
public static void copy(String label,String text, Context context){
ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText(label, text);
clipboard.setPrimaryClip(clip);
}
/**
* This method sets underlined string text to a TextView
*
* @param textView TextView associated with string resource
* @param stringResourceName string resource name
* @param context
*/
public static void setUnderlinedText(TextView textView, int stringResourceName, Context context) {
SpannableString content = new SpannableString(context.getString(stringResourceName));
content.setSpan(new UnderlineSpan(), 0, content.length(), 0);
textView.setText(content);
}
/**
* For now we are enabling the monuments only when the date lies between 1 Sept & 31 OCt
* @param date
* @return
*/
public static boolean isMonumentsEnabled(final Date date) {
if (date.getMonth() == 8) {
return true;
}
return false;
}
/**
* Util function to get the start date of wlm monument
* For this release we are hardcoding it to be 1st September
* @return
*/
public static String getWLMStartDate() {
return "1 Sep";
}
/***
* Util function to get the end date of wlm monument
* For this release we are hardcoding it to be 31st October
* @return
*/
public static String getWLMEndDate() {
return "30 Sep";
}
/***
* 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,57 +0,0 @@
package fr.free.nrw.commons;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
import java.util.ArrayList;
import java.util.List;
/**
* This adapter will be used to display fragments in a ViewPager
*/
public class ViewPagerAdapter extends FragmentPagerAdapter {
private List<Fragment> fragmentList = new ArrayList<>();
private List<String> fragmentTitleList = new ArrayList<>();
public ViewPagerAdapter(FragmentManager manager) {
super(manager);
}
/**
* This method returns the fragment of the viewpager at a particular position
* @param position
*/
@Override
public Fragment getItem(int position) {
return fragmentList.get(position);
}
/**
* This method returns the total number of fragments in the viewpager.
* @return size
*/
@Override
public int getCount() {
return fragmentList.size();
}
/**
* This method sets the fragment and title list in the viewpager
* @param fragmentList List of all fragments to be displayed in the viewpager
* @param fragmentTitleList List of all titles of the fragments
*/
public void setTabData(List<Fragment> fragmentList, List<String> fragmentTitleList) {
this.fragmentList = fragmentList;
this.fragmentTitleList = fragmentTitleList;
}
/**
* This method returns the title of the page at a particular position
* @param position
*/
@Override
public CharSequence getPageTitle(int position) {
return fragmentTitleList.get(position);
}
}

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,108 +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);
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

@ -1,9 +1,9 @@
package fr.free.nrw.commons.actions
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException
import io.reactivex.Observable
import io.reactivex.Single
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient
/**
* This class acts as a Client to facilitate wiki page editing
@ -14,9 +14,8 @@ import fr.free.nrw.commons.auth.csrf.CsrfTokenClient
*/
class PageEditClient(
private val csrfTokenClient: CsrfTokenClient,
private val pageEditInterface: PageEditInterface
private val pageEditInterface: PageEditInterface,
) {
/**
* Replace the content of a wiki page
* @param pageTitle Title of the page to edit
@ -24,10 +23,53 @@ class PageEditClient(
* @param summary Edit summary
* @return whether the edit was successful
*/
fun edit(pageTitle: String, text: String, summary: String): Observable<Boolean> {
return try {
pageEditInterface.postEdit(pageTitle, summary, text, csrfTokenClient.getTokenBlocking())
.map { editResponse -> editResponse.edit()!!.editSucceeded() }
fun edit(
pageTitle: String,
text: String,
summary: String,
): Observable<Boolean> =
try {
pageEditInterface
.postEdit(pageTitle, summary, text, csrfTokenClient.getTokenBlocking())
.map { editResponse ->
editResponse.edit()!!.editSucceeded()
}
} catch (throwable: Throwable) {
if (throwable is InvalidLoginTokenException) {
throw throwable
} else {
Observable.just(false)
}
}
/**
* Creates a new page with the given title, text, and summary.
*
* @param pageTitle The title of the page to be created.
* @param text The content of the page in wikitext format.
* @param summary The edit summary for the page creation.
* @return An observable that emits true if the page creation succeeded, false otherwise.
* @throws InvalidLoginTokenException If an invalid login token is encountered during the process.
*/
fun postCreate(
pageTitle: String,
text: String,
summary: String,
): Observable<Boolean> =
try {
pageEditInterface
.postCreate(
pageTitle,
summary,
text,
"text/x-wiki",
"wikitext",
true,
true,
csrfTokenClient.getTokenBlocking(),
).map { editResponse ->
editResponse.edit()!!.editSucceeded()
}
} catch (throwable: Throwable) {
if (throwable is InvalidLoginTokenException) {
throw throwable
@ -35,7 +77,6 @@ class PageEditClient(
Observable.just(false)
}
}
}
/**
* Append text to the end of a wiki page
@ -44,9 +85,14 @@ class PageEditClient(
* @param summary Edit summary
* @return whether the edit was successful
*/
fun appendEdit(pageTitle: String, appendText: String, summary: String): Observable<Boolean> {
return try {
pageEditInterface.postAppendEdit(pageTitle, summary, appendText, csrfTokenClient.getTokenBlocking())
fun appendEdit(
pageTitle: String,
appendText: String,
summary: String,
): Observable<Boolean> =
try {
pageEditInterface
.postAppendEdit(pageTitle, summary, appendText, csrfTokenClient.getTokenBlocking())
.map { editResponse -> editResponse.edit()!!.editSucceeded() }
} catch (throwable: Throwable) {
if (throwable is InvalidLoginTokenException) {
@ -55,7 +101,6 @@ class PageEditClient(
Observable.just(false)
}
}
}
/**
* Prepend text to the beginning of a wiki page
@ -64,9 +109,14 @@ class PageEditClient(
* @param summary Edit summary
* @return whether the edit was successful
*/
fun prependEdit(pageTitle: String, prependText: String, summary: String): Observable<Boolean> {
return try {
pageEditInterface.postPrependEdit(pageTitle, summary, prependText, csrfTokenClient.getTokenBlocking())
fun prependEdit(
pageTitle: String,
prependText: String,
summary: String,
): Observable<Boolean> =
try {
pageEditInterface
.postPrependEdit(pageTitle, summary, prependText, csrfTokenClient.getTokenBlocking())
.map { editResponse -> editResponse.edit()?.editSucceeded() ?: false }
} catch (throwable: Throwable) {
if (throwable is InvalidLoginTokenException) {
@ -75,8 +125,32 @@ class PageEditClient(
Observable.just(false)
}
}
}
/**
* Appends a new section to the wiki page
* @param pageTitle Title of the page to edit
* @param sectionTitle Title of the new section that needs to be created
* @param sectionText The page content that is to be added to the section
* @param summary Edit summary
* @return whether the edit was successful
*/
fun createNewSection(
pageTitle: String,
sectionTitle: String,
sectionText: String,
summary: String,
): Observable<Boolean> =
try {
pageEditInterface
.postNewSection(pageTitle, summary, sectionTitle, sectionText, csrfTokenClient.getTokenBlocking())
.map { editResponse -> editResponse.edit()!!.editSucceeded() }
} catch (throwable: Throwable) {
if (throwable is InvalidLoginTokenException) {
throw throwable
} else {
Observable.just(false)
}
}
/**
* Set new labels to Wikibase server of commons
@ -86,12 +160,21 @@ class PageEditClient(
* @param value label
* @return 1 when the edit was successful
*/
fun setCaptions(summary: String, title: String,
language: String, value: String) : Observable<Int>{
return try {
pageEditInterface.postCaptions(summary, title, language,
value, csrfTokenClient.getTokenBlocking()
).map { it.success }
fun setCaptions(
summary: String,
title: String,
language: String,
value: String,
): Observable<Int> =
try {
pageEditInterface
.postCaptions(
summary,
title,
language,
value,
csrfTokenClient.getTokenBlocking(),
).map { it.success }
} catch (throwable: Throwable) {
if (throwable is InvalidLoginTokenException) {
throw throwable
@ -99,16 +182,20 @@ class PageEditClient(
Observable.just(0)
}
}
}
/**
* Get whole WikiText of required file
* @param title : Name of the file
* @return Observable<MwQueryResult>
*/
fun getCurrentWikiText(title: String): Single<String?> {
return pageEditInterface.getWikiText(title).map {
it.query()?.pages()?.get(0)?.revisions()?.get(0)?.content()
fun getCurrentWikiText(title: String): Single<String?> =
pageEditInterface.getWikiText(title).map {
it
.query()
?.pages()
?.get(0)
?.revisions()
?.get(0)
?.content()
}
}
}

View file

@ -3,10 +3,15 @@ package fr.free.nrw.commons.actions
import fr.free.nrw.commons.wikidata.WikidataConstants.MW_API_PREFIX
import fr.free.nrw.commons.wikidata.model.Entities
import fr.free.nrw.commons.wikidata.model.edit.Edit
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
import io.reactivex.Observable
import io.reactivex.Single
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
import retrofit2.http.*
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.GET
import retrofit2.http.Headers
import retrofit2.http.POST
import retrofit2.http.Query
/**
* This interface facilitates wiki commons page editing services to the Networking module
@ -33,7 +38,34 @@ interface PageEditInterface {
@Field("summary") summary: String,
@Field("text") text: String,
// NOTE: This csrf shold always be sent as the last field of form data
@Field("token") token: String
@Field("token") token: String,
): Observable<Edit>
/**
* This method creates or edits a page for nearby items.
*
* @param title Title of the page to edit. Cannot be used together with pageid.
* @param summary Edit summary. Also used as the section title when section=new and sectiontitle is not set.
* @param text Text of the page.
* @param contentformat Format of the content (e.g., "text/x-wiki").
* @param contentmodel Model of the content (e.g., "wikitext").
* @param minor Whether the edit is a minor edit.
* @param recreate Whether to recreate the page if it does not exist.
* @param token A "csrf" token. This should always be sent as the last field of form data.
*/
@FormUrlEncoded
@Headers("Cache-Control: no-cache")
@POST(MW_API_PREFIX + "action=edit")
fun postCreate(
@Field("title") title: String,
@Field("summary") summary: String,
@Field("text") text: String,
@Field("contentformat") contentformat: String,
@Field("contentmodel") contentmodel: String,
@Field("minor") minor: Boolean,
@Field("recreate") recreate: Boolean,
// NOTE: This csrf shold always be sent as the last field of form data
@Field("token") token: String,
): Observable<Edit>
/**
@ -52,7 +84,7 @@ interface PageEditInterface {
@Field("title") title: String,
@Field("summary") summary: String,
@Field("appendtext") appendText: String,
@Field("token") token: String
@Field("token") token: String,
): Observable<Edit>
/**
@ -71,9 +103,19 @@ interface PageEditInterface {
@Field("title") title: String,
@Field("summary") summary: String,
@Field("prependtext") prependText: String,
@Field("token") token: String
@Field("token") token: String,
): Observable<Edit>
@FormUrlEncoded
@Headers("Cache-Control: no-cache")
@POST(MW_API_PREFIX + "action=edit&section=new")
fun postNewSection(
@Field("title") title: String,
@Field("summary") summary: String,
@Field("sectiontitle") sectionTitle: String,
@Field("text") sectionText: String,
@Field("token") token: String,
): Observable<Edit>
@FormUrlEncoded
@Headers("Cache-Control: no-cache")
@ -83,16 +125,17 @@ interface PageEditInterface {
@Field("title") title: String,
@Field("language") language: String,
@Field("value") value: String,
@Field("token") token: String
@Field("token") token: String,
): Observable<Entities>
/**
* Get wiki text for provided file names
* @param titles : Name of the file
* @return Single<MwQueryResult>
* Gets the wiki text for the provided file name.
*
* @param title The title (name) of the file to fetch wiki text for.
* @return A Single emitting the wiki query response.
*/
@GET(MW_API_PREFIX + "action=query&prop=revisions&rvprop=content|timestamp&rvlimit=1&converttitles=")
fun getWikiText(
@Query("titles") title: String
@Query("titles") title: String,
): Single<MwQueryResponse?>
}

View file

@ -1,11 +1,10 @@
package fr.free.nrw.commons.actions
import fr.free.nrw.commons.CommonsApplication
import fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_CSRF
import io.reactivex.Observable
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException
import fr.free.nrw.commons.auth.login.LoginFailedException
import fr.free.nrw.commons.di.NetworkingModule.Companion.NAMED_COMMONS_CSRF
import io.reactivex.Observable
import javax.inject.Inject
import javax.inject.Named
import javax.inject.Singleton
@ -15,34 +14,33 @@ import javax.inject.Singleton
* Thanks are used by a user to show gratitude to another user for their contributions
*/
@Singleton
class ThanksClient @Inject constructor(
@param:Named(NAMED_COMMONS_CSRF) private val csrfTokenClient: CsrfTokenClient,
private val service: ThanksInterface
) {
/**
* Thanks a user for a particular revision
* @param revisionId The revision ID the user would like to thank someone for
* @return if thanks was successfully sent to intended recipient
*/
fun thank(revisionId: Long): Observable<Boolean> {
return try {
service.thank(
revisionId.toString(), // Rev
null, // Log
csrfTokenClient.getTokenBlocking(), // Token
CommonsApplication.getInstance().userAgent // Source
).map {
mwThankPostResponse -> mwThankPostResponse.result?.success == 1
class ThanksClient
@Inject
constructor(
@param:Named(NAMED_COMMONS_CSRF) private val csrfTokenClient: CsrfTokenClient,
private val service: ThanksInterface,
) {
/**
* Thanks a user for a particular revision
* @param revisionId The revision ID the user would like to thank someone for
* @return if thanks was successfully sent to intended recipient
*/
fun thank(revisionId: Long): Observable<Boolean> =
try {
service
.thank(
revisionId.toString(), // Rev
null, // Log
csrfTokenClient.getTokenBlocking(), // Token
CommonsApplication.instance.userAgent, // Source
).map { mwThankPostResponse ->
mwThankPostResponse.result?.success == 1
}
} catch (throwable: Throwable) {
if (throwable is InvalidLoginTokenException) {
Observable.error(throwable)
} else {
Observable.just(false)
}
}
}
catch (throwable: Throwable) {
if (throwable is InvalidLoginTokenException) {
Observable.error(throwable)
}
else {
Observable.just(false)
}
}
}
}

View file

@ -19,6 +19,6 @@ interface ThanksInterface {
@Field("rev") rev: String?,
@Field("log") log: String?,
@Field("token") token: String,
@Field("source") source: String?
@Field("source") source: String?,
): Observable<MwThankPostResponse?>
}

View file

@ -0,0 +1,218 @@
package fr.free.nrw.commons.activity
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.webkit.ConsoleMessage
import android.webkit.CookieManager
import android.webkit.WebChromeClient
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
import fr.free.nrw.commons.CommonsApplication
import fr.free.nrw.commons.CommonsApplication.ActivityLogoutListener
import fr.free.nrw.commons.R
import fr.free.nrw.commons.di.ApplicationlessInjection
import fr.free.nrw.commons.wikidata.cookies.CommonsCookieJar
import okhttp3.HttpUrl.Companion.toHttpUrl
import timber.log.Timber
import javax.inject.Inject
/**
* SingleWebViewActivity is a reusable activity webView based on a given url(initial url) and
* closes itself when a specified success URL is reached to success url.
*/
class SingleWebViewActivity : ComponentActivity() {
@Inject
lateinit var cookieJar: CommonsCookieJar
@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val url = intent.getStringExtra(VANISH_ACCOUNT_URL)
val successUrl = intent.getStringExtra(VANISH_ACCOUNT_SUCCESS_URL)
if (url == null || successUrl == null) {
finish()
return
}
ApplicationlessInjection
.getInstance(applicationContext)
.commonsApplicationComponent
.inject(this)
setCookies(url)
enableEdgeToEdge()
setContent {
Scaffold(
topBar = {
TopAppBar(
modifier = Modifier,
title = { Text(getString(R.string.vanish_account)) },
navigationIcon = {
IconButton(
onClick = {
// Close the WebView Activity if the user taps the back button
finish()
},
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
// TODO("Add contentDescription)
contentDescription = ""
)
}
}
)
},
content = {
WebViewComponent(
url = url,
successUrl = successUrl,
onSuccess = {
//Redirect the user to login screen like we do when the user logout's
val app = applicationContext as CommonsApplication
app.clearApplicationData(
applicationContext,
ActivityLogoutListener(activity = this, ctx = applicationContext)
)
finish()
},
modifier = Modifier
.fillMaxSize()
.padding(it)
)
}
)
}
}
/**
* @param url The initial URL which we are loading in the WebView.
* @param successUrl The URL that, when reached, triggers the `onSuccess` callback.
* @param onSuccess A callback that is invoked when the current url of webView is successUrl.
* This is used when we want to close when the webView once a success url is hit.
* @param modifier An optional [Modifier] to customize the layout or appearance of the WebView.
*/
@SuppressLint("SetJavaScriptEnabled")
@Composable
private fun WebViewComponent(
url: String,
successUrl: String,
onSuccess: () -> Unit,
modifier: Modifier = Modifier
) {
val webView = remember { mutableStateOf<WebView?>(null) }
AndroidView(
modifier = modifier,
factory = {
WebView(it).apply {
settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
javaScriptCanOpenWindowsAutomatically = true
}
webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView?,
request: WebResourceRequest?
): Boolean {
request?.url?.let { url ->
Timber.d("URL Loading: $url")
if (url.toString() == successUrl) {
Timber.d("Success URL detected. Closing WebView.")
onSuccess() // Close the activity
return true
}
return false
}
return false
}
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
setCookies(url.orEmpty())
}
}
webChromeClient = object : WebChromeClient() {
override fun onConsoleMessage(message: ConsoleMessage): Boolean {
Timber.d("%s%s",
"Console: ${message.message()} -- From line ",
"${message.lineNumber()} of ${message.sourceId()}")
return true
}
}
loadUrl(url)
}
},
update = {
webView.value = it
}
)
}
/**
* Sets cookies for the given URL using the cookies stored in the `CommonsCookieJar`.
*
* @param url The URL for which cookies need to be set.
*/
private fun setCookies(url: String) {
CookieManager.getInstance().let {
val cookies = cookieJar.loadForRequest(url.toHttpUrl())
for (cookie in cookies) {
it.setCookie(url, cookie.toString())
}
}
}
companion object {
private const val VANISH_ACCOUNT_URL = "VanishAccountUrl"
private const val VANISH_ACCOUNT_SUCCESS_URL = "vanishAccountSuccessUrl"
/**
* Launch the WebViewActivity with the specified URL and success URL.
* @param context The context from which the activity is launched.
* @param url The initial URL to load in the WebView.
* @param successUrl The URL that triggers the WebView to close when matched.
*/
fun showWebView(
context: Context,
url: String,
successUrl: String
) {
val intent = Intent(
context,
SingleWebViewActivity::class.java
).apply {
putExtra(VANISH_ACCOUNT_URL, url)
putExtra(VANISH_ACCOUNT_SUCCESS_URL, successUrl)
}
context.startActivity(intent)
}
}
}

View file

@ -1,44 +0,0 @@
package fr.free.nrw.commons.auth;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.Context;
import androidx.annotation.Nullable;
import fr.free.nrw.commons.BuildConfig;
import timber.log.Timber;
public class AccountUtil {
public static final String AUTH_TOKEN_TYPE = "CommonsAndroid";
public AccountUtil() {
}
/**
* @return Account|null
*/
@Nullable
public static Account account(Context context) {
try {
Account[] accounts = accountManager(context).getAccountsByType(BuildConfig.ACCOUNT_TYPE);
if (accounts.length > 0) {
return accounts[0];
}
} catch (SecurityException e) {
Timber.e(e);
}
return null;
}
@Nullable
public static String getUserName(Context context) {
Account account = account(context);
return account == null ? null : account.name;
}
private static AccountManager accountManager(Context context) {
return AccountManager.get(context);
}
}

View file

@ -0,0 +1,24 @@
package fr.free.nrw.commons.auth
import android.accounts.Account
import android.accounts.AccountManager
import android.content.Context
import androidx.annotation.VisibleForTesting
import fr.free.nrw.commons.BuildConfig.ACCOUNT_TYPE
import timber.log.Timber
const val AUTH_TOKEN_TYPE: String = "CommonsAndroid"
fun getUserName(context: Context): String? {
return account(context)?.name
}
@VisibleForTesting
fun account(context: Context): Account? = try {
val accountManager = AccountManager.get(context)
val accounts = accountManager.getAccountsByType(ACCOUNT_TYPE)
if (accounts.isNotEmpty()) accounts[0] else null
} catch (e: SecurityException) {
Timber.e(e)
null
}

View file

@ -1,456 +0,0 @@
package fr.free.nrw.commons.auth;
import android.accounts.AccountAuthenticatorActivity;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.TextView;
import androidx.annotation.ColorRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.core.app.NavUtils;
import androidx.core.content.ContextCompat;
import fr.free.nrw.commons.auth.login.LoginClient;
import fr.free.nrw.commons.auth.login.LoginResult;
import fr.free.nrw.commons.databinding.ActivityLoginBinding;
import fr.free.nrw.commons.utils.ActivityUtils;
import java.util.Locale;
import fr.free.nrw.commons.auth.login.LoginCallback;
import java.util.Objects;
import javax.inject.Inject;
import javax.inject.Named;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.utils.ConfigUtils;
import fr.free.nrw.commons.utils.SystemThemeUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.disposables.CompositeDisposable;
import timber.log.Timber;
import static android.view.KeyEvent.KEYCODE_ENTER;
import static android.view.View.VISIBLE;
import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE;
import static fr.free.nrw.commons.CommonsApplication.loginMessageIntentKey;
import static fr.free.nrw.commons.CommonsApplication.loginUsernameIntentKey;
public class LoginActivity extends AccountAuthenticatorActivity {
@Inject
SessionManager sessionManager;
@Inject
@Named("default_preferences")
JsonKvStore applicationKvStore;
@Inject
LoginClient loginClient;
@Inject
SystemThemeUtils systemThemeUtils;
private ActivityLoginBinding binding;
ProgressDialog progressDialog;
private AppCompatDelegate delegate;
private LoginTextWatcher textWatcher = new LoginTextWatcher();
private CompositeDisposable compositeDisposable = new CompositeDisposable();
final String saveProgressDailog="ProgressDailog_state";
final String saveErrorMessage ="errorMessage";
final String saveUsername="username";
final String savePassword="password";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ApplicationlessInjection
.getInstance(this.getApplicationContext())
.getCommonsApplicationComponent()
.inject(this);
boolean isDarkTheme = systemThemeUtils.isDeviceInNightMode();
setTheme(isDarkTheme ? R.style.DarkAppTheme : R.style.LightAppTheme);
getDelegate().installViewFactory();
getDelegate().onCreate(savedInstanceState);
binding = ActivityLoginBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
String message = getIntent().getStringExtra(loginMessageIntentKey);
String username = getIntent().getStringExtra(loginUsernameIntentKey);
binding.loginUsername.addTextChangedListener(textWatcher);
binding.loginPassword.addTextChangedListener(textWatcher);
binding.loginTwoFactor.addTextChangedListener(textWatcher);
binding.skipLogin.setOnClickListener(view -> skipLogin());
binding.forgotPassword.setOnClickListener(view -> forgotPassword());
binding.aboutPrivacyPolicy.setOnClickListener(view -> onPrivacyPolicyClicked());
binding.signUpButton.setOnClickListener(view -> signUp());
binding.loginButton.setOnClickListener(view -> performLogin());
binding.loginPassword.setOnEditorActionListener(this::onEditorAction);
binding.loginPassword.setOnFocusChangeListener(this::onPasswordFocusChanged);
if (ConfigUtils.isBetaFlavour()) {
binding.loginCredentials.setText(getString(R.string.login_credential));
} else {
binding.loginCredentials.setVisibility(View.GONE);
}
if (message != null) {
showMessage(message, R.color.secondaryDarkColor);
}
if (username != null) {
binding.loginUsername.setText(username);
}
}
/**
* Hides the keyboard if the user's focus is not on the password (hasFocus is false).
* @param view The keyboard
* @param hasFocus Set to true if the keyboard has focus
*/
void onPasswordFocusChanged(View view, boolean hasFocus) {
if (!hasFocus) {
ViewUtil.hideKeyboard(view);
}
}
boolean onEditorAction(TextView textView, int actionId, KeyEvent keyEvent) {
if (binding.loginButton.isEnabled()) {
if (actionId == IME_ACTION_DONE) {
performLogin();
return true;
} else if ((keyEvent != null) && keyEvent.getKeyCode() == KEYCODE_ENTER) {
performLogin();
return true;
}
}
return false;
}
protected void skipLogin() {
new AlertDialog.Builder(this).setTitle(R.string.skip_login_title)
.setMessage(R.string.skip_login_message)
.setCancelable(false)
.setPositiveButton(R.string.yes, (dialog, which) -> {
dialog.cancel();
performSkipLogin();
})
.setNegativeButton(R.string.no, (dialog, which) -> dialog.cancel())
.show();
}
protected void forgotPassword() {
Utils.handleWebUrl(this, Uri.parse(BuildConfig.FORGOT_PASSWORD_URL));
}
protected void onPrivacyPolicyClicked() {
Utils.handleWebUrl(this, Uri.parse(BuildConfig.PRIVACY_POLICY_URL));
}
protected void signUp() {
Intent intent = new Intent(this, SignupActivity.class);
startActivity(intent);
}
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
getDelegate().onPostCreate(savedInstanceState);
}
@Override
protected void onResume() {
super.onResume();
if (sessionManager.getCurrentAccount() != null
&& sessionManager.isUserLoggedIn()) {
applicationKvStore.putBoolean("login_skipped", false);
startMainActivity();
}
if (applicationKvStore.getBoolean("login_skipped", false)) {
performSkipLogin();
}
}
@Override
protected void onDestroy() {
compositeDisposable.clear();
try {
// To prevent leaked window when finish() is called, see http://stackoverflow.com/questions/32065854/activity-has-leaked-window-at-alertdialog-show-method
if (progressDialog != null && progressDialog.isShowing()) {
progressDialog.dismiss();
}
} catch (Exception e) {
e.printStackTrace();
}
binding.loginUsername.removeTextChangedListener(textWatcher);
binding.loginPassword.removeTextChangedListener(textWatcher);
binding.loginTwoFactor.removeTextChangedListener(textWatcher);
delegate.onDestroy();
if(null!=loginClient) {
loginClient.cancel();
}
binding = null;
super.onDestroy();
}
public void performLogin() {
Timber.d("Login to start!");
final String username = Objects.requireNonNull(binding.loginUsername.getText()).toString();
final String password = Objects.requireNonNull(binding.loginPassword.getText()).toString();
final String twoFactorCode = Objects.requireNonNull(binding.loginTwoFactor.getText()).toString();
showLoggingProgressBar();
loginClient.doLogin(username, password, twoFactorCode, Locale.getDefault().getLanguage(),
new LoginCallback() {
@Override
public void success(@NonNull LoginResult loginResult) {
runOnUiThread(()->{
Timber.d("Login Success");
hideProgress();
onLoginSuccess(loginResult);
});
}
@Override
public void twoFactorPrompt(@NonNull Throwable caught, @Nullable String token) {
runOnUiThread(()->{
Timber.d("Requesting 2FA prompt");
hideProgress();
askUserForTwoFactorAuth();
});
}
@Override
public void passwordResetPrompt(@Nullable String token) {
runOnUiThread(()->{
Timber.d("Showing password reset prompt");
hideProgress();
showPasswordResetPrompt();
});
}
@Override
public void error(@NonNull Throwable caught) {
runOnUiThread(()->{
Timber.e(caught);
hideProgress();
showMessageAndCancelDialog(caught.getLocalizedMessage());
});
}
});
}
private void hideProgress() {
progressDialog.dismiss();
}
private void showPasswordResetPrompt() {
showMessageAndCancelDialog(getString(R.string.you_must_reset_your_passsword));
}
/**
* This function is called when user skips the login.
* It redirects the user to Explore Activity.
*/
private void performSkipLogin() {
applicationKvStore.putBoolean("login_skipped", true);
MainActivity.startYourself(this);
finish();
}
private void showLoggingProgressBar() {
progressDialog = new ProgressDialog(this);
progressDialog.setIndeterminate(true);
progressDialog.setTitle(getString(R.string.logging_in_title));
progressDialog.setMessage(getString(R.string.logging_in_message));
progressDialog.setCanceledOnTouchOutside(false);
progressDialog.show();
}
private void onLoginSuccess(LoginResult loginResult) {
compositeDisposable.clear();
sessionManager.setUserLoggedIn(true);
sessionManager.updateAccount(loginResult);
progressDialog.dismiss();
showSuccessAndDismissDialog();
startMainActivity();
}
@Override
protected void onStart() {
super.onStart();
delegate.onStart();
}
@Override
protected void onStop() {
super.onStop();
delegate.onStop();
}
@Override
protected void onPostResume() {
super.onPostResume();
getDelegate().onPostResume();
}
@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
getDelegate().setContentView(view, params);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
NavUtils.navigateUpFromSameTask(this);
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
@NonNull
public MenuInflater getMenuInflater() {
return getDelegate().getMenuInflater();
}
public void askUserForTwoFactorAuth() {
progressDialog.dismiss();
binding.twoFactorContainer.setVisibility(VISIBLE);
binding.loginTwoFactor.setVisibility(VISIBLE);
binding.loginTwoFactor.requestFocus();
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY);
showMessageAndCancelDialog(R.string.login_failed_2fa_needed);
}
public void showMessageAndCancelDialog(@StringRes int resId) {
showMessage(resId, R.color.secondaryDarkColor);
if (progressDialog != null) {
progressDialog.cancel();
}
}
public void showMessageAndCancelDialog(String error) {
showMessage(error, R.color.secondaryDarkColor);
if (progressDialog != null) {
progressDialog.cancel();
}
}
public void showSuccessAndDismissDialog() {
showMessage(R.string.login_success, R.color.primaryDarkColor);
progressDialog.dismiss();
}
public void startMainActivity() {
ActivityUtils.startActivityWithFlags(this, MainActivity.class, Intent.FLAG_ACTIVITY_SINGLE_TOP);
finish();
}
private void showMessage(@StringRes int resId, @ColorRes int colorResId) {
binding.errorMessage.setText(getString(resId));
binding.errorMessage.setTextColor(ContextCompat.getColor(this, colorResId));
binding.errorMessageContainer.setVisibility(VISIBLE);
}
private void showMessage(String message, @ColorRes int colorResId) {
binding.errorMessage.setText(message);
binding.errorMessage.setTextColor(ContextCompat.getColor(this, colorResId));
binding.errorMessageContainer.setVisibility(VISIBLE);
}
private AppCompatDelegate getDelegate() {
if (delegate == null) {
delegate = AppCompatDelegate.create(this, null);
}
return delegate;
}
private class LoginTextWatcher implements TextWatcher {
@Override
public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence charSequence, int start, int count, int after) {
}
@Override
public void afterTextChanged(Editable editable) {
boolean enabled = binding.loginUsername.getText().length() != 0 &&
binding.loginPassword.getText().length() != 0 &&
(BuildConfig.DEBUG || binding.loginTwoFactor.getText().length() != 0 ||
binding.loginTwoFactor.getVisibility() != VISIBLE);
binding.loginButton.setEnabled(enabled);
}
}
public static void startYourself(Context context) {
Intent intent = new Intent(context, LoginActivity.class);
context.startActivity(intent);
}
@Override
protected void onSaveInstanceState(Bundle outState) {
// if progressDialog is visible during the configuration change then store state as true else false so that
// we maintain visibility of progressDailog after configuration change
if(progressDialog!=null&&progressDialog.isShowing()) {
outState.putBoolean(saveProgressDailog,true);
} else {
outState.putBoolean(saveProgressDailog,false);
}
outState.putString(saveErrorMessage,binding.errorMessage.getText().toString()); //Save the errorMessage
outState.putString(saveUsername,getUsername()); // Save the username
outState.putString(savePassword,getPassword()); // Save the password
}
private String getUsername() {
return binding.loginUsername.getText().toString();
}
private String getPassword(){
return binding.loginPassword.getText().toString();
}
@Override
protected void onRestoreInstanceState(final Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
binding.loginUsername.setText(savedInstanceState.getString(saveUsername));
binding.loginPassword.setText(savedInstanceState.getString(savePassword));
if(savedInstanceState.getBoolean(saveProgressDailog)) {
performLogin();
}
String errorMessage=savedInstanceState.getString(saveErrorMessage);
if(sessionManager.isUserLoggedIn()) {
showMessage(R.string.login_success, R.color.primaryDarkColor);
} else {
showMessage(errorMessage, R.color.secondaryDarkColor);
}
}
}

View file

@ -0,0 +1,489 @@
package fr.free.nrw.commons.auth
import android.accounts.AccountAuthenticatorActivity
import android.app.ProgressDialog
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.KeyEvent
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.TextView
import androidx.annotation.ColorRes
import androidx.annotation.StringRes
import androidx.annotation.VisibleForTesting
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.app.NavUtils
import androidx.core.content.ContextCompat
import androidx.core.view.WindowCompat
import fr.free.nrw.commons.BuildConfig
import fr.free.nrw.commons.CommonsApplication
import fr.free.nrw.commons.R
import fr.free.nrw.commons.auth.login.LoginCallback
import fr.free.nrw.commons.auth.login.LoginClient
import fr.free.nrw.commons.auth.login.LoginResult
import fr.free.nrw.commons.contributions.MainActivity
import fr.free.nrw.commons.databinding.ActivityLoginBinding
import fr.free.nrw.commons.di.ApplicationlessInjection
import fr.free.nrw.commons.kvstore.JsonKvStore
import fr.free.nrw.commons.utils.applyEdgeToEdgeAllInsets
import fr.free.nrw.commons.utils.AbstractTextWatcher
import fr.free.nrw.commons.utils.ActivityUtils.startActivityWithFlags
import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour
import fr.free.nrw.commons.utils.SystemThemeUtils
import fr.free.nrw.commons.utils.ViewUtil.hideKeyboard
import fr.free.nrw.commons.utils.handleKeyboardInsets
import fr.free.nrw.commons.utils.handleWebUrl
import io.reactivex.disposables.CompositeDisposable
import timber.log.Timber
import java.util.Locale
import javax.inject.Inject
import javax.inject.Named
class LoginActivity : AccountAuthenticatorActivity() {
@Inject
lateinit var sessionManager: SessionManager
@Inject
@field:Named("default_preferences")
lateinit var applicationKvStore: JsonKvStore
@Inject
lateinit var loginClient: LoginClient
@Inject
lateinit var systemThemeUtils: SystemThemeUtils
private var binding: ActivityLoginBinding? = null
private var progressDialog: ProgressDialog? = null
private val textWatcher = AbstractTextWatcher(::onTextChanged)
private val compositeDisposable = CompositeDisposable()
private val delegate: AppCompatDelegate by lazy {
AppCompatDelegate.create(this, null)
}
private var lastLoginResult: LoginResult? = null
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ApplicationlessInjection
.getInstance(this.applicationContext)
.commonsApplicationComponent
.inject(this)
val isDarkTheme = systemThemeUtils.isDeviceInNightMode()
setTheme(if (isDarkTheme) R.style.DarkAppTheme else R.style.LightAppTheme)
delegate.installViewFactory()
delegate.onCreate(savedInstanceState)
WindowCompat.getInsetsController(window, window.decorView)
.isAppearanceLightStatusBars = !isDarkTheme
WindowCompat.setDecorFitsSystemWindows(window, false)
binding = ActivityLoginBinding.inflate(layoutInflater)
applyEdgeToEdgeAllInsets(binding!!.root)
binding!!.root.handleKeyboardInsets()
with(binding!!) {
setContentView(root)
loginUsername.addTextChangedListener(textWatcher)
loginPassword.addTextChangedListener(textWatcher)
loginTwoFactor.addTextChangedListener(textWatcher)
skipLogin.setOnClickListener { skipLogin() }
forgotPassword.setOnClickListener { forgotPassword() }
aboutPrivacyPolicy.setOnClickListener { onPrivacyPolicyClicked() }
signUpButton.setOnClickListener { signUp() }
loginButton.setOnClickListener { performLogin() }
loginPassword.setOnEditorActionListener { textView, actionId, keyEvent ->
if (binding!!.loginButton.isEnabled && isTriggerAction(actionId, keyEvent)) {
if (actionId == EditorInfo.IME_ACTION_NEXT && lastLoginResult != null) {
askUserForTwoFactorAuthWithKeyboard()
true
} else {
performLogin()
true
}
} else {
false
}
}
loginPassword.onFocusChangeListener =
View.OnFocusChangeListener(::onPasswordFocusChanged)
if (isBetaFlavour) {
loginCredentials.text = getString(R.string.login_credential)
} else {
loginCredentials.visibility = View.GONE
}
intent.getStringExtra(CommonsApplication.LOGIN_MESSAGE_INTENT_KEY)?.let {
showMessage(it, R.color.secondaryDarkColor)
}
intent.getStringExtra(CommonsApplication.LOGIN_USERNAME_INTENT_KEY)?.let {
loginUsername.setText(it)
}
}
}
@VisibleForTesting
fun askUserForTwoFactorAuthWithKeyboard() {
if (binding == null) {
Timber.w("Binding is null, reinitializing in askUserForTwoFactorAuthWithKeyboard")
binding = ActivityLoginBinding.inflate(layoutInflater)
setContentView(binding!!.root)
}
progressDialog!!.dismiss()
if (binding != null) {
with(binding!!) {
twoFactorContainer.visibility = View.VISIBLE
twoFactorContainer.hint = getString(if (lastLoginResult is LoginResult.EmailAuthResult) R.string.email_auth_code else R.string._2fa_code)
loginTwoFactor.visibility = View.VISIBLE
loginTwoFactor.requestFocus()
val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(loginTwoFactor, InputMethodManager.SHOW_IMPLICIT)
loginTwoFactor.setOnEditorActionListener { _, actionId, event ->
if (actionId == EditorInfo.IME_ACTION_DONE ||
(event != null && event.keyCode == KeyEvent.KEYCODE_ENTER && event.action == KeyEvent.ACTION_DOWN)) {
performLogin()
true
} else {
false
}
}
}
} else {
Timber.e("Binding is null in askUserForTwoFactorAuthWithKeyboard after reinitialization attempt")
}
showMessageAndCancelDialog(getString(if (lastLoginResult is LoginResult.EmailAuthResult) R.string.login_failed_email_auth_needed else R.string.login_failed_2fa_needed))
}
override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState)
delegate.onPostCreate(savedInstanceState)
}
override fun onResume() {
super.onResume()
if (sessionManager.currentAccount != null && sessionManager.isUserLoggedIn) {
applicationKvStore.putBoolean("login_skipped", false)
startMainActivity()
}
if (applicationKvStore.getBoolean("login_skipped", false)) {
performSkipLogin()
}
}
override fun onDestroy() {
compositeDisposable.clear()
try {
// To prevent leaked window when finish() is called, see http://stackoverflow.com/questions/32065854/activity-has-leaked-window-at-alertdialog-show-method
if (progressDialog?.isShowing == true) {
progressDialog!!.dismiss()
}
} catch (e: Exception) {
e.printStackTrace()
}
with(binding!!) {
loginUsername.removeTextChangedListener(textWatcher)
loginPassword.removeTextChangedListener(textWatcher)
loginTwoFactor.removeTextChangedListener(textWatcher)
}
delegate.onDestroy()
loginClient.cancel()
binding = null
super.onDestroy()
}
override fun onStart() {
super.onStart()
delegate.onStart()
}
override fun onStop() {
super.onStop()
delegate.onStop()
}
override fun onPostResume() {
super.onPostResume()
delegate.onPostResume()
}
override fun setContentView(view: View, params: ViewGroup.LayoutParams) {
delegate.setContentView(view, params)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
NavUtils.navigateUpFromSameTask(this)
return true
}
}
return super.onOptionsItemSelected(item)
}
override fun onSaveInstanceState(outState: Bundle) {
// if progressDialog is visible during the configuration change then store state as true else false so that
// we maintain visibility of progressDialog after configuration change
if (progressDialog != null && progressDialog!!.isShowing) {
outState.putBoolean(SAVE_PROGRESS_DIALOG, true)
} else {
outState.putBoolean(SAVE_PROGRESS_DIALOG, false)
}
outState.putString(
SAVE_ERROR_MESSAGE,
binding!!.errorMessage.text.toString()
) //Save the errorMessage
outState.putString(
SAVE_USERNAME,
binding!!.loginUsername.text.toString()
) // Save the username
outState.putString(
SAVE_PASSWORD,
binding!!.loginPassword.text.toString()
) // Save the password
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
binding!!.loginUsername.setText(savedInstanceState.getString(SAVE_USERNAME))
binding!!.loginPassword.setText(savedInstanceState.getString(SAVE_PASSWORD))
if (savedInstanceState.getBoolean(SAVE_PROGRESS_DIALOG)) {
performLogin()
}
val errorMessage = savedInstanceState.getString(SAVE_ERROR_MESSAGE)
if (sessionManager.isUserLoggedIn) {
showMessage(R.string.login_success, R.color.primaryDarkColor)
} else {
showMessage(errorMessage, R.color.secondaryDarkColor)
}
}
/**
* Hides the keyboard if the user's focus is not on the password (hasFocus is false).
* @param view The keyboard
* @param hasFocus Set to true if the keyboard has focus
*/
private fun onPasswordFocusChanged(view: View, hasFocus: Boolean) {
if (!hasFocus) {
hideKeyboard(view)
}
}
private fun onEditorAction(textView: TextView, actionId: Int, keyEvent: KeyEvent?) =
if (binding!!.loginButton.isEnabled && isTriggerAction(actionId, keyEvent)) {
performLogin()
true
} else false
private fun isTriggerAction(actionId: Int, keyEvent: KeyEvent?) =
actionId == EditorInfo.IME_ACTION_NEXT || actionId == EditorInfo.IME_ACTION_DONE || keyEvent?.keyCode == KeyEvent.KEYCODE_ENTER
private fun skipLogin() {
AlertDialog.Builder(this)
.setTitle(R.string.skip_login_title)
.setMessage(R.string.skip_login_message)
.setCancelable(false)
.setPositiveButton(R.string.yes) { dialog: DialogInterface, which: Int ->
dialog.cancel()
performSkipLogin()
}
.setNegativeButton(R.string.no) { dialog: DialogInterface, which: Int ->
dialog.cancel()
}
.show()
}
private fun forgotPassword() =
handleWebUrl(this, Uri.parse(BuildConfig.FORGOT_PASSWORD_URL))
private fun onPrivacyPolicyClicked() =
handleWebUrl(this, Uri.parse(BuildConfig.PRIVACY_POLICY_URL))
private fun signUp() =
startActivity(Intent(this, SignupActivity::class.java))
@VisibleForTesting
fun performLogin() {
Timber.d("Login to start!")
val username = binding!!.loginUsername.text.toString()
val password = binding!!.loginPassword.text.toString()
val twoFactorCode = binding!!.loginTwoFactor.text.toString()
showLoggingProgressBar()
loginClient.doLogin(username,
password,
lastLoginResult,
twoFactorCode,
Locale.getDefault().language,
object : LoginCallback {
override fun success(loginResult: LoginResult) = runOnUiThread {
Timber.d("Login Success")
progressDialog!!.dismiss()
onLoginSuccess(loginResult)
}
override fun twoFactorPrompt(loginResult: LoginResult, caught: Throwable, token: String?) = runOnUiThread {
Timber.d("Requesting 2FA prompt")
progressDialog!!.dismiss()
lastLoginResult = loginResult
askUserForTwoFactorAuthWithKeyboard()
}
override fun emailAuthPrompt(loginResult: LoginResult, caught: Throwable, token: String?) = runOnUiThread {
Timber.d("Requesting email auth prompt")
progressDialog!!.dismiss()
lastLoginResult = loginResult
askUserForTwoFactorAuthWithKeyboard()
}
override fun passwordResetPrompt(token: String?) = runOnUiThread {
Timber.d("Showing password reset prompt")
progressDialog!!.dismiss()
showPasswordResetPrompt()
}
override fun error(caught: Throwable) = runOnUiThread {
Timber.e(caught)
progressDialog!!.dismiss()
showMessageAndCancelDialog(caught.localizedMessage ?: "")
}
}
)
}
private fun showPasswordResetPrompt() =
showMessageAndCancelDialog(getString(R.string.you_must_reset_your_passsword))
/**
* This function is called when user skips the login.
* It redirects the user to Explore Activity.
*/
private fun performSkipLogin() {
applicationKvStore.putBoolean("login_skipped", true)
MainActivity.startYourself(this)
finish()
}
private fun showLoggingProgressBar() {
progressDialog = ProgressDialog(this).apply {
isIndeterminate = true
setTitle(getString(R.string.logging_in_title))
setMessage(getString(R.string.logging_in_message))
setCancelable(false)
}
progressDialog!!.show()
}
private fun onLoginSuccess(loginResult: LoginResult) {
compositeDisposable.clear()
sessionManager.setUserLoggedIn(true)
sessionManager.updateAccount(loginResult)
progressDialog!!.dismiss()
showSuccessAndDismissDialog()
startMainActivity()
}
override fun getMenuInflater(): MenuInflater =
delegate.menuInflater
@VisibleForTesting
fun askUserForTwoFactorAuth() {
if (binding == null) {
Timber.w("Binding is null, reinitializing in askUserForTwoFactorAuth")
binding = ActivityLoginBinding.inflate(layoutInflater)
setContentView(binding!!.root)
}
progressDialog!!.dismiss()
if (binding != null) {
with(binding!!) {
twoFactorContainer.visibility = View.VISIBLE
twoFactorContainer.hint = getString(if (lastLoginResult is LoginResult.EmailAuthResult) R.string.email_auth_code else R.string._2fa_code)
loginTwoFactor.visibility = View.VISIBLE
loginTwoFactor.requestFocus()
loginTwoFactor.setOnEditorActionListener { _, actionId, event ->
if (actionId == EditorInfo.IME_ACTION_DONE ||
(event != null && event.keyCode == KeyEvent.KEYCODE_ENTER && event.action == KeyEvent.ACTION_DOWN)) {
performLogin()
true
} else {
false
}
}
}
} else {
Timber.e("Binding is null in askUserForTwoFactorAuth after reinitialization attempt")
}
val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY)
showMessageAndCancelDialog(getString(if (lastLoginResult is LoginResult.EmailAuthResult) R.string.login_failed_email_auth_needed else R.string.login_failed_2fa_needed))
}
@VisibleForTesting
fun showMessageAndCancelDialog(@StringRes resId: Int) {
showMessage(resId, R.color.secondaryDarkColor)
progressDialog?.cancel()
}
@VisibleForTesting
fun showMessageAndCancelDialog(error: String) {
showMessage(error, R.color.secondaryDarkColor)
progressDialog?.cancel()
}
@VisibleForTesting
fun showSuccessAndDismissDialog() {
showMessage(R.string.login_success, R.color.primaryDarkColor)
progressDialog!!.dismiss()
}
@VisibleForTesting
fun startMainActivity() {
startActivityWithFlags(this, MainActivity::class.java, Intent.FLAG_ACTIVITY_SINGLE_TOP)
finish()
}
private fun showMessage(@StringRes resId: Int, @ColorRes colorResId: Int) = with(binding!!) {
errorMessage.text = getString(resId)
errorMessage.setTextColor(ContextCompat.getColor(this@LoginActivity, colorResId))
errorMessageContainer.visibility = View.VISIBLE
}
private fun showMessage(message: String?, @ColorRes colorResId: Int) = with(binding!!) {
errorMessage.text = message
errorMessage.setTextColor(ContextCompat.getColor(this@LoginActivity, colorResId))
errorMessageContainer.visibility = View.VISIBLE
}
private fun onTextChanged(text: String) {
val enabled =
binding!!.loginUsername.text!!.length != 0 && binding!!.loginPassword.text!!.length != 0 &&
(BuildConfig.DEBUG || binding!!.loginTwoFactor.text!!.length != 0 || binding!!.loginTwoFactor.visibility != View.VISIBLE)
binding!!.loginButton.isEnabled = enabled
}
companion object {
fun startYourself(context: Context) =
context.startActivity(Intent(context, LoginActivity::class.java))
const val SAVE_PROGRESS_DIALOG: String = "ProgressDialog_state"
const val SAVE_ERROR_MESSAGE: String = "errorMessage"
const val SAVE_USERNAME: String = "username"
const val SAVE_PASSWORD: String = "password"
}
}

View file

@ -1,148 +0,0 @@
package fr.free.nrw.commons.auth;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.Context;
import android.os.Build;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import fr.free.nrw.commons.auth.login.LoginResult;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import io.reactivex.Completable;
import io.reactivex.Observable;
/**
* Manage the current logged in user session.
*/
@Singleton
public class SessionManager {
private final Context context;
private Account currentAccount; // Unlike a savings account... ;-)
private JsonKvStore defaultKvStore;
@Inject
public SessionManager(Context context,
@Named("default_preferences") JsonKvStore defaultKvStore) {
this.context = context;
this.currentAccount = null;
this.defaultKvStore = defaultKvStore;
}
private boolean createAccount(@NonNull String userName, @NonNull String password) {
Account account = getCurrentAccount();
if (account == null || TextUtils.isEmpty(account.name) || !account.name.equals(userName)) {
removeAccount();
account = new Account(userName, BuildConfig.ACCOUNT_TYPE);
return accountManager().addAccountExplicitly(account, password, null);
}
return true;
}
private void removeAccount() {
Account account = getCurrentAccount();
if (account != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
accountManager().removeAccountExplicitly(account);
} else {
//noinspection deprecation
accountManager().removeAccount(account, null, null);
}
}
}
public void updateAccount(LoginResult result) {
boolean accountCreated = createAccount(result.getUserName(), result.getPassword());
if (accountCreated) {
setPassword(result.getPassword());
}
}
private void setPassword(@NonNull String password) {
Account account = getCurrentAccount();
if (account != null) {
accountManager().setPassword(account, password);
}
}
/**
* @return Account|null
*/
@Nullable
public Account getCurrentAccount() {
if (currentAccount == null) {
AccountManager accountManager = AccountManager.get(context);
Account[] allAccounts = accountManager.getAccountsByType(BuildConfig.ACCOUNT_TYPE);
if (allAccounts.length != 0) {
currentAccount = allAccounts[0];
}
}
return currentAccount;
}
public boolean doesAccountExist() {
return getCurrentAccount() != null;
}
@Nullable
public String getUserName() {
Account account = getCurrentAccount();
return account == null ? null : account.name;
}
@Nullable
public String getPassword() {
Account account = getCurrentAccount();
return account == null ? null : accountManager().getPassword(account);
}
private AccountManager accountManager() {
return AccountManager.get(context);
}
public boolean isUserLoggedIn() {
return defaultKvStore.getBoolean("isUserLoggedIn", false);
}
void setUserLoggedIn(boolean isLoggedIn) {
defaultKvStore.putBoolean("isUserLoggedIn", isLoggedIn);
}
public void forceLogin(Context context) {
if (context != null) {
LoginActivity.startYourself(context);
}
}
/**
* 1. Clears existing accounts from account manager
* 2. Calls MediaWikiApi's logout function to clear cookies
* @return
*/
public Completable logout() {
AccountManager accountManager = AccountManager.get(context);
Account[] allAccounts = accountManager.getAccountsByType(BuildConfig.ACCOUNT_TYPE);
return Completable.fromObservable(Observable.fromArray(allAccounts)
.map(a -> accountManager.removeAccount(a, null, null).getResult()))
.doOnComplete(() -> {
currentAccount = null;
});
}
/**
* Return a corresponding boolean preference
*
* @param key
* @return
*/
public boolean getPreference(String key) {
return defaultKvStore.getBoolean(key);
}
}

View file

@ -0,0 +1,95 @@
package fr.free.nrw.commons.auth
import android.accounts.Account
import android.accounts.AccountManager
import android.content.Context
import android.os.Build
import android.text.TextUtils
import fr.free.nrw.commons.BuildConfig.ACCOUNT_TYPE
import fr.free.nrw.commons.auth.login.LoginResult
import fr.free.nrw.commons.kvstore.JsonKvStore
import io.reactivex.Completable
import io.reactivex.Observable
import javax.inject.Inject
import javax.inject.Named
import javax.inject.Singleton
/**
* Manage the current logged in user session.
*/
@Singleton
class SessionManager @Inject constructor(
private val context: Context,
@param:Named("default_preferences") private val defaultKvStore: JsonKvStore
) {
private val accountManager: AccountManager get() = AccountManager.get(context)
private var _currentAccount: Account? = null // Unlike a savings account... ;-)
val currentAccount: Account? get() {
if (_currentAccount == null) {
val allAccounts = AccountManager.get(context).getAccountsByType(ACCOUNT_TYPE)
if (allAccounts.isNotEmpty()) {
_currentAccount = allAccounts[0]
}
}
return _currentAccount
}
val userName: String?
get() = currentAccount?.name
var password: String?
get() = currentAccount?.let { accountManager.getPassword(it) }
private set(value) {
currentAccount?.let { accountManager.setPassword(it, value) }
}
val isUserLoggedIn: Boolean
get() = defaultKvStore.getBoolean("isUserLoggedIn", false)
fun updateAccount(result: LoginResult) {
if (createAccount(result.userName!!, result.password!!)) {
password = result.password
}
}
fun doesAccountExist(): Boolean =
currentAccount != null
fun setUserLoggedIn(isLoggedIn: Boolean) =
defaultKvStore.putBoolean("isUserLoggedIn", isLoggedIn)
fun forceLogin(context: Context?) =
context?.let { LoginActivity.startYourself(it) }
fun getPreference(key: String): Boolean =
defaultKvStore.getBoolean(key)
fun logout(): Completable = Completable.fromObservable(
Observable.empty<Any>()
.doOnComplete {
removeAccount()
_currentAccount = null
}
)
private fun createAccount(userName: String, password: String): Boolean {
var account = currentAccount
if (account == null || TextUtils.isEmpty(account.name) || account.name != userName) {
removeAccount()
account = Account(userName, ACCOUNT_TYPE)
return accountManager.addAccountExplicitly(account, password, null)
}
return true
}
private fun removeAccount() {
currentAccount?.let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
accountManager.removeAccountExplicitly(it)
} else {
accountManager.removeAccount(it, null, null)
}
}
}
}

View file

@ -1,82 +0,0 @@
package fr.free.nrw.commons.auth;
import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Toast;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.theme.BaseActivity;
import timber.log.Timber;
public class SignupActivity extends BaseActivity {
private WebView webView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Timber.d("Signup Activity started");
webView = new WebView(this);
setContentView(webView);
webView.setWebViewClient(new MyWebViewClient());
WebSettings webSettings = webView.getSettings();
/*Needed to refresh Captcha. Might introduce XSS vulnerabilities, but we can
trust Wikimedia's site... right?*/
webSettings.setJavaScriptEnabled(true);
webView.loadUrl(BuildConfig.SIGNUP_LANDING_URL);
}
private class MyWebViewClient extends WebViewClient {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url.equals(BuildConfig.SIGNUP_SUCCESS_REDIRECTION_URL)) {
//Signup success, so clear cookies, notify user, and load LoginActivity again
Timber.d("Overriding URL %s", url);
Toast toast = Toast.makeText(SignupActivity.this,
R.string.account_created, Toast.LENGTH_LONG);
toast.show();
// terminate on task completion.
finish();
return true;
} else {
//If user clicks any other links in the webview
Timber.d("Not overriding URL, URL is: %s", url);
return false;
}
}
}
@Override
public void onBackPressed() {
if (webView.canGoBack()) {
webView.goBack();
} else {
super.onBackPressed();
}
}
/**
* Known bug in androidx.appcompat library version 1.1.0 being tracked here
* https://issuetracker.google.com/issues/141132133
* App tries to put light/dark theme to webview and crashes in the process
* This code tries to prevent applying the theme when sdk is between api 21 to 25
* @param overrideConfiguration
*/
@Override
public void applyOverrideConfiguration(final Configuration overrideConfiguration) {
if (Build.VERSION.SDK_INT <= 25 &&
(getResources().getConfiguration().uiMode == getApplicationContext().getResources().getConfiguration().uiMode)) {
return;
}
super.applyOverrideConfiguration(overrideConfiguration);
}
}

View file

@ -0,0 +1,77 @@
package fr.free.nrw.commons.auth
import android.annotation.SuppressLint
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.Toast
import fr.free.nrw.commons.BuildConfig
import fr.free.nrw.commons.R
import fr.free.nrw.commons.theme.BaseActivity
import fr.free.nrw.commons.utils.applyEdgeToEdgeAllInsets
import timber.log.Timber
class SignupActivity : BaseActivity() {
private var webView: WebView? = null
@SuppressLint("SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Timber.d("Signup Activity started")
webView = WebView(this)
applyEdgeToEdgeAllInsets(webView!!)
with(webView!!) {
setContentView(this)
webViewClient = MyWebViewClient()
// Needed to refresh Captcha. Might introduce XSS vulnerabilities, but we can
// trust Wikimedia's site... right?
settings.javaScriptEnabled = true
loadUrl(BuildConfig.SIGNUP_LANDING_URL)
}
}
override fun onBackPressed() {
if (webView!!.canGoBack()) {
webView!!.goBack()
} else {
super.onBackPressed()
}
}
/**
* Known bug in androidx.appcompat library version 1.1.0 being tracked here
* https://issuetracker.google.com/issues/141132133
* App tries to put light/dark theme to webview and crashes in the process
* This code tries to prevent applying the theme when sdk is between api 21 to 25
*/
override fun applyOverrideConfiguration(overrideConfiguration: Configuration) {
if (Build.VERSION.SDK_INT <= 25 &&
(resources.configuration.uiMode == applicationContext.resources.configuration.uiMode)
) return
super.applyOverrideConfiguration(overrideConfiguration)
}
private inner class MyWebViewClient : WebViewClient() {
@Deprecated("Deprecated in Java")
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean =
if (url == BuildConfig.SIGNUP_SUCCESS_REDIRECTION_URL) {
//Signup success, so clear cookies, notify user, and load LoginActivity again
Timber.d("Overriding URL %s", url)
Toast.makeText(
this@SignupActivity, R.string.account_created, Toast.LENGTH_LONG
).show()
// terminate on task completion.
finish()
true
} else {
//If user clicks any other links in the webview
Timber.d("Not overriding URL, URL is: %s", url)
false
}
}
}

View file

@ -1,141 +0,0 @@
package fr.free.nrw.commons.auth;
import android.accounts.AbstractAccountAuthenticator;
import android.accounts.Account;
import android.accounts.AccountAuthenticatorResponse;
import android.accounts.AccountManager;
import android.accounts.NetworkErrorException;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import fr.free.nrw.commons.BuildConfig;
import static fr.free.nrw.commons.auth.AccountUtil.AUTH_TOKEN_TYPE;
/**
* Handles WikiMedia commons account Authentication
*/
public class WikiAccountAuthenticator extends AbstractAccountAuthenticator {
private static final String[] SYNC_AUTHORITIES = {BuildConfig.CONTRIBUTION_AUTHORITY, BuildConfig.MODIFICATION_AUTHORITY};
@NonNull
private final Context context;
public WikiAccountAuthenticator(@NonNull Context context) {
super(context);
this.context = context;
}
/**
* Provides Bundle with edited Account Properties
*/
@Override
public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
Bundle bundle = new Bundle();
bundle.putString("test", "editProperties");
return bundle;
}
@Override
public Bundle addAccount(@NonNull AccountAuthenticatorResponse response,
@NonNull String accountType, @Nullable String authTokenType,
@Nullable String[] requiredFeatures, @Nullable Bundle options)
throws NetworkErrorException {
// account type not supported returns bundle without loginActivity Intent, it just contains "test" key
if (!supportedAccountType(accountType)) {
Bundle bundle = new Bundle();
bundle.putString("test", "addAccount");
return bundle;
}
return addAccount(response);
}
@Override
public Bundle confirmCredentials(@NonNull AccountAuthenticatorResponse response,
@NonNull Account account, @Nullable Bundle options)
throws NetworkErrorException {
Bundle bundle = new Bundle();
bundle.putString("test", "confirmCredentials");
return bundle;
}
@Override
public Bundle getAuthToken(@NonNull AccountAuthenticatorResponse response,
@NonNull Account account, @NonNull String authTokenType,
@Nullable Bundle options)
throws NetworkErrorException {
Bundle bundle = new Bundle();
bundle.putString("test", "getAuthToken");
return bundle;
}
@Nullable
@Override
public String getAuthTokenLabel(@NonNull String authTokenType) {
return supportedAccountType(authTokenType) ? AUTH_TOKEN_TYPE : null;
}
@Nullable
@Override
public Bundle updateCredentials(@NonNull AccountAuthenticatorResponse response,
@NonNull Account account, @Nullable String authTokenType,
@Nullable Bundle options)
throws NetworkErrorException {
Bundle bundle = new Bundle();
bundle.putString("test", "updateCredentials");
return bundle;
}
@Nullable
@Override
public Bundle hasFeatures(@NonNull AccountAuthenticatorResponse response,
@NonNull Account account, @NonNull String[] features)
throws NetworkErrorException {
Bundle bundle = new Bundle();
bundle.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false);
return bundle;
}
private boolean supportedAccountType(@Nullable String type) {
return BuildConfig.ACCOUNT_TYPE.equals(type);
}
/**
* Provides a bundle containing a Parcel
* the Parcel packs an Intent with LoginActivity and Authenticator response (requires valid account type)
*/
private Bundle addAccount(AccountAuthenticatorResponse response) {
Intent intent = new Intent(context, LoginActivity.class);
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
Bundle bundle = new Bundle();
bundle.putParcelable(AccountManager.KEY_INTENT, intent);
return bundle;
}
@Override
public Bundle getAccountRemovalAllowed(AccountAuthenticatorResponse response,
Account account) throws NetworkErrorException {
Bundle result = super.getAccountRemovalAllowed(response, account);
if (result.containsKey(AccountManager.KEY_BOOLEAN_RESULT)
&& !result.containsKey(AccountManager.KEY_INTENT)) {
boolean allowed = result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT);
if (allowed) {
for (String auth : SYNC_AUTHORITIES) {
ContentResolver.cancelSync(account, auth);
}
}
}
return result;
}
}

View file

@ -0,0 +1,108 @@
package fr.free.nrw.commons.auth
import android.accounts.AbstractAccountAuthenticator
import android.accounts.Account
import android.accounts.AccountAuthenticatorResponse
import android.accounts.AccountManager
import android.accounts.NetworkErrorException
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.core.os.bundleOf
import fr.free.nrw.commons.BuildConfig
private val SYNC_AUTHORITIES = arrayOf(
BuildConfig.CONTRIBUTION_AUTHORITY, BuildConfig.MODIFICATION_AUTHORITY
)
/**
* Handles WikiMedia commons account Authentication
*/
class WikiAccountAuthenticator(
private val context: Context
) : AbstractAccountAuthenticator(context) {
/**
* Provides Bundle with edited Account Properties
*/
override fun editProperties(
response: AccountAuthenticatorResponse,
accountType: String
) = bundleOf("test" to "editProperties")
// account type not supported returns bundle without loginActivity Intent, it just contains "test" key
@Throws(NetworkErrorException::class)
override fun addAccount(
response: AccountAuthenticatorResponse,
accountType: String,
authTokenType: String?,
requiredFeatures: Array<String>?,
options: Bundle?
) = if (BuildConfig.ACCOUNT_TYPE == accountType) {
addAccount(response)
} else {
bundleOf("test" to "addAccount")
}
@Throws(NetworkErrorException::class)
override fun confirmCredentials(
response: AccountAuthenticatorResponse, account: Account, options: Bundle?
) = bundleOf("test" to "confirmCredentials")
@Throws(NetworkErrorException::class)
override fun getAuthToken(
response: AccountAuthenticatorResponse,
account: Account,
authTokenType: String,
options: Bundle?
) = bundleOf("test" to "getAuthToken")
override fun getAuthTokenLabel(authTokenType: String) =
if (BuildConfig.ACCOUNT_TYPE == authTokenType) AUTH_TOKEN_TYPE else null
@Throws(NetworkErrorException::class)
override fun updateCredentials(
response: AccountAuthenticatorResponse,
account: Account,
authTokenType: String?,
options: Bundle?
) = bundleOf("test" to "updateCredentials")
@Throws(NetworkErrorException::class)
override fun hasFeatures(
response: AccountAuthenticatorResponse,
account: Account, features: Array<String>
) = bundleOf(AccountManager.KEY_BOOLEAN_RESULT to false)
/**
* Provides a bundle containing a Parcel
* the Parcel packs an Intent with LoginActivity and Authenticator response (requires valid account type)
*/
private fun addAccount(response: AccountAuthenticatorResponse): Bundle {
val intent = Intent(context, LoginActivity::class.java)
.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response)
return bundleOf(AccountManager.KEY_INTENT to intent)
}
@Throws(NetworkErrorException::class)
override fun getAccountRemovalAllowed(
response: AccountAuthenticatorResponse?,
account: Account?
): Bundle {
val result = super.getAccountRemovalAllowed(response, account)
if (result.containsKey(AccountManager.KEY_BOOLEAN_RESULT)
&& !result.containsKey(AccountManager.KEY_INTENT)
) {
val allowed = result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT)
if (allowed) {
for (auth in SYNC_AUTHORITIES) {
ContentResolver.cancelSync(account, auth)
}
}
}
return result
}
}

View file

@ -1,31 +0,0 @@
package fr.free.nrw.commons.auth;
import android.accounts.AbstractAccountAuthenticator;
import android.content.Intent;
import android.os.IBinder;
import androidx.annotation.Nullable;
import fr.free.nrw.commons.di.CommonsDaggerService;
/**
* Handles the Auth service of the App, see AndroidManifests for details
* (Uses Dagger 2 as injector)
*/
public class WikiAccountAuthenticatorService extends CommonsDaggerService {
@Nullable
private AbstractAccountAuthenticator authenticator;
@Override
public void onCreate() {
super.onCreate();
authenticator = new WikiAccountAuthenticator(this);
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return authenticator == null ? null : authenticator.getIBinder();
}
}

View file

@ -0,0 +1,22 @@
package fr.free.nrw.commons.auth
import android.accounts.AbstractAccountAuthenticator
import android.content.Intent
import android.os.IBinder
import fr.free.nrw.commons.di.CommonsDaggerService
/**
* Handles the Auth service of the App, see AndroidManifests for details
* (Uses Dagger 2 as injector)
*/
class WikiAccountAuthenticatorService : CommonsDaggerService() {
private var authenticator: AbstractAccountAuthenticator? = null
override fun onCreate() {
super.onCreate()
authenticator = WikiAccountAuthenticator(this)
}
override fun onBind(intent: Intent): IBinder? =
authenticator?.iBinder
}

View file

@ -2,15 +2,14 @@ package fr.free.nrw.commons.auth.csrf
import androidx.annotation.VisibleForTesting
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
import fr.free.nrw.commons.auth.login.LoginClient
import fr.free.nrw.commons.auth.login.LoginCallback
import fr.free.nrw.commons.auth.login.LoginClient
import fr.free.nrw.commons.auth.login.LoginFailedException
import fr.free.nrw.commons.auth.login.LoginResult
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
import retrofit2.Call
import retrofit2.Response
import timber.log.Timber
import java.io.IOException
import java.util.concurrent.Callable
import java.util.concurrent.Executors.newSingleThreadExecutor
@ -18,12 +17,11 @@ class CsrfTokenClient(
private val sessionManager: SessionManager,
private val csrfTokenInterface: CsrfTokenInterface,
private val loginClient: LoginClient,
private val logoutClient: LogoutClient
private val logoutClient: LogoutClient,
) {
private var retries = 0
private var csrfTokenCall: Call<MwQueryResponse?>? = null
@Throws(Throwable::class)
fun getTokenBlocking(): String {
var token = ""
@ -34,15 +32,24 @@ class CsrfTokenClient(
try {
if (retry > 0) {
// Log in explicitly
loginClient.loginBlocking(userName, password, "")
loginClient.loginBlocking(userName, password)
}
// Get CSRFToken response off the main thread.
val response = newSingleThreadExecutor().submit(Callable {
csrfTokenInterface.getCsrfTokenCall().execute()
}).get()
val response =
newSingleThreadExecutor()
.submit(
Callable {
csrfTokenInterface.getCsrfTokenCall().execute()
},
).get()
if (response.body()?.query()?.csrfToken().isNullOrEmpty()) {
if (response
.body()
?.query()
?.csrfToken()
.isNullOrEmpty()
) {
continue
}
@ -52,9 +59,8 @@ class CsrfTokenClient(
}
break
} catch (e: LoginFailedException) {
throw InvalidLoginTokenException(ANONYMOUS_TOKEN_MESSAGE)
}
catch (t: Throwable) {
throw InvalidLoginTokenException(ANONYMOUS_TOKEN_MESSAGE)
} catch (t: Throwable) {
Timber.w(t)
}
}
@ -66,45 +72,67 @@ class CsrfTokenClient(
}
@VisibleForTesting
fun request(service: CsrfTokenInterface, cb: Callback): Call<MwQueryResponse?> =
requestToken(service, object : Callback {
override fun success(token: String?) {
if (sessionManager.isUserLoggedIn && token == ANON_TOKEN) {
retryWithLogin(cb) {
InvalidLoginTokenException(ANONYMOUS_TOKEN_MESSAGE)
fun request(
service: CsrfTokenInterface,
cb: Callback,
): Call<MwQueryResponse?> =
requestToken(
service,
object : Callback {
override fun success(token: String?) {
if (sessionManager.isUserLoggedIn && token == ANON_TOKEN) {
retryWithLogin(cb) {
InvalidLoginTokenException(ANONYMOUS_TOKEN_MESSAGE)
}
} else {
cb.success(token)
}
} else {
cb.success(token)
}
}
override fun failure(caught: Throwable?) = retryWithLogin(cb) { caught }
override fun failure(caught: Throwable?) = retryWithLogin(cb) { caught }
override fun twoFactorPrompt() = cb.twoFactorPrompt()
})
override fun twoFactorPrompt() = cb.twoFactorPrompt()
override fun emailAuthPrompt() = cb.emailAuthPrompt()
},
)
@VisibleForTesting
fun requestToken(service: CsrfTokenInterface, cb: Callback): Call<MwQueryResponse?> {
fun requestToken(
service: CsrfTokenInterface,
cb: Callback,
): Call<MwQueryResponse?> {
val call = service.getCsrfTokenCall()
call.enqueue(object : retrofit2.Callback<MwQueryResponse?> {
override fun onResponse(call: Call<MwQueryResponse?>, response: Response<MwQueryResponse?>) {
if (call.isCanceled) {
return
call.enqueue(
object : retrofit2.Callback<MwQueryResponse?> {
override fun onResponse(
call: Call<MwQueryResponse?>,
response: Response<MwQueryResponse?>,
) {
if (call.isCanceled) {
return
}
cb.success(response.body()!!.query()!!.csrfToken())
}
cb.success(response.body()!!.query()!!.csrfToken())
}
override fun onFailure(call: Call<MwQueryResponse?>, t: Throwable) {
if (call.isCanceled) {
return
override fun onFailure(
call: Call<MwQueryResponse?>,
t: Throwable,
) {
if (call.isCanceled) {
return
}
cb.failure(t)
}
cb.failure(t)
}
})
},
)
return call
}
private fun retryWithLogin(callback: Callback, caught: () -> Throwable?) {
private fun retryWithLogin(
callback: Callback,
caught: () -> Throwable?,
) {
val userName = sessionManager.userName
val password = sessionManager.password
if (retries < MAX_RETRIES && !userName.isNullOrEmpty() && !password.isNullOrEmpty()) {
@ -124,26 +152,38 @@ class CsrfTokenClient(
username: String,
password: String,
callback: Callback,
retryCallback: () -> Unit
) = loginClient.request(username, password, object : LoginCallback {
override fun success(loginResult: LoginResult) {
if (loginResult.pass) {
sessionManager.updateAccount(loginResult)
retryCallback()
} else {
callback.failure(LoginFailedException(loginResult.message))
retryCallback: () -> Unit,
) = loginClient.request(
username,
password,
object : LoginCallback {
override fun success(loginResult: LoginResult) {
if (loginResult.pass) {
sessionManager.updateAccount(loginResult)
retryCallback()
} else {
callback.failure(LoginFailedException(loginResult.message))
}
}
}
override fun twoFactorPrompt(caught: Throwable, token: String?) =
callback.twoFactorPrompt()
override fun twoFactorPrompt(
loginResult: LoginResult,
caught: Throwable,
token: String?,
) = callback.twoFactorPrompt()
// Should not happen here, but call the callback just in case.
override fun passwordResetPrompt(token: String?) =
callback.failure(LoginFailedException("Logged in with temporary password."))
override fun emailAuthPrompt(
loginResult: LoginResult,
caught: Throwable,
token: String?,
) = callback.emailAuthPrompt()
override fun error(caught: Throwable) = callback.failure(caught)
})
// Should not happen here, but call the callback just in case.
override fun passwordResetPrompt(token: String?) = callback.failure(LoginFailedException("Logged in with temporary password."))
override fun error(caught: Throwable) = callback.failure(caught)
},
)
private fun cancel() {
loginClient.cancel()
@ -155,8 +195,12 @@ class CsrfTokenClient(
interface Callback {
fun success(token: String?)
fun failure(caught: Throwable?)
fun twoFactorPrompt()
fun emailAuthPrompt()
}
companion object {
@ -167,5 +211,7 @@ class CsrfTokenClient(
const val ANONYMOUS_TOKEN_MESSAGE = "App believes we're logged in, but got anonymous token."
}
}
class InvalidLoginTokenException(message: String) : Exception(message)
class InvalidLoginTokenException(
message: String,
) : Exception(message)

View file

@ -3,6 +3,10 @@ package fr.free.nrw.commons.auth.csrf
import fr.free.nrw.commons.wikidata.cookies.CommonsCookieStorage
import javax.inject.Inject
class LogoutClient @Inject constructor(private val store: CommonsCookieStorage) {
fun logout() = store.clear()
}
class LogoutClient
@Inject
constructor(
private val store: CommonsCookieStorage,
) {
fun logout() = store.clear()
}

View file

@ -2,7 +2,20 @@ package fr.free.nrw.commons.auth.login
interface LoginCallback {
fun success(loginResult: LoginResult)
fun twoFactorPrompt(caught: Throwable, token: String?)
fun twoFactorPrompt(
loginResult: LoginResult,
caught: Throwable,
token: String?,
)
fun emailAuthPrompt(
loginResult: LoginResult,
caught: Throwable,
token: String?,
)
fun passwordResetPrompt(token: String?)
fun error(caught: Throwable)
}

View file

@ -1,12 +1,13 @@
package fr.free.nrw.commons.auth.login
import android.text.TextUtils
import fr.free.nrw.commons.auth.login.LoginResult.EmailAuthResult
import fr.free.nrw.commons.auth.login.LoginResult.OAuthResult
import fr.free.nrw.commons.auth.login.LoginResult.ResetPasswordResult
import fr.free.nrw.commons.wikidata.WikidataConstants.WIKIPEDIA_URL
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
@ -16,7 +17,9 @@ import java.io.IOException
/**
* Responsible for making login related requests to the server.
*/
class LoginClient(private val loginInterface: LoginInterface) {
class LoginClient(
private val loginInterface: LoginInterface,
) {
private var tokenCall: Call<MwQueryResponse?>? = null
private var loginCall: Call<LoginResponse?>? = null
@ -30,131 +33,206 @@ class LoginClient(private val loginInterface: LoginInterface) {
private fun getLoginToken() = loginInterface.getLoginToken()
fun request(userName: String, password: String, cb: LoginCallback) {
fun request(
userName: String,
password: String,
cb: LoginCallback,
) {
cancel()
tokenCall = getLoginToken()
tokenCall!!.enqueue(object : Callback<MwQueryResponse?> {
override fun onResponse(call: Call<MwQueryResponse?>, response: Response<MwQueryResponse?>) {
login(
userName, password, null, null, response.body()!!.query()!!.loginToken(),
userLanguage, cb
)
}
override fun onFailure(call: Call<MwQueryResponse?>, caught: Throwable) {
if (call.isCanceled) {
return
tokenCall!!.enqueue(
object : Callback<MwQueryResponse?> {
override fun onResponse(
call: Call<MwQueryResponse?>,
response: Response<MwQueryResponse?>,
) {
login(
userName,
password,
null,
null,
null,
response.body()!!.query()!!.loginToken(),
userLanguage,
cb,
)
}
cb.error(caught)
}
})
override fun onFailure(
call: Call<MwQueryResponse?>,
caught: Throwable,
) {
if (call.isCanceled) {
return
}
cb.error(caught)
}
},
)
}
fun login(
userName: String, password: String, retypedPassword: String?, twoFactorCode: String?,
loginToken: String?, userLanguage: String, cb: LoginCallback
userName: String,
password: String,
retypedPassword: String?,
twoFactorCode: String?,
emailAuthCode: String?,
loginToken: String?,
userLanguage: String,
cb: LoginCallback,
) {
this.userLanguage = userLanguage
loginCall = if (twoFactorCode.isNullOrEmpty() && retypedPassword.isNullOrEmpty()) {
loginInterface.postLogIn(userName, password, loginToken, userLanguage, WIKIPEDIA_URL)
} else {
loginInterface.postLogIn(
userName, password, retypedPassword, twoFactorCode, loginToken, userLanguage, true
)
}
loginCall =
if (twoFactorCode.isNullOrEmpty() && emailAuthCode.isNullOrEmpty() && retypedPassword.isNullOrEmpty()) {
loginInterface.postLogIn(userName, password, loginToken, userLanguage, WIKIPEDIA_URL)
} else {
loginInterface.postLogIn(
userName,
password,
retypedPassword,
twoFactorCode,
emailAuthCode,
loginToken,
userLanguage,
true,
)
}
loginCall!!.enqueue(object : Callback<LoginResponse?> {
override fun onResponse(
call: Call<LoginResponse?>,
response: Response<LoginResponse?>
) {
val loginResult = response.body()?.toLoginResult(password)
if (loginResult != null) {
if (loginResult.pass && !loginResult.userName.isNullOrEmpty()) {
// The server could do some transformations on user names, e.g. on some
// wikis is uppercases the first letter.
getExtendedInfo(loginResult.userName, loginResult, cb)
} else if ("UI" == loginResult.status) {
when (loginResult) {
is OAuthResult -> cb.twoFactorPrompt(
LoginFailedException(loginResult.message),
loginToken
)
loginCall!!.enqueue(
object : Callback<LoginResponse?> {
override fun onResponse(
call: Call<LoginResponse?>,
response: Response<LoginResponse?>,
) {
val loginResult = response.body()?.toLoginResult(password)
if (loginResult != null) {
if (loginResult.pass && !loginResult.userName.isNullOrEmpty()) {
// The server could do some transformations on user names, e.g. on some
// wikis is uppercases the first letter.
getExtendedInfo(loginResult.userName, loginResult, cb)
} else if ("UI" == loginResult.status) {
when (loginResult) {
is OAuthResult ->
cb.twoFactorPrompt(
loginResult,
LoginFailedException(loginResult.message),
loginToken,
)
is ResetPasswordResult -> cb.passwordResetPrompt(loginToken)
is EmailAuthResult ->
cb.emailAuthPrompt(
loginResult,
LoginFailedException(loginResult.message),
loginToken
)
is LoginResult.Result -> cb.error(
LoginFailedException(loginResult.message)
)
is ResetPasswordResult -> cb.passwordResetPrompt(loginToken)
is LoginResult.Result ->
cb.error(
LoginFailedException(loginResult.message),
)
}
} else {
cb.error(LoginFailedException(loginResult.message))
}
} else {
cb.error(LoginFailedException(loginResult.message))
cb.error(IOException("Login failed. Unexpected response."))
}
} else {
cb.error(IOException("Login failed. Unexpected response."))
}
}
override fun onFailure(call: Call<LoginResponse?>, t: Throwable) {
if (call.isCanceled) {
return
override fun onFailure(
call: Call<LoginResponse?>,
t: Throwable,
) {
if (call.isCanceled) {
return
}
cb.error(t)
}
cb.error(t)
}
})
},
)
}
fun doLogin(
username: String,
password: String,
lastLoginResult: LoginResult?,
twoFactorCode: String,
userLanguage: String,
loginCallback: LoginCallback
loginCallback: LoginCallback,
) {
getLoginToken().enqueue(object :Callback<MwQueryResponse?>{
override fun onResponse(
call: Call<MwQueryResponse?>,
response: Response<MwQueryResponse?>
) = if (response.isSuccessful){
val loginToken = response.body()?.query()?.loginToken()
loginToken?.let {
login(username, password, null, twoFactorCode, it, userLanguage, loginCallback)
} ?: run {
getLoginToken().enqueue(
object : Callback<MwQueryResponse?> {
override fun onResponse(
call: Call<MwQueryResponse?>,
response: Response<MwQueryResponse?>,
) = if (response.isSuccessful) {
val loginToken = response.body()?.query()?.loginToken()
loginToken?.let {
login(username, password, null,
if (lastLoginResult is OAuthResult) twoFactorCode else null,
if (lastLoginResult is EmailAuthResult) twoFactorCode else null,
it, userLanguage, loginCallback)
} ?: run {
loginCallback.error(IOException("Failed to retrieve login token"))
}
} else {
loginCallback.error(IOException("Failed to retrieve login token"))
}
} else {
loginCallback.error(IOException("Failed to retrieve login token"))
}
override fun onFailure(call: Call<MwQueryResponse?>, t: Throwable) {
loginCallback.error(t)
}
})
override fun onFailure(
call: Call<MwQueryResponse?>,
t: Throwable,
) {
loginCallback.error(t)
}
},
)
}
@Throws(Throwable::class)
fun loginBlocking(userName: String, password: String, twoFactorCode: String?) {
fun loginBlocking(
userName: String,
password: String,
twoFactorCode: String? = null,
emailAuthCode: String? = null
) {
val tokenResponse = getLoginToken().execute()
if (tokenResponse.body()?.query()?.loginToken().isNullOrEmpty()) {
if (tokenResponse
.body()
?.query()
?.loginToken()
.isNullOrEmpty()
) {
throw IOException("Unexpected response when getting login token.")
}
val loginToken = tokenResponse.body()?.query()?.loginToken()
val tempLoginCall = if (twoFactorCode.isNullOrEmpty()) {
loginInterface.postLogIn(userName, password, loginToken, userLanguage, WIKIPEDIA_URL)
} else {
loginInterface.postLogIn(
userName, password, null, twoFactorCode, loginToken, userLanguage, true
)
}
val tempLoginCall =
if (twoFactorCode.isNullOrEmpty() && emailAuthCode.isNullOrEmpty()) {
loginInterface.postLogIn(userName, password, loginToken, userLanguage, WIKIPEDIA_URL)
} else {
loginInterface.postLogIn(
userName,
password,
null,
twoFactorCode,
emailAuthCode,
loginToken,
userLanguage,
true,
)
}
val response = tempLoginCall.execute()
val loginResponse = response.body() ?: throw IOException("Unexpected response when logging in.")
val loginResult = loginResponse.toLoginResult(password) ?: throw IOException("Unexpected response when logging in.")
if ("UI" == loginResult.status) {
if (loginResult is OAuthResult) {
if (loginResult is OAuthResult || loginResult is EmailAuthResult) {
// TODO: Find a better way to boil up the warning about 2FA
throw LoginFailedException(loginResult.message)
}
@ -166,18 +244,23 @@ class LoginClient(private val loginInterface: LoginInterface) {
}
}
private fun getExtendedInfo(userName: String, loginResult: LoginResult, cb: LoginCallback) =
loginInterface.getUserInfo(userName)
.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
.subscribe({ response: MwQueryResponse? ->
loginResult.userId = response?.query()?.userInfo()?.id() ?: 0
loginResult.groups =
response?.query()?.getUserResponse(userName)?.groups ?: emptySet()
cb.success(loginResult)
}, { caught: Throwable ->
Timber.e(caught, "Login succeeded but getting group information failed. ")
cb.error(caught)
})
private fun getExtendedInfo(
userName: String,
loginResult: LoginResult,
cb: LoginCallback,
) = loginInterface
.getUserInfo(userName)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ response: MwQueryResponse? ->
loginResult.userId = response?.query()?.userInfo()?.id() ?: 0
loginResult.groups =
response?.query()?.getUserResponse(userName)?.getGroups() ?: emptySet()
cb.success(loginResult)
}, { caught: Throwable ->
Timber.e(caught, "Login succeeded but getting group information failed. ")
cb.error(caught)
})
fun cancel() {
tokenCall?.let {

View file

@ -1,3 +1,5 @@
package fr.free.nrw.commons.auth.login
class LoginFailedException(message: String?) : Throwable(message)
class LoginFailedException(
message: String?,
) : Throwable(message)

View file

@ -1,8 +1,8 @@
package fr.free.nrw.commons.auth.login
import fr.free.nrw.commons.wikidata.WikidataConstants.MW_API_PREFIX
import io.reactivex.Observable
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
import io.reactivex.Observable
import retrofit2.Call
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
@ -24,7 +24,7 @@ interface LoginInterface {
@Field("password") pass: String?,
@Field("logintoken") token: String?,
@Field("uselang") userLanguage: String?,
@Field("loginreturnurl") url: String?
@Field("loginreturnurl") url: String?,
): Call<LoginResponse?>
@Headers("Cache-Control: no-cache")
@ -35,11 +35,14 @@ interface LoginInterface {
@Field("password") pass: String?,
@Field("retype") retypedPass: String?,
@Field("OATHToken") twoFactorCode: String?,
@Field("logintoken") token: String?,
@Field("token") emailAuthToken: String?,
@Field("logintoken") loginToken: String?,
@Field("uselang") userLanguage: String?,
@Field("logincontinue") loginContinue: Boolean
@Field("logincontinue") loginContinue: Boolean,
): Call<LoginResponse?>
@GET(MW_API_PREFIX + "action=query&meta=userinfo&list=users&usprop=groups|cancreate")
fun getUserInfo(@Query("ususers") userName: String): Observable<MwQueryResponse?>
fun getUserInfo(
@Query("ususers") userName: String,
): Observable<MwQueryResponse?>
}

View file

@ -2,6 +2,7 @@ package fr.free.nrw.commons.auth.login
import com.google.gson.annotations.SerializedName
import fr.free.nrw.commons.auth.login.LoginResult.OAuthResult
import fr.free.nrw.commons.auth.login.LoginResult.EmailAuthResult
import fr.free.nrw.commons.auth.login.LoginResult.ResetPasswordResult
import fr.free.nrw.commons.auth.login.LoginResult.Result
import fr.free.nrw.commons.wikidata.mwapi.MwServiceError
@ -13,9 +14,7 @@ class LoginResponse {
@SerializedName("clientlogin")
private val clientLogin: ClientLogin? = null
fun toLoginResult(password: String): LoginResult? {
return clientLogin?.toLoginResult(password)
}
fun toLoginResult(password: String): LoginResult? = clientLogin?.toLoginResult(password)
}
internal class ClientLogin {
@ -29,17 +28,19 @@ internal class ClientLogin {
fun toLoginResult(password: String): LoginResult {
var userMessage = message
if ("UI" == status) {
if (requests != null) {
for (req in requests) {
if ("MediaWiki\\Extension\\OATHAuth\\Auth\\TOTPAuthenticationRequest" == req.id()) {
requests?.forEach { request ->
request.id()?.let {
if (it.endsWith("TOTPAuthenticationRequest")) {
return OAuthResult(status, userName, password, message)
} else if ("MediaWiki\\Auth\\PasswordAuthenticationRequest" == req.id()) {
} else if (it.endsWith("EmailAuthAuthenticationRequest")) {
return EmailAuthResult(status, userName, password, message)
} else if (it.endsWith("PasswordAuthenticationRequest")) {
return ResetPasswordResult(status, userName, password, message)
}
}
}
} else if ("PASS" != status && "FAIL" != status) {
//TODO: String resource -- Looks like needed for others in this class too
// TODO: String resource -- Looks like needed for others in this class too
userMessage = "An unknown error occurred."
}
return Result(status ?: "", userName, password, userMessage)
@ -51,7 +52,7 @@ internal class Request {
private val required: String? = null
private val provider: String? = null
private val account: String? = null
private val fields: Map<String, RequestField>? = null
internal val fields: Map<String, RequestField>? = null
fun id(): String? = id
}
@ -59,5 +60,5 @@ internal class Request {
internal class RequestField {
private val type: String? = null
private val label: String? = null
private val help: String? = null
internal val help: String? = null
}

View file

@ -4,7 +4,7 @@ sealed class LoginResult(
val status: String,
val userName: String?,
val password: String?,
val message: String?
val message: String?,
) {
var userId = 0
var groups = emptySet<String>()
@ -14,20 +14,27 @@ sealed class LoginResult(
status: String,
userName: String?,
password: String?,
message: String?
): LoginResult(status, userName, password, message)
message: String?,
) : LoginResult(status, userName, password, message)
class OAuthResult(
status: String,
userName: String?,
password: String?,
message: String?
message: String?,
) : LoginResult(status, userName, password, message)
class EmailAuthResult(
status: String,
userName: String?,
password: String?,
message: String?,
) : LoginResult(status, userName, password, message)
class ResetPasswordResult(
status: String,
userName: String?,
password: String?,
message: String?
message: String?,
) : LoginResult(status, userName, password, message)
}

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,259 +0,0 @@
package fr.free.nrw.commons.bookmarks;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsFragment;
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsFragment;
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment;
import fr.free.nrw.commons.category.CategoryImagesCallback;
import fr.free.nrw.commons.category.GridViewAdapter;
import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.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;
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");
if (order == 0) {
listFragment = new BookmarkPicturesFragment();
} else {
listFragment = new BookmarkLocationsFragment();
if(orderItem == 2) {
listFragment = new BookmarkItemsFragment();
}
}
Bundle featuredArguments = new Bundle();
featuredArguments.putString("categoryName", title);
listFragment.setArguments(featuredArguments);
this.bookmarksPagerAdapter = bookmarksPagerAdapter;
}
@Nullable
@Override
public View onCreateView(@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
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) {
Log.d("deneme8", "on media clicked");
/*container.setVisibility(View.VISIBLE);
((BookmarkFragment)getParentFragment()).tabLayout.setVisibility(View.GONE);
mediaDetails = new MediaDetailPagerFragment(false, true, position);
setFragment(mediaDetails, bookmarkPicturesFragment);*/
}
/**
* This method is called mediaDetailPagerFragment. It returns the Media Object at that Index
*
* @param i It is the index of which media object is to be returned which is same as current
* index of viewPager.
* @return Media Object
*/
@Override
public Media getMediaAtPosition(int i) {
if (bookmarksPagerAdapter.getMediaAdapter() == null) {
// not yet ready to return data
return null;
} else {
return (Media) bookmarksPagerAdapter.getMediaAdapter().getItem(i);
}
}
/**
* This method is called on from getCount of MediaDetailPagerFragment The viewpager will contain
* same number of media items as that of media elements in adapter.
*
* @return Total Media count in the adapter
*/
@Override
public int getTotalMediaCount() {
if (bookmarksPagerAdapter.getMediaAdapter() == null) {
return 0;
}
return bookmarksPagerAdapter.getMediaAdapter().getCount();
}
@Override
public Integer getContributionStateAt(int position) {
return null;
}
/**
* Reload media detail fragment once media is nominated
*
* @param index item position that has been nominated
*/
@Override
public void refreshNominatedMedia(int index) {
if (mediaDetails != null && !listFragment.isVisible()) {
removeFragment(mediaDetails);
mediaDetails = 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) {
Log.d("deneme8", "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,32 +0,0 @@
package fr.free.nrw.commons.bookmarks;
import androidx.fragment.app.Fragment;
/**
* Data class for handling a bookmark fragment and it title
*/
public class BookmarkPages {
private Fragment page;
private String title;
BookmarkPages(Fragment fragment, String title) {
this.title = title;
this.page = fragment;
}
/**
* Return the fragment
* @return fragment object
*/
public Fragment getPage() {
return page;
}
/**
* Return the fragment title
* @return title
*/
public String getTitle() {
return title;
}
}

View file

@ -0,0 +1,8 @@
package fr.free.nrw.commons.bookmarks
import androidx.fragment.app.Fragment
data class BookmarkPages (
val page: Fragment? = null,
val title: String? = null
)

View file

@ -1,87 +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)));
}
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

@ -0,0 +1,52 @@
package fr.free.nrw.commons.bookmarks.category
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import kotlinx.coroutines.flow.Flow
/**
* Bookmark categories dao
*
* @constructor Create empty Bookmark categories dao
*/
@Dao
interface BookmarkCategoriesDao {
/**
* Insert or Delete category bookmark into DB
*
* @param bookmarksCategoryModal
*/
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(bookmarksCategoryModal: BookmarksCategoryModal)
/**
* Delete category bookmark from DB
*
* @param bookmarksCategoryModal
*/
@Delete
suspend fun delete(bookmarksCategoryModal: BookmarksCategoryModal)
/**
* Checks if given category exist in DB
*
* @param categoryName
* @return
*/
@Query("SELECT EXISTS (SELECT 1 FROM bookmarks_categories WHERE categoryName = :categoryName)")
suspend fun doesExist(categoryName: String): Boolean
/**
* Get all categories
*
* @return
*/
@Query("SELECT * FROM bookmarks_categories")
fun getAllCategories(): Flow<List<BookmarksCategoryModal>>
}

View file

@ -0,0 +1,143 @@
package fr.free.nrw.commons.bookmarks.category
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import dagger.android.support.DaggerFragment
import fr.free.nrw.commons.R
import fr.free.nrw.commons.category.CategoryDetailsActivity
import javax.inject.Inject
/**
* Tab fragment to show list of bookmarked Categories
*/
class BookmarkCategoriesFragment : DaggerFragment() {
@Inject
lateinit var bookmarkCategoriesDao: BookmarkCategoriesDao
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
MaterialTheme(
colorScheme = if (isSystemInDarkTheme()) darkColorScheme(
primary = colorResource(R.color.primaryDarkColor),
surface = colorResource(R.color.main_background_dark),
background = colorResource(R.color.main_background_dark)
) else lightColorScheme(
primary = colorResource(R.color.primaryColor),
surface = colorResource(R.color.main_background_light),
background = colorResource(R.color.main_background_light)
)
) {
val listOfBookmarks by bookmarkCategoriesDao.getAllCategories()
.collectAsStateWithLifecycle(initialValue = emptyList())
Surface(modifier = Modifier.fillMaxSize()) {
Box(contentAlignment = Alignment.Center) {
if (listOfBookmarks.isEmpty()) {
Text(
text = stringResource(R.string.bookmark_empty),
style = MaterialTheme.typography.bodyMedium,
color = if (isSystemInDarkTheme()) Color(0xB3FFFFFF)
else Color(
0x8A000000
)
)
} else {
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(items = listOfBookmarks) { bookmarkItem ->
CategoryItem(
categoryName = bookmarkItem.categoryName,
onClick = {
val categoryDetailsIntent = Intent(
requireContext(),
CategoryDetailsActivity::class.java
).putExtra("categoryName", it)
startActivity(categoryDetailsIntent)
}
)
}
}
}
}
}
}
}
}
}
@Composable
fun CategoryItem(
modifier: Modifier = Modifier,
onClick: (String) -> Unit,
categoryName: String
) {
Row(modifier = modifier.clickable {
onClick(categoryName)
}) {
ListItem(
leadingContent = {
Image(
modifier = Modifier.size(48.dp),
painter = painterResource(R.drawable.commons),
contentDescription = null
)
},
headlineContent = {
Text(
text = categoryName,
maxLines = 2,
color = if (isSystemInDarkTheme()) Color.White else Color.Black,
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.SemiBold
)
}
)
}
}
@Preview
@Composable
private fun CategoryItemPreview() {
CategoryItem(
onClick = {},
categoryName = "Test Category"
)
}
}

View file

@ -0,0 +1,15 @@
package fr.free.nrw.commons.bookmarks.category
import androidx.room.Entity
import androidx.room.PrimaryKey
/**
* Data class representing bookmarked category in DB
*
* @property categoryName
* @constructor Create empty Bookmarks category modal
*/
@Entity(tableName = "bookmarks_categories")
data class BookmarksCategoryModal(
@PrimaryKey val categoryName: String
)

View file

@ -15,25 +15,34 @@ import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
/**
* Helps to inflate Wikidata Items into Items tab
*/
class BookmarkItemsAdapter (val list: List<DepictedItem>, val context: Context) :
RecyclerView.Adapter<BookmarkItemsAdapter.BookmarkItemViewHolder>() {
class BookmarkItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
class BookmarkItemsAdapter(
val list: List<DepictedItem>,
val context: Context,
) : RecyclerView.Adapter<BookmarkItemsAdapter.BookmarkItemViewHolder>() {
class BookmarkItemViewHolder(
itemView: View,
) : RecyclerView.ViewHolder(itemView) {
var depictsLabel: TextView = itemView.findViewById(R.id.depicts_label)
var description: TextView = itemView.findViewById(R.id.description)
var depictsImage: SimpleDraweeView = itemView.findViewById(R.id.depicts_image)
var layout : ConstraintLayout = itemView.findViewById(R.id.layout_item)
var layout: ConstraintLayout = itemView.findViewById(R.id.layout_item)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BookmarkItemViewHolder {
val v: View = LayoutInflater.from(context)
.inflate(R.layout.item_depictions, parent, false)
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int,
): BookmarkItemViewHolder {
val v: View =
LayoutInflater
.from(context)
.inflate(R.layout.item_depictions, parent, false)
return BookmarkItemViewHolder(v)
}
override fun onBindViewHolder(holder: BookmarkItemViewHolder, position: Int) {
override fun onBindViewHolder(
holder: BookmarkItemViewHolder,
position: Int,
) {
val depictedItem = list[position]
holder.depictsLabel.text = depictedItem.name
holder.description.text = depictedItem.description
@ -48,7 +57,5 @@ class BookmarkItemsAdapter (val list: List<DepictedItem>, val context: Context)
}
}
override fun getItemCount(): Int {
return list.size
}
override fun getItemCount(): Int = list.size
}

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

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