diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml
index 7a1e7c030..958c13fda 100644
--- a/.github/workflows/android.yml
+++ b/.github/workflows/android.yml
@@ -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,7 @@ 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
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
index a5d456928..f39734eb4 100644
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -1,16 +1,12 @@
-
-
-
-
@@ -25,13 +21,11 @@
-
-
@@ -47,6 +41,5 @@
-
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f4250ebd9..e7accf82b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,116 @@
# Wikimedia Commons for Android
+## v5.1.2
+
+### What's changed
+
+* Fix the broken category search in the explore screen
+
+## v5.1.1
+
+### What's changed
+
+* Use Android's new EXIF interface to mitigate security issues in old
+ EXIF interface.
+* Make the icon that helps view the upload queue always visible as it ensures
+ that the queue accessible at all times.
+
+## v5.1.0
+
+### What's Changed
+
+* Enhanced **upload queue management** in the Commons app for smoother, sequential
+ processing, clearer progress tracking, prevention of stuck or duplicate
+ uploads. As part of this improvement, the "Limited Connection mode" has been
+ removed.
+* Added an option in "Nearby" feature enabling users to **provide feedback on
+ Wikidata items**. Users can report if an item doesn’t exist, is at a different
+ location, or has other issues, with submissions tagged for easy tracking and
+ updates.
+* Improved the "Nearby" feature by splitting the query into two parts for faster
+ loading and **better performance, especially in areas with dense amount of
+ places**. This update also resolves issues with pins overlapping place names.
+* Upgraded AGP and **target/compile SDK to 34** and make necessary adjustments to
+ the app such as adding **"Partial Access" support**. Also includes some minor
+ refactoring, and replacement of deprecated circular progress bars.
+* Fixed an **UI issue where the 'Subcategories' and 'Parent Categories' tabs
+ appeared blank** in the Category Details screen. Resolved by optimizing view
+ binding handling in the parent fragments.
+* Fixed an issue where editing depictions removed all other structured data from
+ images. Now, **only depictions are updated, preserving other associated data**.
+* Fixed **map centering** in the image upload flow to **use GPS EXIF tag location**
+ from pictures and ensured "Show in map app" accurately reflects this location.
+* Fixed navigation **after uploading via Nearby by directing users to the Uploads
+ activity** instead of returning to Nearby, preventing confusion about needing to
+ upload again.
+
+### Bug fixes and various changes
+
+* Improved the "Nearby" feature to fetch labels based on the user's preferred
+ language instead of defaulting to English.
+* Added a legend to the "Nearby" feature indicating pin statuses: red for items
+ without pictures, green for those with pictures, and grey for items being
+ checked. A floating action button now allows users to toggle the legend's
+ visibility.
+* Fixed an issue where the "Nominate for deletion" option is shown to logged out
+ users, preventing app errors and crashes.
+* Updated the regex pattern that filters categories with an year in it to also
+ filter the 2020s.
+* Fix an issue where past depictions were not shown as suggestions, despite
+ being saved correctly.
+* Fixed an issue in custom image picker where exiting the media preview showed
+ only the first image and cleared selections. Now, previously selected images
+ are restored correctly after exiting the preview. This was contributed.
+* Fixed an issue in custom image picker where scrolling behavior did not
+ maintain position after exiting fullscreen preview, ensuring users remain at
+ the same point in their image roll unless actioned images are filtered. This
+ was contributed.
+* Fixed Nearby map not showing new pins on map move by removing the 2000m scroll
+ threshold and adding an 800ms debounce for smoother pin updates when the map
+ is moved. Queued searches are now canceled on fragment destruction.
+* Revised author information retrieval to emphasize the custom author name from
+ the metadata instead of the default registered username.
+* Enhanced notification classification to properly identify "email" type
+ notifications and prompting users to check their e-mail inbox when such
+ notifications are clicked.
+* Resolved a bug in the language chooser that incorrectly greyed-out previously
+ selected languages, ensuring only the current language is non-selectable during
+ image upload.
+* Resolved pin color update issue in "Nearby" feature where the pin colour
+ failed to be updated after a successful image upload.
+
+What's listed here is only a subset of all the changes. Check the full-list of
+the changes in [this link](https://github.com/commons-app/apps-android-commons/compare/v5.0.2...v5.1.0).
+Alternatively, checkout [this release on GitHub releases page](https://github.com/commons-app/apps-android-commons/releases/tag/v5.1.0)
+for an exhaustive list of changes and the various contributors who contributed the same.
+
+## v5.0.2
+
+- Enhanced multi-upload functionality with user prompts to clarify that all images would share the
+ same category and depictions.
+- Show Wikidata description on currently active Nearby pin to provide more useful information.
+- Improve the visibility of map markers by dynamically adjusting their colors based on the app's
+ theme. The map markers will now appear lighter when the app is in dark mode and darker when the
+ app is in light mode. This change aims to enhance marker visibility and improve the overall user
+ experience.
+- Added information on where user feedback is posted, helping users track existing feedback and
+ monitor their own submissions.
+- Enhanced the edit location screen of the upload screen by centering the map on the picture's
+ location from metadata when editing, or on the device's GPS location if metadata is unavailable,
+ improving accuracy and user experience.
+- Ensured the 'Add Location' button is renamed to 'Edit Location' when copying the location of a
+ recently uploaded image, enhancing clarity and user experience.
+- Added a ProgressBar to the media detail screen to indicate image loading status, enhancing user
+ experience by showing loading progress until the image is fully loaded.
+- Fixed an issue where caption and description fields would intermittently disappear when using
+ voice input, ensuring text remains visible and stable across all entries.
+- Fixed a crash that occurred when attempting to remove multiple instances of caption/description
+ fields after initially adding them.
+- Improve the text in the prompt shown when skipping login to sound more natural.
+- Modified feedback addition logic to append new sections at the bottom of the page, ensuring
+ auto-archiving of sections functions correctly on the feedback page.
+- Resolved issue where the app failed to clear cookies upon logout.
+
## v5.0.1
Same as v5.0.0 except this fixes some R8 rules to ensure that the release
diff --git a/app/build.gradle b/app/build.gradle
index 19c570a6b..2bde0d4f1 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -47,9 +47,27 @@ dependencies {
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.google.android.material:material:1.12.0"
implementation 'com.karumi:dexter:5.0.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
+ implementation 'androidx.compose.ui:ui-tooling-preview'
+ androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
+
+ // Jetpack Compose
+ def composeBom = platform('androidx.compose:compose-bom:2024.11.00')
+
+ implementation "androidx.activity:activity-compose:1.9.3"
+ implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.8.4"
+ implementation (composeBom)
+ implementation "androidx.compose.runtime:runtime"
+ implementation "androidx.compose.ui:ui"
+ implementation "androidx.compose.ui:ui-viewbinding"
+ implementation "androidx.compose.ui:ui-graphics"
+ implementation "androidx.compose.ui:ui-tooling"
+ implementation "androidx.compose.foundation:foundation"
+ implementation "androidx.compose.foundation:foundation-layout"
+ implementation "androidx.compose.material3:material3"
+ androidTestImplementation(composeBom)
implementation "com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:$ADAPTER_DELEGATES_VERSION"
implementation "com.hannesdorfmann:adapterdelegates4-pagination:$ADAPTER_DELEGATES_VERSION"
@@ -71,6 +89,8 @@ dependencies {
// Dependency injector
implementation "com.google.dagger:dagger-android:$DAGGER_VERSION"
implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION"
+ debugImplementation 'androidx.compose.ui:ui-tooling'
+ debugImplementation 'androidx.compose.ui:ui-test-manifest'
kapt "com.google.dagger:dagger-android-processor:$DAGGER_VERSION"
kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
annotationProcessor "com.google.dagger:dagger-android-processor:$DAGGER_VERSION"
@@ -83,6 +103,7 @@ dependencies {
testImplementation 'org.mockito:mockito-core:5.6.0'
testImplementation "org.powermock:powermock-module-junit4:2.0.9"
testImplementation "org.powermock:powermock-api-mockito2:2.0.9"
+ testImplementation("io.mockk:mockk:1.13.5")
// Unit testing
testImplementation 'junit:junit:4.13.2'
@@ -92,7 +113,7 @@ dependencies {
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 "com.jraska.livedata:testing-ktx:1.2.0"
testImplementation "androidx.arch.core:core-testing:2.2.0"
testImplementation "org.junit.jupiter:junit-jupiter-api:5.10.0"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.10.0"
@@ -122,7 +143,7 @@ dependencies {
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.exifinterface:exifinterface:1.3.7'
implementation "androidx.core:core-ktx:$CORE_KTX_VERSION"
implementation 'com.simplecityapps:recyclerview-fastscroll:2.0.1'
@@ -159,7 +180,7 @@ dependencies {
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' }
+ implementation("io.github.coordinates2country:coordinates2country-android:1.8") { exclude group: 'com.google.android', module: 'android' }
//OSMDroid
implementation ("org.osmdroid:osmdroid-android:$OSMDROID_VERSION")
@@ -186,17 +207,17 @@ project.gradle.taskGraph.whenReady {
}
android {
- compileSdkVersion 33
+ compileSdkVersion 34
defaultConfig {
//applicationId 'fr.free.nrw.commons'
- versionCode 1039
- versionName '5.0.1'
+ versionCode 1043
+ versionName '5.1.2'
setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName())
minSdkVersion 21
- targetSdkVersion 33
+ targetSdkVersion 34
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments clearPackageData: 'true'
@@ -211,7 +232,7 @@ android {
excludes += ['META-INF/androidx.*']
}
resources {
- excludes += ['META-INF/androidx.*', 'META-INF/proguard/androidx-annotations.pro']
+ excludes += ['META-INF/androidx.*', 'META-INF/proguard/androidx-annotations.pro', '/META-INF/LICENSE.md', '/META-INF/LICENSE-notice.md']
}
}
@@ -253,11 +274,12 @@ android {
}
}
debug {
- testCoverageEnabled true
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
testProguardFile 'test-proguard-rules.txt'
versionNameSuffix "-debug-" + getBranchName()
+ enableUnitTestCoverage true
+ enableAndroidTestCoverage true
}
}
@@ -292,10 +314,12 @@ android {
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\""
@@ -327,10 +351,12 @@ android {
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\""
@@ -350,17 +376,21 @@ android {
compileOptions {
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
- jvmTarget = "1.8"
+ jvmTarget = "17"
}
buildToolsVersion buildToolsVersion
buildFeatures {
viewBinding true
+ compose true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion '1.5.8'
}
namespace 'fr.free.nrw.commons'
lint {
diff --git a/app/src/androidTest/java/fr/free/nrw/commons/AboutActivityTest.kt b/app/src/androidTest/java/fr/free/nrw/commons/AboutActivityTest.kt
index 9425a1d47..50dfe8e7f 100644
--- a/app/src/androidTest/java/fr/free/nrw/commons/AboutActivityTest.kt
+++ b/app/src/androidTest/java/fr/free/nrw/commons/AboutActivityTest.kt
@@ -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().getVersionNameWithSha())
- )
+ withText(getApplicationContext().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),
+ ),
)
}
}
diff --git a/app/src/androidTest/java/fr/free/nrw/commons/LoginActivityTest.kt b/app/src/androidTest/java/fr/free/nrw/commons/LoginActivityTest.kt
index 5039a2495..9bfc9321b 100644
--- a/app/src/androidTest/java/fr/free/nrw/commons/LoginActivityTest.kt
+++ b/app/src/androidTest/java/fr/free/nrw/commons/LoginActivityTest.kt
@@ -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),
+ ),
)
}
@@ -64,4 +66,4 @@ class LoginActivityTest {
fun orientationChange() {
UITestHelper.changeOrientation(activityRule)
}
-}
\ No newline at end of file
+}
diff --git a/app/src/androidTest/java/fr/free/nrw/commons/MainActivityTest.kt b/app/src/androidTest/java/fr/free/nrw/commons/MainActivityTest.kt
index c898ca029..3d2fc9e48 100644
--- a/app/src/androidTest/java/fr/free/nrw/commons/MainActivityTest.kt
+++ b/app/src/androidTest/java/fr/free/nrw/commons/MainActivityTest.kt
@@ -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)
- )
- }
- }
-
-}
\ No newline at end of file
+}
diff --git a/app/src/androidTest/java/fr/free/nrw/commons/ProfileActivityTest.kt b/app/src/androidTest/java/fr/free/nrw/commons/ProfileActivityTest.kt
index 524274d54..003fc0674 100644
--- a/app/src/androidTest/java/fr/free/nrw/commons/ProfileActivityTest.kt
+++ b/app/src/androidTest/java/fr/free/nrw/commons/ProfileActivityTest.kt
@@ -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))
}
-
}
diff --git a/app/src/androidTest/java/fr/free/nrw/commons/ReviewActivityTest.kt b/app/src/androidTest/java/fr/free/nrw/commons/ReviewActivityTest.kt
index dd52dbc3b..3f6487e47 100644
--- a/app/src/androidTest/java/fr/free/nrw/commons/ReviewActivityTest.kt
+++ b/app/src/androidTest/java/fr/free/nrw/commons/ReviewActivityTest.kt
@@ -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)
}
-
-}
\ No newline at end of file
+}
diff --git a/app/src/androidTest/java/fr/free/nrw/commons/SearchActivityTest.kt b/app/src/androidTest/java/fr/free/nrw/commons/SearchActivityTest.kt
index c42b49e92..69ce412b9 100644
--- a/app/src/androidTest/java/fr/free/nrw/commons/SearchActivityTest.kt
+++ b/app/src/androidTest/java/fr/free/nrw/commons/SearchActivityTest.kt
@@ -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)
@@ -56,4 +56,4 @@ class SearchActivityTest {
device.swipe(800, 1400, 600, 1400, 20)
UITestHelper.sleep(1000)
}
-}
\ No newline at end of file
+}
diff --git a/app/src/androidTest/java/fr/free/nrw/commons/SettingsActivityLoggedInTest.kt b/app/src/androidTest/java/fr/free/nrw/commons/SettingsActivityLoggedInTest.kt
index 3d464fb5a..ec132b447 100644
--- a/app/src/androidTest/java/fr/free/nrw/commons/SettingsActivityLoggedInTest.kt
+++ b/app/src/androidTest/java/fr/free/nrw/commons/SettingsActivityLoggedInTest.kt
@@ -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)
}
-
-}
\ No newline at end of file
+}
diff --git a/app/src/androidTest/java/fr/free/nrw/commons/SettingsActivityTest.kt b/app/src/androidTest/java/fr/free/nrw/commons/SettingsActivityTest.kt
index 211483a83..c5a91cd56 100644
--- a/app/src/androidTest/java/fr/free/nrw/commons/SettingsActivityTest.kt
+++ b/app/src/androidTest/java/fr/free/nrw/commons/SettingsActivityTest.kt
@@ -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(6, click()),
)
- ).perform(
- RecyclerViewActions.actionOnItemAtPosition(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
diff --git a/app/src/androidTest/java/fr/free/nrw/commons/UITestHelper.kt b/app/src/androidTest/java/fr/free/nrw/commons/UITestHelper.kt
index 26d6f9246..ebb06e4af 100644
--- a/app/src/androidTest/java/fr/free/nrw/commons/UITestHelper.kt
+++ b/app/src/androidTest/java/fr/free/nrw/commons/UITestHelper.kt
@@ -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, position: Int
+ parentMatcher: Matcher,
+ position: Int,
): Matcher {
-
return object : TypeSafeMatcher() {
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 changeOrientation(activityRule: ActivityTestRule) {
@@ -174,6 +187,7 @@ class UITestHelper {
fun first(matcher: Matcher): Matcher? {
return object : BaseMatcher() {
var isFirst = true
+
override fun matches(item: Any): Boolean {
if (isFirst && matcher.matches(item)) {
isFirst = false
@@ -188,4 +202,4 @@ class UITestHelper {
}
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/androidTest/java/fr/free/nrw/commons/UploadCancelledTest.kt b/app/src/androidTest/java/fr/free/nrw/commons/UploadCancelledTest.kt
index 4041b92e5..c3d3dc3c3 100644
--- a/app/src/androidTest/java/fr/free/nrw/commons/UploadCancelledTest.kt
+++ b/app/src/androidTest/java/fr/free/nrw/commons/UploadCancelledTest.kt
@@ -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 =
@@ -48,14 +50,14 @@ class UploadCancelledTest {
try {
Intents.init()
} catch (ex: 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))
}
@@ -64,130 +66,137 @@ class UploadCancelledTest {
try {
Intents.release()
} catch (ex: 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(
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())
}
diff --git a/app/src/androidTest/java/fr/free/nrw/commons/UploadTest.kt b/app/src/androidTest/java/fr/free/nrw/commons/UploadTest.kt
index 8370e9848..88c7e5d3d 100644
--- a/app/src/androidTest/java/fr/free/nrw/commons/UploadTest.kt
+++ b/app/src/androidTest/java/fr/free/nrw/commons/UploadTest.kt
@@ -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)
@@ -61,7 +72,6 @@ class UploadTest {
try {
Intents.init()
} catch (ex: IllegalStateException) {
-
}
UITestHelper.loginUser()
UITestHelper.skipWelcome()
@@ -94,14 +104,13 @@ class UploadTest {
dismissWarning("Yes")
onView(allOf(isDisplayed(), withId(R.id.tv_title)))
- .perform(replaceText(commonsFileName))
+ .perform(replaceText(commonsFileName))
onView(allOf(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(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(isDisplayed(), withId(R.id.tv_title)))
- .perform(replaceText(commonsFileName))
+ .perform(replaceText(commonsFileName))
onView(withId(R.id.rv_descriptions)).perform(
- RecyclerViewActions
- .actionOnItemAtPosition(0,
- MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Test description")))
+ RecyclerViewActions
+ .actionOnItemAtPosition(
+ 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(1,
- MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Description")))
+ RecyclerViewActions
+ .actionOnItemAtPosition(
+ 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(withId(R.id.fab_plus), isDisplayed()))
- .perform(click())
+ .perform(click())
// Click gallery
onView(allOf(withId(R.id.fab_gallery), isDisplayed()))
- .perform(click())
+ .perform(click())
}
-}
\ No newline at end of file
+}
diff --git a/app/src/androidTest/java/fr/free/nrw/commons/WelcomeActivityTest.kt b/app/src/androidTest/java/fr/free/nrw/commons/WelcomeActivityTest.kt
index 777a1859a..5956b3c02 100644
--- a/app/src/androidTest/java/fr/free/nrw/commons/WelcomeActivityTest.kt
+++ b/app/src/androidTest/java/fr/free/nrw/commons/WelcomeActivityTest.kt
@@ -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))
}
}
}
@@ -130,4 +130,4 @@ class WelcomeActivityTest {
fun orientationChange() {
UITestHelper.changeOrientation(activityRule)
}
-}
\ No newline at end of file
+}
diff --git a/app/src/androidTest/java/fr/free/nrw/commons/ui/PasteSensitiveTextInputEditTextTest.kt b/app/src/androidTest/java/fr/free/nrw/commons/ui/PasteSensitiveTextInputEditTextTest.kt
index 37ea557d1..647c5bbda 100644
--- a/app/src/androidTest/java/fr/free/nrw/commons/ui/PasteSensitiveTextInputEditTextTest.kt
+++ b/app/src/androidTest/java/fr/free/nrw/commons/ui/PasteSensitiveTextInputEditTextTest.kt
@@ -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)
}
@@ -40,4 +43,4 @@ class PasteSensitiveTextInputEditTextTest {
textView!!.setFormattingAllowed(false)
Assert.assertFalse(fieldFormattingAllowed.getBoolean(textView))
}
-}
\ No newline at end of file
+}
diff --git a/app/src/androidTest/java/fr/free/nrw/commons/util/MyViewAction.kt b/app/src/androidTest/java/fr/free/nrw/commons/util/MyViewAction.kt
index 955c712b9..52ac18e4d 100644
--- a/app/src/androidTest/java/fr/free/nrw/commons/util/MyViewAction.kt
+++ b/app/src/androidTest/java/fr/free/nrw/commons/util/MyViewAction.kt
@@ -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? {
- return null
- }
+ fun typeTextInChildViewWithId(
+ id: Int,
+ textToBeTyped: String,
+ ): ViewAction =
+ object : ViewAction {
+ override fun getConstraints(): Matcher? = 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(id) as EditText
v.setText(textToBeTyped)
}
}
- }
- fun selectSpinnerItemInChildViewWithId(id: Int, position: Int): ViewAction {
- return object : ViewAction {
- override fun getConstraints(): Matcher? {
- return null
- }
+ fun selectSpinnerItemInChildViewWithId(
+ id: Int,
+ position: Int,
+ ): ViewAction =
+ object : ViewAction {
+ override fun getConstraints(): Matcher? = 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(id) as AppCompatSpinner
v.setSelection(position)
}
}
- }
- fun clickItemWithId(id: Int, position: Int): ViewAction {
- return object : ViewAction {
- override fun getConstraints(): Matcher? {
- return null
- }
+ fun clickItemWithId(
+ id: Int,
+ position: Int,
+ ): ViewAction =
+ object : ViewAction {
+ override fun getConstraints(): Matcher? = 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(id) as View
v.performClick()
}
}
- }
-
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 5ba49201a..fb776920e 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,262 +1,265 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ xmlns:tools="http://schemas.android.com/tools">
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
-
+
-
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
-
+
+
-
+
+
+
+
+
+
-
-
-
+
-
-
+
+
+
+
+
-
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
-
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
+
+
+
+
-
-
-
-
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
diff --git a/app/src/main/java/fr/free/nrw/commons/AboutActivity.java b/app/src/main/java/fr/free/nrw/commons/AboutActivity.java
index ca433a263..dcc9bfd43 100644
--- a/app/src/main/java/fr/free/nrw/commons/AboutActivity.java
+++ b/app/src/main/java/fr/free/nrw/commons/AboutActivity.java
@@ -180,8 +180,8 @@ public class AboutActivity extends BaseActivity {
getString(R.string.about_translate_cancel),
positiveButtonRunnable,
() -> {},
- spinner,
- true);
+ spinner
+ );
}
}
diff --git a/app/src/main/java/fr/free/nrw/commons/BaseMarker.kt b/app/src/main/java/fr/free/nrw/commons/BaseMarker.kt
index 277faea84..28b01d603 100644
--- a/app/src/main/java/fr/free/nrw/commons/BaseMarker.kt
+++ b/app/src/main/java/fr/free/nrw/commons/BaseMarker.kt
@@ -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
+ }
}
}
-
-
-
-
-
-
-
diff --git a/app/src/main/java/fr/free/nrw/commons/BetaConstants.kt b/app/src/main/java/fr/free/nrw/commons/BetaConstants.kt
index 018d787f9..c0c0b9a61 100644
--- a/app/src/main/java/fr/free/nrw/commons/BetaConstants.kt
+++ b/app/src/main/java/fr/free/nrw/commons/BetaConstants.kt
@@ -10,9 +10,10 @@ 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
*/
const val DEPICTS_PROPERTY = "P180"
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/CameraPosition.kt b/app/src/main/java/fr/free/nrw/commons/CameraPosition.kt
index 31136e61b..e3a644c6a 100644
--- a/app/src/main/java/fr/free/nrw/commons/CameraPosition.kt
+++ b/app/src/main/java/fr/free/nrw/commons/CameraPosition.kt
@@ -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 {
- override fun createFromParcel(parcel: Parcel): CameraPosition {
- return CameraPosition(parcel)
- }
+ override fun createFromParcel(parcel: Parcel): CameraPosition = CameraPosition(parcel)
- override fun newArray(size: Int): Array {
- return arrayOfNulls(size)
- }
+ override fun newArray(size: Int): Array = arrayOfNulls(size)
}
}
diff --git a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java
deleted file mode 100644
index 93413213d..000000000
--- a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java
+++ /dev/null
@@ -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 pauseUploads = new HashMap<>();
-
- /**
- * In-memory list of uploads that have been cancelled by the user
- */
- public static HashSet 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 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();
- }
- }
-}
-
diff --git a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.kt b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.kt
new file mode 100644
index 000000000..9ed19d686
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.kt
@@ -0,0 +1,414 @@
+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.BookmarkItemsDao
+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.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
+
+ try {
+ contributionDao.deleteAll()
+ } catch (e: SQLiteException) {
+ Timber.e(e)
+ }
+ BookmarkPicturesDao.Table.onDelete(db)
+ BookmarkLocationsDao.Table.onDelete(db)
+ BookmarkItemsDao.Table.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)
+ }
+ }
+ }
+ }
+}
+
diff --git a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPicker.java b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPicker.java
deleted file mode 100644
index 58801c499..000000000
--- a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPicker.java
+++ /dev/null
@@ -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;
- }
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.java b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.java
deleted file mode 100644
index 035f542b3..000000000
--- a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.java
+++ /dev/null
@@ -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 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);
- }
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerConstants.java b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerConstants.java
deleted file mode 100644
index 060a15c88..000000000
--- a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerConstants.java
+++ /dev/null
@@ -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() {
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerViewModel.java b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerViewModel.java
deleted file mode 100644
index 57bb238d2..000000000
--- a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerViewModel.java
+++ /dev/null
@@ -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 {
-
- /**
- * Wrapping CameraPosition with MutableLiveData
- */
- private final MutableLiveData 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
- * @param response Response
- */
- @Override
- public void onResponse(final @NotNull Call call,
- final Response response) {
- if (response.body() == null) {
- result.setValue(null);
- return;
- }
- result.setValue(response.body());
- }
-
- @Override
- public void onFailure(final @NotNull Call call, final @NotNull Throwable t) {
- Timber.e(t);
- }
-
- /**
- * Gets live CameraPosition
- *
- * @return MutableLiveData
- */
- public MutableLiveData getResult() {
- return result;
- }
-
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/Media.kt b/app/src/main/java/fr/free/nrw/commons/Media.kt
index 65e66e279..025302cfd 100644
--- a/app/src/main/java/fr/free/nrw/commons/Media.kt
+++ b/app/src/main/java/fr/free/nrw/commons/Media.kt
@@ -2,9 +2,12 @@ package fr.free.nrw.commons
import android.os.Parcelable
import fr.free.nrw.commons.location.LatLng
-import kotlinx.parcelize.Parcelize
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 +17,6 @@ class Media constructor(
*/
var pageId: String = UUID.randomUUID().toString(),
var thumbUrl: String? = null,
-
/**
* Gets image URL
* @return Image URL
@@ -26,16 +28,11 @@ class Media constructor(
*/
var filename: String? = null,
/**
- * Gets the file description.
+ * Gets or sets the file description.
* @return file description as a string
- */
- // monolingual description on input...
- /**
- * Sets the file description.
* @param fallbackDescription the new description of the file
*/
var fallbackDescription: String? = null,
-
/**
* Gets the upload date of the file.
* Can be null.
@@ -43,28 +40,19 @@ class Media constructor(
*/
var dateUploaded: Date? = null,
/**
- * Gets the license name of the file.
+ * Gets or sets the license name of the file.
* @return license as a String
- */
- /**
- * Sets the license name of the file.
- *
* @param license license name as a String
*/
var license: String? = null,
var licenseUrl: String? = null,
/**
- * Gets the name of the creator of the file.
+ * Gets or sets the name of the creator of the file.
* @return author name as a String
- */
- /**
- * Sets the author name of the file.
* @param author creator name as a string
*/
var author: String? = null,
-
- var user:String?=null,
-
+ var user: String? = null,
/**
* Gets the categories the file falls under.
* @return file categories as an ArrayList of Strings
@@ -83,23 +71,23 @@ class Media constructor(
* Stores the mapping of category title to hidden attribute
* Example: "Mountains" => false, "CC-BY-SA-2.0" => true
*/
- var categoriesHiddenStatus: Map = emptyMap()
+ var categoriesHiddenStatus: Map = emptyMap(),
) : Parcelable {
-
constructor(
captions: Map,
categories: List?,
filename: String?,
fallbackDescription: String?,
- author: String?, user:String?
+ author: String?,
+ user: String?,
) : this(
filename = filename,
fallbackDescription = fallbackDescription,
dateUploaded = Date(),
author = author,
- user=user,
+ user = user,
categories = categories,
- captions = captions
+ captions = captions,
)
/**
@@ -108,10 +96,11 @@ class Media constructor(
*/
val displayTitle: String
get() =
- if (filename != null)
+ if (filename != null) {
pageTitle.displayTextWithoutNamespace.replaceFirst("[.][^.]+$".toRegex(), "")
- else
+ } else {
""
+ }
/**
* Gets file page title
@@ -127,17 +116,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? = 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
}
diff --git a/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.kt b/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.kt
index b18c343e0..2ff54959d 100644
--- a/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.kt
+++ b/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.kt
@@ -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.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,46 @@ 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) -> IdAndCaptions(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 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 =
+ Single.ambArray(
+ mediaClient
+ .getMediaById(PAGE_ID_PREFIX + media.pageId)
+ .onErrorResumeNext { Single.never() },
+ mediaClient
+ .getMediaSuppressingErrors(media.filename)
+ .onErrorResumeNext { Single.never() },
+ )
- fun refresh(media: Media): Single {
- return 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);
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/Urls.kt b/app/src/main/java/fr/free/nrw/commons/Urls.kt
index d2f6f4caa..3eb7ee243 100644
--- a/app/src/main/java/fr/free/nrw/commons/Urls.kt
+++ b/app/src/main/java/fr/free/nrw/commons/Urls.kt
@@ -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"
diff --git a/app/src/main/java/fr/free/nrw/commons/WelcomeActivity.java b/app/src/main/java/fr/free/nrw/commons/WelcomeActivity.java
index bed2a8755..c8cedfef1 100644
--- a/app/src/main/java/fr/free/nrw/commons/WelcomeActivity.java
+++ b/app/src/main/java/fr/free/nrw/commons/WelcomeActivity.java
@@ -50,6 +50,7 @@ public class WelcomeActivity extends BaseActivity {
copyrightBinding = PopupForCopyrightBinding.inflate(getLayoutInflater());
final View contactPopupView = copyrightBinding.getRoot();
dialogBuilder.setView(contactPopupView);
+ dialogBuilder.setCancelable(false);
dialog = dialogBuilder.create();
dialog.show();
diff --git a/app/src/main/java/fr/free/nrw/commons/actions/PageEditClient.kt b/app/src/main/java/fr/free/nrw/commons/actions/PageEditClient.kt
index 0036f2f8e..a3d6de257 100644
--- a/app/src/main/java/fr/free/nrw/commons/actions/PageEditClient.kt
+++ b/app/src/main/java/fr/free/nrw/commons/actions/PageEditClient.kt
@@ -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 {
- return try {
- pageEditInterface.postEdit(pageTitle, summary, text, csrfTokenClient.getTokenBlocking())
- .map { editResponse -> editResponse.edit()!!.editSucceeded() }
+ fun edit(
+ pageTitle: String,
+ text: String,
+ summary: String,
+ ): Observable =
+ 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 =
+ 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 {
- return try {
- pageEditInterface.postAppendEdit(pageTitle, summary, appendText, csrfTokenClient.getTokenBlocking())
+ fun appendEdit(
+ pageTitle: String,
+ appendText: String,
+ summary: String,
+ ): Observable =
+ 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 {
- return try {
- pageEditInterface.postPrependEdit(pageTitle, summary, prependText, csrfTokenClient.getTokenBlocking())
+ fun prependEdit(
+ pageTitle: String,
+ prependText: String,
+ summary: String,
+ ): Observable =
+ 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 =
+ 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{
- return try {
- pageEditInterface.postCaptions(summary, title, language,
- value, csrfTokenClient.getTokenBlocking()
- ).map { it.success }
+ fun setCaptions(
+ summary: String,
+ title: String,
+ language: String,
+ value: String,
+ ): Observable =
+ 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
*/
- fun getCurrentWikiText(title: String): Single {
- return pageEditInterface.getWikiText(title).map {
- it.query()?.pages()?.get(0)?.revisions()?.get(0)?.content()
+ fun getCurrentWikiText(title: String): Single =
+ pageEditInterface.getWikiText(title).map {
+ it
+ .query()
+ ?.pages()
+ ?.get(0)
+ ?.revisions()
+ ?.get(0)
+ ?.content()
}
- }
}
diff --git a/app/src/main/java/fr/free/nrw/commons/actions/PageEditInterface.kt b/app/src/main/java/fr/free/nrw/commons/actions/PageEditInterface.kt
index 56f0bf610..db43bb620 100644
--- a/app/src/main/java/fr/free/nrw/commons/actions/PageEditInterface.kt
+++ b/app/src/main/java/fr/free/nrw/commons/actions/PageEditInterface.kt
@@ -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
+
+ /**
+ * 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
/**
@@ -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
/**
@@ -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
+ @FormUrlEncoded
+ @Headers("Cache-Control: no-cache")
+ @POST(MW_API_PREFIX + "action=edit§ion=new")
+ fun postNewSection(
+ @Field("title") title: String,
+ @Field("summary") summary: String,
+ @Field("sectiontitle") sectionTitle: String,
+ @Field("text") sectionText: String,
+ @Field("token") token: String,
+ ): Observable
@FormUrlEncoded
@Headers("Cache-Control: no-cache")
@@ -83,7 +125,7 @@ interface PageEditInterface {
@Field("title") title: String,
@Field("language") language: String,
@Field("value") value: String,
- @Field("token") token: String
+ @Field("token") token: String,
): Observable
/**
@@ -93,6 +135,6 @@ interface PageEditInterface {
*/
@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
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/actions/ThanksClient.kt b/app/src/main/java/fr/free/nrw/commons/actions/ThanksClient.kt
index 06ad63d25..1dcf93edf 100644
--- a/app/src/main/java/fr/free/nrw/commons/actions/ThanksClient.kt
+++ b/app/src/main/java/fr/free/nrw/commons/actions/ThanksClient.kt
@@ -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 {
- 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 =
+ 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)
- }
- }
}
-
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/actions/ThanksInterface.kt b/app/src/main/java/fr/free/nrw/commons/actions/ThanksInterface.kt
index bc7d224af..62934d0f2 100644
--- a/app/src/main/java/fr/free/nrw/commons/actions/ThanksInterface.kt
+++ b/app/src/main/java/fr/free/nrw/commons/actions/ThanksInterface.kt
@@ -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
}
diff --git a/app/src/main/java/fr/free/nrw/commons/activity/SingleWebViewActivity.kt b/app/src/main/java/fr/free/nrw/commons/activity/SingleWebViewActivity.kt
new file mode 100644
index 000000000..0583ae2f9
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/activity/SingleWebViewActivity.kt
@@ -0,0 +1,209 @@
+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.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 = {
+ // TODO Redirect the user to login screen like we do when the user logout's
+ 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(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("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)
+ }
+ }
+}
+
diff --git a/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.java b/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.java
deleted file mode 100644
index 53903769d..000000000
--- a/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.java
+++ /dev/null
@@ -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);
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.kt b/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.kt
new file mode 100644
index 000000000..aa86cd0d8
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.kt
@@ -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
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java
deleted file mode 100644
index 0b6d1831c..000000000
--- a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java
+++ /dev/null
@@ -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);
- }
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.kt b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.kt
new file mode 100644
index 000000000..75c4ac26d
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.kt
@@ -0,0 +1,404 @@
+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 fr.free.nrw.commons.BuildConfig
+import fr.free.nrw.commons.CommonsApplication
+import fr.free.nrw.commons.R
+import fr.free.nrw.commons.Utils
+import fr.free.nrw.commons.auth.login.LoginCallback
+import fr.free.nrw.commons.auth.login.LoginClient
+import fr.free.nrw.commons.auth.login.LoginResult
+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.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 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)
+ }
+
+ 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)
+
+ binding = ActivityLoginBinding.inflate(layoutInflater)
+ 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(::onEditorAction)
+
+ 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)
+ }
+ }
+ }
+
+ 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_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() =
+ Utils.handleWebUrl(this, Uri.parse(BuildConfig.FORGOT_PASSWORD_URL))
+
+ private fun onPrivacyPolicyClicked() =
+ Utils.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,
+ twoFactorCode,
+ Locale.getDefault().language,
+ object : LoginCallback {
+ override fun success(loginResult: LoginResult) = runOnUiThread {
+ Timber.d("Login Success")
+ progressDialog!!.dismiss()
+ onLoginSuccess(loginResult)
+ }
+
+ override fun twoFactorPrompt(caught: Throwable, token: String?) = runOnUiThread {
+ Timber.d("Requesting 2FA prompt")
+ progressDialog!!.dismiss()
+ askUserForTwoFactorAuth()
+ }
+
+ 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() {
+ progressDialog!!.dismiss()
+ with(binding!!) {
+ twoFactorContainer.visibility = View.VISIBLE
+ loginTwoFactor.visibility = View.VISIBLE
+ loginTwoFactor.requestFocus()
+ }
+ val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
+ imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY)
+ showMessageAndCancelDialog(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"
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java b/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java
deleted file mode 100644
index f5395ceda..000000000
--- a/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java
+++ /dev/null
@@ -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);
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.kt b/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.kt
new file mode 100644
index 000000000..c9eb7d2f1
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.kt
@@ -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()
+ .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)
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.java b/app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.java
deleted file mode 100644
index be90bb4bb..000000000
--- a/app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.java
+++ /dev/null
@@ -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);
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.kt b/app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.kt
new file mode 100644
index 000000000..5b48ecd8f
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.kt
@@ -0,0 +1,75 @@
+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 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)
+ 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
+ }
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.java b/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.java
deleted file mode 100644
index 643725604..000000000
--- a/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.java
+++ /dev/null
@@ -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;
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.kt b/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.kt
new file mode 100644
index 000000000..367989f14
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.kt
@@ -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?,
+ 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
+ ) = 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
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticatorService.java b/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticatorService.java
deleted file mode 100644
index bb41f27aa..000000000
--- a/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticatorService.java
+++ /dev/null
@@ -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();
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticatorService.kt b/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticatorService.kt
new file mode 100644
index 000000000..852536a48
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticatorService.kt
@@ -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
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/auth/csrf/CsrfTokenClient.kt b/app/src/main/java/fr/free/nrw/commons/auth/csrf/CsrfTokenClient.kt
index 9e3136237..f35e5f003 100644
--- a/app/src/main/java/fr/free/nrw/commons/auth/csrf/CsrfTokenClient.kt
+++ b/app/src/main/java/fr/free/nrw/commons/auth/csrf/CsrfTokenClient.kt
@@ -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? = null
-
@Throws(Throwable::class)
fun getTokenBlocking(): String {
var token = ""
@@ -38,11 +36,20 @@ class CsrfTokenClient(
}
// 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,65 @@ class CsrfTokenClient(
}
@VisibleForTesting
- fun request(service: CsrfTokenInterface, cb: Callback): Call =
- 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 =
+ 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()
+ },
+ )
@VisibleForTesting
- fun requestToken(service: CsrfTokenInterface, cb: Callback): Call {
+ fun requestToken(
+ service: CsrfTokenInterface,
+ cb: Callback,
+ ): Call {
val call = service.getCsrfTokenCall()
- call.enqueue(object : retrofit2.Callback {
- override fun onResponse(call: Call, response: Response) {
- if (call.isCanceled) {
- return
+ call.enqueue(
+ object : retrofit2.Callback {
+ override fun onResponse(
+ call: Call,
+ response: Response,
+ ) {
+ if (call.isCanceled) {
+ return
+ }
+ cb.success(response.body()!!.query()!!.csrfToken())
}
- cb.success(response.body()!!.query()!!.csrfToken())
- }
- override fun onFailure(call: Call, t: Throwable) {
- if (call.isCanceled) {
- return
+ override fun onFailure(
+ call: Call,
+ 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 +150,31 @@ 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(
+ 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."))
+ // 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)
- })
+ override fun error(caught: Throwable) = callback.failure(caught)
+ },
+ )
private fun cancel() {
loginClient.cancel()
@@ -155,7 +186,9 @@ class CsrfTokenClient(
interface Callback {
fun success(token: String?)
+
fun failure(caught: Throwable?)
+
fun twoFactorPrompt()
}
@@ -167,5 +200,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)
diff --git a/app/src/main/java/fr/free/nrw/commons/auth/csrf/LogoutClient.kt b/app/src/main/java/fr/free/nrw/commons/auth/csrf/LogoutClient.kt
index eb462beaf..84481c918 100644
--- a/app/src/main/java/fr/free/nrw/commons/auth/csrf/LogoutClient.kt
+++ b/app/src/main/java/fr/free/nrw/commons/auth/csrf/LogoutClient.kt
@@ -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()
-}
\ No newline at end of file
+class LogoutClient
+ @Inject
+ constructor(
+ private val store: CommonsCookieStorage,
+ ) {
+ fun logout() = store.clear()
+ }
diff --git a/app/src/main/java/fr/free/nrw/commons/auth/login/LoginCallback.kt b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginCallback.kt
index cfaf90b58..8092f73ae 100644
--- a/app/src/main/java/fr/free/nrw/commons/auth/login/LoginCallback.kt
+++ b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginCallback.kt
@@ -2,7 +2,13 @@ package fr.free.nrw.commons.auth.login
interface LoginCallback {
fun success(loginResult: LoginResult)
- fun twoFactorPrompt(caught: Throwable, token: String?)
+
+ fun twoFactorPrompt(
+ caught: Throwable,
+ token: String?,
+ )
+
fun passwordResetPrompt(token: String?)
+
fun error(caught: Throwable)
}
diff --git a/app/src/main/java/fr/free/nrw/commons/auth/login/LoginClient.kt b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginClient.kt
index 44bb68448..2a799c847 100644
--- a/app/src/main/java/fr/free/nrw/commons/auth/login/LoginClient.kt
+++ b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginClient.kt
@@ -4,9 +4,9 @@ import android.text.TextUtils
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 +16,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? = null
private var loginCall: Call? = null
@@ -30,80 +32,116 @@ 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 {
- override fun onResponse(call: Call, response: Response) {
- login(
- userName, password, null, null, response.body()!!.query()!!.loginToken(),
- userLanguage, cb
- )
- }
-
- override fun onFailure(call: Call, caught: Throwable) {
- if (call.isCanceled) {
- return
+ tokenCall!!.enqueue(
+ object : Callback {
+ override fun onResponse(
+ call: Call,
+ response: Response,
+ ) {
+ login(
+ userName,
+ password,
+ null,
+ null,
+ response.body()!!.query()!!.loginToken(),
+ userLanguage,
+ cb,
+ )
}
- cb.error(caught)
- }
- })
+
+ override fun onFailure(
+ call: Call,
+ 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?,
+ 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() && retypedPassword.isNullOrEmpty()) {
+ loginInterface.postLogIn(userName, password, loginToken, userLanguage, WIKIPEDIA_URL)
+ } else {
+ loginInterface.postLogIn(
+ userName,
+ password,
+ retypedPassword,
+ twoFactorCode,
+ loginToken,
+ userLanguage,
+ true,
+ )
+ }
- loginCall!!.enqueue(object : Callback {
- override fun onResponse(
- call: Call,
- response: Response
- ) {
- 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 {
+ override fun onResponse(
+ call: Call,
+ response: Response,
+ ) {
+ 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,
+ )
- is ResetPasswordResult -> cb.passwordResetPrompt(loginToken)
+ is ResetPasswordResult -> cb.passwordResetPrompt(loginToken)
- is LoginResult.Result -> cb.error(
- LoginFailedException(loginResult.message)
- )
+ 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, t: Throwable) {
- if (call.isCanceled) {
- return
+ override fun onFailure(
+ call: Call,
+ t: Throwable,
+ ) {
+ if (call.isCanceled) {
+ return
+ }
+ cb.error(t)
}
- cb.error(t)
- }
- })
+ },
+ )
}
fun doLogin(
@@ -111,43 +149,65 @@ class LoginClient(private val loginInterface: LoginInterface) {
password: String,
twoFactorCode: String,
userLanguage: String,
- loginCallback: LoginCallback
+ loginCallback: LoginCallback,
) {
- getLoginToken().enqueue(object :Callback{
- override fun onResponse(
- call: Call,
- response: Response
- ) = if (response.isSuccessful){
- val loginToken = response.body()?.query()?.loginToken()
- loginToken?.let {
- login(username, password, null, twoFactorCode, it, userLanguage, loginCallback)
- } ?: run {
+ getLoginToken().enqueue(
+ object : Callback {
+ override fun onResponse(
+ call: Call,
+ response: Response,
+ ) = if (response.isSuccessful) {
+ val loginToken = response.body()?.query()?.loginToken()
+ loginToken?.let {
+ login(username, password, null, twoFactorCode, 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, t: Throwable) {
- loginCallback.error(t)
- }
- })
+ override fun onFailure(
+ call: Call,
+ t: Throwable,
+ ) {
+ loginCallback.error(t)
+ }
+ },
+ )
}
+
@Throws(Throwable::class)
- fun loginBlocking(userName: String, password: String, twoFactorCode: String?) {
+ fun loginBlocking(
+ userName: String,
+ password: String,
+ twoFactorCode: String?,
+ ) {
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()) {
+ loginInterface.postLogIn(userName, password, loginToken, userLanguage, WIKIPEDIA_URL)
+ } else {
+ loginInterface.postLogIn(
+ userName,
+ password,
+ null,
+ twoFactorCode,
+ loginToken,
+ userLanguage,
+ true,
+ )
+ }
val response = tempLoginCall.execute()
val loginResponse = response.body() ?: throw IOException("Unexpected response when logging in.")
@@ -166,18 +226,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 {
diff --git a/app/src/main/java/fr/free/nrw/commons/auth/login/LoginFailedException.kt b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginFailedException.kt
index 2f60b2071..fb5ad14c6 100644
--- a/app/src/main/java/fr/free/nrw/commons/auth/login/LoginFailedException.kt
+++ b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginFailedException.kt
@@ -1,3 +1,5 @@
package fr.free.nrw.commons.auth.login
-class LoginFailedException(message: String?) : Throwable(message)
+class LoginFailedException(
+ message: String?,
+) : Throwable(message)
diff --git a/app/src/main/java/fr/free/nrw/commons/auth/login/LoginInterface.kt b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginInterface.kt
index 0502cf8c7..07e1cd45c 100644
--- a/app/src/main/java/fr/free/nrw/commons/auth/login/LoginInterface.kt
+++ b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginInterface.kt
@@ -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
@Headers("Cache-Control: no-cache")
@@ -37,9 +37,11 @@ interface LoginInterface {
@Field("OATHToken") twoFactorCode: String?,
@Field("logintoken") token: String?,
@Field("uselang") userLanguage: String?,
- @Field("logincontinue") loginContinue: Boolean
+ @Field("logincontinue") loginContinue: Boolean,
): Call
@GET(MW_API_PREFIX + "action=query&meta=userinfo&list=users&usprop=groups|cancreate")
- fun getUserInfo(@Query("ususers") userName: String): Observable
-}
\ No newline at end of file
+ fun getUserInfo(
+ @Query("ususers") userName: String,
+ ): Observable
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/auth/login/LoginResponse.kt b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginResponse.kt
index 7e3a80c9e..a96778e38 100644
--- a/app/src/main/java/fr/free/nrw/commons/auth/login/LoginResponse.kt
+++ b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginResponse.kt
@@ -13,9 +13,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 {
@@ -39,7 +37,7 @@ internal class ClientLogin {
}
}
} 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)
diff --git a/app/src/main/java/fr/free/nrw/commons/auth/login/LoginResult.kt b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginResult.kt
index 69e4a7f89..6a7594ec0 100644
--- a/app/src/main/java/fr/free/nrw/commons/auth/login/LoginResult.kt
+++ b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginResult.kt
@@ -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()
@@ -14,20 +14,20 @@ 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 ResetPasswordResult(
status: String,
userName: String?,
password: String?,
- message: String?
+ message: String?,
) : LoginResult(status, userName, password, message)
}
diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkListRootFragment.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkListRootFragment.java
index 281248ca4..ca7dd3f3b 100644
--- a/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkListRootFragment.java
+++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkListRootFragment.java
@@ -2,18 +2,17 @@ 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.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;
@@ -26,6 +25,7 @@ import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.navtab.NavTab;
import java.util.ArrayList;
import java.util.Iterator;
+import timber.log.Timber;
public class BookmarkListRootFragment extends CommonsDaggerSupportFragment implements
FragmentManager.OnBackStackChangedListener,
@@ -48,14 +48,21 @@ public class BookmarkListRootFragment extends CommonsDaggerSupportFragment imple
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();
+
+ switch (order){
+ case 0: listFragment = new BookmarkPicturesFragment();
+ break;
+
+ case 1: listFragment = new BookmarkLocationsFragment();
+ break;
+
+ case 3: listFragment = new BookmarkCategoriesFragment();
+ break;
+ }
if(orderItem == 2) {
listFragment = new BookmarkItemsFragment();
}
- }
+
Bundle featuredArguments = new Bundle();
featuredArguments.putString("categoryName", title);
listFragment.setArguments(featuredArguments);
@@ -129,7 +136,7 @@ public class BookmarkListRootFragment extends CommonsDaggerSupportFragment imple
@Override
public void onMediaClicked(int position) {
- Log.d("deneme8", "on media clicked");
+ Timber.d("on media clicked");
/*container.setVisibility(View.VISIBLE);
((BookmarkFragment)getParentFragment()).tabLayout.setVisibility(View.GONE);
mediaDetails = new MediaDetailPagerFragment(false, true, position);
@@ -237,7 +244,7 @@ public class BookmarkListRootFragment extends CommonsDaggerSupportFragment imple
@Override
public void onItemClick(AdapterView> parent, View view, int position, long id) {
- Log.d("deneme8", "on media clicked");
+ Timber.d("on media clicked");
binding.exploreContainer.setVisibility(View.VISIBLE);
((BookmarkFragment) getParentFragment()).binding.tabLayout.setVisibility(View.GONE);
mediaDetails = MediaDetailPagerFragment.newInstance(false, true);
diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkPages.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkPages.java
deleted file mode 100644
index 71690c5e2..000000000
--- a/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkPages.java
+++ /dev/null
@@ -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;
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkPages.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkPages.kt
new file mode 100644
index 000000000..e0ade52fe
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkPages.kt
@@ -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
+)
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarksPagerAdapter.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarksPagerAdapter.java
index ea3a9a453..f0620032a 100644
--- a/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarksPagerAdapter.java
+++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarksPagerAdapter.java
@@ -49,6 +49,13 @@ public class BookmarksPagerAdapter extends FragmentPagerAdapter {
new BookmarkListRootFragment(locationBundle, this),
context.getString(R.string.title_page_bookmarks_items)));
}
+ final Bundle categoriesBundle = new Bundle();
+ categoriesBundle.putString("categoryName",
+ context.getString(R.string.title_page_bookmarks_categories));
+ categoriesBundle.putInt("order", 3);
+ pages.add(new BookmarkPages(
+ new BookmarkListRootFragment(categoriesBundle, this),
+ context.getString(R.string.title_page_bookmarks_categories)));
notifyDataSetChanged();
}
diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/category/BookmarkCategoriesDao.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/category/BookmarkCategoriesDao.kt
new file mode 100644
index 000000000..71a2d1ec9
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/category/BookmarkCategoriesDao.kt
@@ -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>
+
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/category/BookmarkCategoriesFragment.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/category/BookmarkCategoriesFragment.kt
new file mode 100644
index 000000000..ef5bc613d
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/category/BookmarkCategoriesFragment.kt
@@ -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"
+ )
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/category/BookmarksCategoryModal.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/category/BookmarksCategoryModal.kt
new file mode 100644
index 000000000..ab679611f
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/category/BookmarksCategoryModal.kt
@@ -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
+)
diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsAdapter.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsAdapter.kt
index 64bdf5315..4233d9508 100644
--- a/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsAdapter.kt
+++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsAdapter.kt
@@ -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, val context: Context) :
- RecyclerView.Adapter() {
-
- class BookmarkItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
-
+class BookmarkItemsAdapter(
+ val list: List,
+ val context: Context,
+) : RecyclerView.Adapter() {
+ 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, val context: Context)
}
}
- override fun getItemCount(): Int {
- return list.size
- }
-}
\ No newline at end of file
+ override fun getItemCount(): Int = list.size
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsDao.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsDao.java
index 70c370836..6788a8290 100644
--- a/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsDao.java
+++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsDao.java
@@ -1,5 +1,6 @@
package fr.free.nrw.commons.bookmarks.items;
+import android.annotation.SuppressLint;
import android.content.ContentProviderClient;
import android.content.ContentValues;
import android.database.Cursor;
@@ -134,6 +135,7 @@ public class BookmarkItemsDao {
* @param cursor : Object for storing database data
* @return DepictedItem
*/
+ @SuppressLint("Range")
DepictedItem fromCursor(final Cursor cursor) {
final String fileName = cursor.getString(cursor.getColumnIndex(Table.COLUMN_NAME));
final String description
diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsDao.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsDao.java
index a55ae5e0d..fe4f603f4 100644
--- a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsDao.java
+++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsDao.java
@@ -1,5 +1,6 @@
package fr.free.nrw.commons.bookmarks.locations;
+import android.annotation.SuppressLint;
import android.content.ContentProviderClient;
import android.content.ContentValues;
import android.database.Cursor;
@@ -46,11 +47,11 @@ public class BookmarkLocationsDao {
ContentProviderClient db = clientProvider.get();
try {
cursor = db.query(
- BookmarkLocationsContentProvider.BASE_URI,
- Table.ALL_FIELDS,
- null,
- new String[]{},
- null);
+ BookmarkLocationsContentProvider.BASE_URI,
+ Table.ALL_FIELDS,
+ null,
+ new String[]{},
+ null);
while (cursor != null && cursor.moveToNext()) {
items.add(fromCursor(cursor));
}
@@ -126,11 +127,11 @@ public class BookmarkLocationsDao {
ContentProviderClient db = clientProvider.get();
try {
cursor = db.query(
- BookmarkLocationsContentProvider.BASE_URI,
- Table.ALL_FIELDS,
- Table.COLUMN_NAME + "=?",
- new String[]{bookmarkLocation.name},
- null);
+ BookmarkLocationsContentProvider.BASE_URI,
+ Table.ALL_FIELDS,
+ Table.COLUMN_NAME + "=?",
+ new String[]{bookmarkLocation.name},
+ null);
if (cursor != null && cursor.moveToFirst()) {
return true;
}
@@ -146,10 +147,11 @@ public class BookmarkLocationsDao {
return false;
}
+ @SuppressLint("Range")
@NonNull
Place fromCursor(final Cursor cursor) {
final LatLng location = new LatLng(cursor.getDouble(cursor.getColumnIndex(Table.COLUMN_LAT)),
- cursor.getDouble(cursor.getColumnIndex(Table.COLUMN_LONG)), 1F);
+ cursor.getDouble(cursor.getColumnIndex(Table.COLUMN_LONG)), 1F);
final Sitelinks.Builder builder = new Sitelinks.Builder();
builder.setWikipediaLink(cursor.getString(cursor.getColumnIndex(Table.COLUMN_WIKIPEDIA_LINK)));
@@ -207,40 +209,40 @@ public class BookmarkLocationsDao {
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
public static final String[] ALL_FIELDS = {
- COLUMN_NAME,
- COLUMN_LANGUAGE,
- COLUMN_DESCRIPTION,
- COLUMN_CATEGORY,
- COLUMN_LABEL_TEXT,
- COLUMN_LABEL_ICON,
- COLUMN_LAT,
- COLUMN_LONG,
- COLUMN_IMAGE_URL,
- COLUMN_WIKIPEDIA_LINK,
- COLUMN_WIKIDATA_LINK,
- COLUMN_COMMONS_LINK,
- COLUMN_PIC,
- COLUMN_EXISTS,
+ COLUMN_NAME,
+ COLUMN_LANGUAGE,
+ COLUMN_DESCRIPTION,
+ COLUMN_CATEGORY,
+ COLUMN_LABEL_TEXT,
+ COLUMN_LABEL_ICON,
+ COLUMN_LAT,
+ COLUMN_LONG,
+ COLUMN_IMAGE_URL,
+ COLUMN_WIKIPEDIA_LINK,
+ COLUMN_WIKIDATA_LINK,
+ COLUMN_COMMONS_LINK,
+ COLUMN_PIC,
+ COLUMN_EXISTS,
};
static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME;
static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
- + COLUMN_NAME + " STRING PRIMARY KEY,"
- + COLUMN_LANGUAGE + " STRING,"
- + COLUMN_DESCRIPTION + " STRING,"
- + COLUMN_CATEGORY + " STRING,"
- + COLUMN_LABEL_TEXT + " STRING,"
- + COLUMN_LABEL_ICON + " INTEGER,"
- + COLUMN_LAT + " DOUBLE,"
- + COLUMN_LONG + " DOUBLE,"
- + COLUMN_IMAGE_URL + " STRING,"
- + COLUMN_WIKIPEDIA_LINK + " STRING,"
- + COLUMN_WIKIDATA_LINK + " STRING,"
- + COLUMN_COMMONS_LINK + " STRING,"
- + COLUMN_PIC + " STRING,"
- + COLUMN_EXISTS + " STRING"
- + ");";
+ + COLUMN_NAME + " STRING PRIMARY KEY,"
+ + COLUMN_LANGUAGE + " STRING,"
+ + COLUMN_DESCRIPTION + " STRING,"
+ + COLUMN_CATEGORY + " STRING,"
+ + COLUMN_LABEL_TEXT + " STRING,"
+ + COLUMN_LABEL_ICON + " INTEGER,"
+ + COLUMN_LAT + " DOUBLE,"
+ + COLUMN_LONG + " DOUBLE,"
+ + COLUMN_IMAGE_URL + " STRING,"
+ + COLUMN_WIKIPEDIA_LINK + " STRING,"
+ + COLUMN_WIKIDATA_LINK + " STRING,"
+ + COLUMN_COMMONS_LINK + " STRING,"
+ + COLUMN_PIC + " STRING,"
+ + COLUMN_EXISTS + " STRING"
+ + ");";
public static void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_TABLE_STATEMENT);
@@ -308,4 +310,4 @@ public class BookmarkLocationsDao {
}
}
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsFragment.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsFragment.java
index 65d0e45a8..f5ce556c4 100644
--- a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsFragment.java
+++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsFragment.java
@@ -9,6 +9,7 @@ import android.view.ViewGroup;
import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager;
@@ -33,6 +34,23 @@ public class BookmarkLocationsFragment extends DaggerFragment {
@Inject BookmarkLocationsDao bookmarkLocationDao;
@Inject CommonPlaceClickActions commonPlaceClickActions;
private PlaceAdapter adapter;
+
+ private final ActivityResultLauncher cameraPickLauncherForResult =
+ registerForActivityResult(new StartActivityForResult(),
+ result -> {
+ contributionController.handleActivityResultWithCallback(requireActivity(), callbacks -> {
+ contributionController.onPictureReturnedFromCamera(result, requireActivity(), callbacks);
+ });
+ });
+
+ private final ActivityResultLauncher galleryPickLauncherForResult =
+ registerForActivityResult(new StartActivityForResult(),
+ result -> {
+ contributionController.handleActivityResultWithCallback(requireActivity(), callbacks -> {
+ contributionController.onPictureReturnedFromGallery(result, requireActivity(), callbacks);
+ });
+ });
+
private ActivityResultLauncher inAppCameraLocationPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), new ActivityResultCallback