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/app/build.gradle b/app/build.gradle
index 43c1695da..14bf5f3b7 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -47,18 +47,19 @@ dependencies {
implementation 'com.jakewharton.timber:timber:4.7.1'
implementation 'com.github.deano2390:MaterialShowcaseView:1.2.0'
- implementation "com.google.android.material:material:1.9.0"
+ implementation "com.google.android.material:material:1.12.0"
implementation 'com.karumi:dexter:5.0.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
// Jetpack Compose
- def composeBom = platform('androidx.compose:compose-bom:2024.08.00')
+ def composeBom = platform('androidx.compose:compose-bom:2024.11.00')
- implementation "androidx.activity:activity-compose:1.9.1"
+ 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"
@@ -98,6 +99,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'
@@ -137,7 +139,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'
@@ -226,7 +228,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']
}
}
@@ -312,6 +314,7 @@ android {
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\""
@@ -347,6 +350,7 @@ android {
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\""
@@ -366,11 +370,11 @@ android {
compileOptions {
- sourceCompatibility JavaVersion.VERSION_11
- targetCompatibility JavaVersion.VERSION_11
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
- jvmTarget = "11"
+ jvmTarget = "17"
}
buildToolsVersion buildToolsVersion
@@ -380,7 +384,7 @@ android {
compose true
}
composeOptions {
- kotlinCompilerExtensionVersion '1.3.2'
+ 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 b5a752ef9..50dfe8e7f 100644
--- a/app/src/androidTest/java/fr/free/nrw/commons/AboutActivityTest.kt
+++ b/app/src/androidTest/java/fr/free/nrw/commons/AboutActivityTest.kt
@@ -105,7 +105,7 @@ 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),
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 0277b8324..c3d3dc3c3 100644
--- a/app/src/androidTest/java/fr/free/nrw/commons/UploadCancelledTest.kt
+++ b/app/src/androidTest/java/fr/free/nrw/commons/UploadCancelledTest.kt
@@ -18,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
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 aedbcb133..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
@@ -17,7 +17,7 @@ class PasteSensitiveTextInputEditTextTest {
@Before
fun setup() {
context = ApplicationProvider.getApplicationContext()
- textView = PasteSensitiveTextInputEditText(context)
+ textView = PasteSensitiveTextInputEditText(context!!)
}
// this test has no real value, just % for test code coverage
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 89ed630d8..ab2edf719 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -99,7 +99,6 @@
android:exported="true"
android:hardwareAccelerated="false"
android:icon="@mipmap/ic_launcher"
- android:label="@string/app_name"
android:windowSoftInputMode="adjustResize">
@@ -122,7 +121,7 @@
android:name=".contributions.MainActivity"
android:configChanges="screenSize|keyboard|orientation"
android:icon="@mipmap/ic_launcher"
- android:label="@string/app_name" />
+ />
@@ -172,7 +171,7 @@
android:name=".review.ReviewActivity"
android:label="@string/title_activity_review" />
{},
- 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 1daadb5a1..28b01d603 100644
--- a/app/src/main/java/fr/free/nrw/commons/BaseMarker.kt
+++ b/app/src/main/java/fr/free/nrw/commons/BaseMarker.kt
@@ -46,7 +46,7 @@ class BaseMarker {
val drawable: Drawable = context.resources.getDrawable(drawableResId)
icon =
if (drawable is BitmapDrawable) {
- (drawable as BitmapDrawable).bitmap
+ drawable.bitmap
} else {
val bitmap =
Bitmap.createBitmap(
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 c3dde9caa..000000000
--- a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java
+++ /dev/null
@@ -1,433 +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;
-
- public static Boolean isPaused = false;
-
- /**
- * 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(() -> 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, 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 8c54fd292..000000000
--- a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.java
+++ /dev/null
@@ -1,679 +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.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();
- }
-
- /**
- * Moves the center of the map to the specified coordinates
- *
- */
- private void moveMapTo(double latitude, double longitude){
- if(mapView != null && mapView.getController() != null){
- GeoPoint point = new GeoPoint(latitude, longitude);
-
- mapView.getController().setCenter(point);
- mapView.getController().animateTo(point);
- }
- }
-
- /**
- * Moves the center of the map to the specified coordinates
- * @param point The GeoPoint object which contains the coordinates to move to
- */
- private void moveMapTo(GeoPoint point){
- if(point != null){
- moveMapTo(point.getLatitude(), point.getLongitude());
- }
- }
-
- /**
- * 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() {
- requestLocationPermissions();
-
- //If location metadata is available, move map to that location.
- if(activity.equals("UploadActivity") || activity.equals("MediaActivity")){
- moveMapToMediaLocation();
- } else {
- //If location metadata is not available, move map to device GPS location.
- moveMapToGPSLocation();
- }
-
- modifyLocationButton.setOnClickListener(v -> onClickModifyLocation());
- removeLocationButton.setOnClickListener(v -> onClickRemoveLocation());
- showInMapButton.setOnClickListener(v -> showInMapApp());
- darkThemeSetup();
- }
-
- /**
- * 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();
- moveMapToMediaLocation();
- }
-
- /**
- * 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. Map will center on the location metadata, if available.
- * If there is no location metadata, the map will center on the commons app map center.
- */
- private void showInMapApp() {
- fr.free.nrw.commons.location.LatLng position = null;
-
- if(activity.equals("UploadActivity") && cameraPosition != null){
- //location metadata is available
- position = new fr.free.nrw.commons.location.LatLng(cameraPosition.getLatitude(),
- cameraPosition.getLongitude(), 0.0f);
- } else if(mapView != null){
- //location metadata is not available
- position = new fr.free.nrw.commons.location.LatLng(mapView.getMapCenter().getLatitude(),
- mapView.getMapCenter().getLongitude(), 0.0f);
- }
-
- if(position != null){
- Utils.handleGeoCoordinates(this, position);
- }
- }
-
- /**
- * Moves the center of the map to the media's location, if that data
- * is available.
- */
- private void moveMapToMediaLocation() {
- if (cameraPosition != null) {
-
- GeoPoint point = new GeoPoint(cameraPosition.getLatitude(),
- cameraPosition.getLongitude());
-
- moveMapTo(point);
- }
- }
-
- /**
- * Moves the center of the map to the device's GPS location, if that data is available.
- */
- private void moveMapToGPSLocation(){
- if(locationManager != null){
- fr.free.nrw.commons.location.LatLng location = locationManager.getLastLocation();
-
- if(location != null){
- GeoPoint point = new GeoPoint(location.getLatitude(), location.getLongitude());
-
- moveMapTo(point);
- }
- }
- }
-
- /**
- * 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);
- addMarkerAtGPSLocation();
- } else {
- addMarkerAtGPSLocation();
- locationPermissionsHelper.showLocationOffDialog(this,
- R.string.ask_to_turn_location_on_text);
- }
- }
- }
-
- /**
- * Adds a marker to the map at the most recent GPS location
- * (which may be the current GPS location).
- */
- private void addMarkerAtGPSLocation() {
- fr.free.nrw.commons.location.LatLng currLocation = locationManager.getLastLocation();
- if (currLocation != null) {
- GeoPoint currLocationGeopoint = new GeoPoint(currLocation.getLatitude(),
- currLocation.getLongitude());
- addLocationMarker(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 93efac7b2..025302cfd 100644
--- a/app/src/main/java/fr/free/nrw/commons/Media.kt
+++ b/app/src/main/java/fr/free/nrw/commons/Media.kt
@@ -3,6 +3,7 @@ package fr.free.nrw.commons
import android.os.Parcelable
import fr.free.nrw.commons.location.LatLng
import fr.free.nrw.commons.wikidata.model.page.PageTitle
+import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
import java.util.Date
import java.util.Locale
@@ -124,6 +125,7 @@ class Media constructor(
* 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
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/ThanksClient.kt b/app/src/main/java/fr/free/nrw/commons/actions/ThanksClient.kt
index de716db99..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
@@ -3,7 +3,7 @@ package fr.free.nrw.commons.actions
import fr.free.nrw.commons.CommonsApplication
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException
-import fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_CSRF
+import fr.free.nrw.commons.di.NetworkingModule.Companion.NAMED_COMMONS_CSRF
import io.reactivex.Observable
import javax.inject.Inject
import javax.inject.Named
@@ -32,7 +32,7 @@ class ThanksClient
revisionId.toString(), // Rev
null, // Log
csrfTokenClient.getTokenBlocking(), // Token
- CommonsApplication.getInstance().userAgent, // Source
+ CommonsApplication.instance.userAgent, // Source
).map { mwThankPostResponse ->
mwThankPostResponse.result?.success == 1
}
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..330792fa7
--- /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 progressDailog after configuration change
+ if (progressDialog != null && progressDialog!!.isShowing) {
+ outState.putBoolean(saveProgressDailog, true)
+ } else {
+ outState.putBoolean(saveProgressDailog, false)
+ }
+ outState.putString(
+ saveErrorMessage,
+ binding!!.errorMessage.text.toString()
+ ) //Save the errorMessage
+ outState.putString(
+ saveUsername,
+ binding!!.loginUsername.text.toString()
+ ) // Save the username
+ outState.putString(
+ savePassword,
+ binding!!.loginPassword.text.toString()
+ ) // Save the password
+ }
+
+ override fun onRestoreInstanceState(savedInstanceState: Bundle) {
+ super.onRestoreInstanceState(savedInstanceState)
+ binding!!.loginUsername.setText(savedInstanceState.getString(saveUsername))
+ binding!!.loginPassword.setText(savedInstanceState.getString(savePassword))
+ if (savedInstanceState.getBoolean(saveProgressDailog)) {
+ performLogin()
+ }
+ val errorMessage = savedInstanceState.getString(saveErrorMessage)
+ 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 saveProgressDailog: String = "ProgressDailog_state"
+ const val saveErrorMessage: String = "errorMessage"
+ const val saveUsername: String = "username"
+ const val savePassword: 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 7c2f4a334..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);
- }
- }
-
- /**
- * Returns a Completable that clears existing accounts from account manager
- */
- public Completable logout() {
- return Completable.fromObservable(
- Observable.empty()
- .doOnComplete(
- () -> {
- removeAccount();
- 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/login/LoginClient.kt b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginClient.kt
index 48ca37cf0..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
@@ -237,7 +237,7 @@ class LoginClient(
.subscribe({ response: MwQueryResponse? ->
loginResult.userId = response?.query()?.userInfo()?.id() ?: 0
loginResult.groups =
- response?.query()?.getUserResponse(userName)?.groups ?: emptySet()
+ response?.query()?.getUserResponse(userName)?.getGroups() ?: emptySet()
cb.success(loginResult)
}, { caught: Throwable ->
Timber.e(caught, "Login succeeded but getting group information failed. ")
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/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 850b953e9..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;
@@ -146,6 +147,7 @@ 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)),
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