mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-27 04:43:54 +01:00
Merge branch 'main' into 1-issue#5829
This commit is contained in:
commit
7631dfb57b
656 changed files with 26512 additions and 26467 deletions
7
.idea/inspectionProfiles/Project_Default.xml
generated
7
.idea/inspectionProfiles/Project_Default.xml
generated
|
|
@ -1,16 +1,12 @@
|
|||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="AndroidLintNewerVersionAvailable" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="AutoCloseableResource" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="ClassWithOnlyPrivateConstructors" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="ConfusingElse" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="reportWhenNoStatementFollow" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ControlFlowStatementWithoutBraces" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
<inspection_tool class="DefaultNotLastCaseInSwitch" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="ExplicitThis" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="FieldMayBeFinal" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="LocalCanBeFinal" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="REPORT_VARIABLES" value="true" />
|
||||
<option name="REPORT_PARAMETERS" value="true" />
|
||||
|
|
@ -25,13 +21,11 @@
|
|||
<option name="ignoreInMatchingInstanceof" value="false" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ProblematicWhitespace" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="ProtectedMemberInFinalClass" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="RedundantFieldInitialization" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="RedundantImplements" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="ignoreSerializable" value="false" />
|
||||
<option name="ignoreCloneable" value="false" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="RedundantMethodOverride" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="SimplifiableEqualsExpression" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="TypeParameterExtendsFinalClass" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="UnnecessarilyQualifiedStaticUsage" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
|
|
@ -47,6 +41,5 @@
|
|||
<inspection_tool class="UnnecessaryQualifierForThis" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="UnnecessarySuperConstructor" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="UnnecessaryThis" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="UnnecessaryToStringCall" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
</profile>
|
||||
</component>
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -99,7 +99,6 @@
|
|||
android:exported="true"
|
||||
android:hardwareAccelerated="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter android:label="@string/intent_share_upload_label">
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
|
|
@ -122,7 +121,7 @@
|
|||
android:name=".contributions.MainActivity"
|
||||
android:configChanges="screenSize|keyboard|orientation"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name" />
|
||||
/>
|
||||
<activity
|
||||
android:name=".settings.SettingsActivity"
|
||||
android:label="@string/title_activity_settings" />
|
||||
|
|
@ -172,7 +171,7 @@
|
|||
android:name=".review.ReviewActivity"
|
||||
android:label="@string/title_activity_review" />
|
||||
<activity
|
||||
android:name=".LocationPicker.LocationPickerActivity"
|
||||
android:name=".locationpicker.LocationPickerActivity"
|
||||
android:label="Location Picker" />
|
||||
|
||||
<service
|
||||
|
|
|
|||
|
|
@ -180,8 +180,8 @@ public class AboutActivity extends BaseActivity {
|
|||
getString(R.string.about_translate_cancel),
|
||||
positiveButtonRunnable,
|
||||
() -> {},
|
||||
spinner,
|
||||
true);
|
||||
spinner
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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<String> defaultExifTagsSet = defaultPrefs.getStringSet(Prefs.MANAGED_EXIF_TAGS);
|
||||
if (null == defaultExifTagsSet) {
|
||||
defaultExifTagsSet = new HashSet<>();
|
||||
}
|
||||
defaultExifTagsSet.add(getString(R.string.exif_tag_location));
|
||||
defaultPrefs.putStringSet(Prefs.MANAGED_EXIF_TAGS, defaultExifTagsSet);
|
||||
}
|
||||
|
||||
// Set DownsampleEnabled to True to downsample the image in case it's heavy
|
||||
ImagePipelineConfig config = ImagePipelineConfig.newBuilder(this)
|
||||
.setNetworkFetcher(customOkHttpNetworkFetcher)
|
||||
.setDownsampleEnabled(true)
|
||||
.build();
|
||||
try {
|
||||
Fresco.initialize(this, config);
|
||||
} catch (Exception e) {
|
||||
Timber.e(e);
|
||||
// TODO: Remove when we're able to initialize Fresco in test builds.
|
||||
}
|
||||
|
||||
createNotificationChannel(this);
|
||||
|
||||
languageLookUpTable = new AppLanguageLookUpTable(this);
|
||||
|
||||
// This handler will catch exceptions thrown from Observables after they are disposed,
|
||||
// or from Observables that are (deliberately or not) missing an onError handler.
|
||||
RxJavaPlugins.setErrorHandler(Functions.emptyConsumer());
|
||||
|
||||
// Fire progress callbacks for every 3% of uploaded content
|
||||
System.setProperty("in.yuvi.http.fluent.PROGRESS_TRIGGER_THRESHOLD", "3.0");
|
||||
}
|
||||
|
||||
/**
|
||||
* Plants debug and file logging tree. Timber lets you plant your own logging trees.
|
||||
*/
|
||||
private void initTimber() {
|
||||
boolean isBeta = ConfigUtils.isBetaFlavour();
|
||||
String logFileName =
|
||||
isBeta ? "CommonsBetaAppLogs" : "CommonsAppLogs";
|
||||
String logDirectory = LogUtils.getLogDirectory();
|
||||
//Delete stale logs if they have exceeded the specified size
|
||||
deleteStaleLogs(logFileName, logDirectory);
|
||||
|
||||
FileLoggingTree tree = new FileLoggingTree(
|
||||
Log.VERBOSE,
|
||||
logFileName,
|
||||
logDirectory,
|
||||
1000,
|
||||
getFileLoggingThreadPool());
|
||||
|
||||
Timber.plant(tree);
|
||||
Timber.plant(new Timber.DebugTree());
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the logs zip file at the specified directory and file locations specified in the
|
||||
* params
|
||||
*
|
||||
* @param logFileName
|
||||
* @param logDirectory
|
||||
*/
|
||||
private void deleteStaleLogs(String logFileName, String logDirectory) {
|
||||
try {
|
||||
File file = new File(logDirectory + "/zip/" + logFileName + ".zip");
|
||||
if (file.exists() && file.getTotalSpace() > 1000000) {// In Kbs
|
||||
file.delete();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Timber.e(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isRoboUnitTest() {
|
||||
return "robolectric".equals(Build.FINGERPRINT);
|
||||
}
|
||||
|
||||
private ThreadPoolService getFileLoggingThreadPool() {
|
||||
return new ThreadPoolService.Builder("file-logging-thread")
|
||||
.setPriority(Process.THREAD_PRIORITY_LOWEST)
|
||||
.setPoolSize(1)
|
||||
.setExceptionHandler(new BackgroundPoolExceptionHandler())
|
||||
.build();
|
||||
}
|
||||
|
||||
public static void createNotificationChannel(@NonNull Context context) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
NotificationManager manager = (NotificationManager) context
|
||||
.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
NotificationChannel channel = manager
|
||||
.getNotificationChannel(NOTIFICATION_CHANNEL_ID_ALL);
|
||||
if (channel == null) {
|
||||
channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID_ALL,
|
||||
context.getString(R.string.notifications_channel_name_all),
|
||||
NotificationManager.IMPORTANCE_DEFAULT);
|
||||
manager.createNotificationChannel(channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String getUserAgent() {
|
||||
return "Commons/" + ConfigUtils.getVersionNameWithSha(this)
|
||||
+ " (https://mediawiki.org/wiki/Apps/Commons) Android/" + Build.VERSION.RELEASE;
|
||||
}
|
||||
|
||||
/**
|
||||
* clears data of current application
|
||||
*
|
||||
* @param context Application context
|
||||
* @param logoutListener Implementation of interface LogoutListener
|
||||
*/
|
||||
@SuppressLint("CheckResult")
|
||||
public void clearApplicationData(Context context, LogoutListener logoutListener) {
|
||||
File cacheDirectory = context.getCacheDir();
|
||||
File applicationDirectory = new File(cacheDirectory.getParent());
|
||||
if (applicationDirectory.exists()) {
|
||||
String[] fileNames = applicationDirectory.list();
|
||||
for (String fileName : fileNames) {
|
||||
if (!fileName.equals("lib")) {
|
||||
FileUtils.deleteFile(new File(applicationDirectory, fileName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sessionManager.logout()
|
||||
.andThen(Completable.fromAction(() -> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
414
app/src/main/java/fr/free/nrw/commons/CommonsApplication.kt
Normal file
414
app/src/main/java/fr/free/nrw/commons/CommonsApplication.kt
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Overlay> overlays = mapView.getOverlays();
|
||||
for (int i = 0; i < overlays.size(); i++) {
|
||||
if (overlays.get(i) instanceof Marker) {
|
||||
Marker item = (Marker) overlays.get(i);
|
||||
if (cameraPosition.getLatitude() == item.getPosition().getLatitude()
|
||||
&& cameraPosition.getLongitude() == item.getPosition().getLongitude()) {
|
||||
mapView.getOverlays().remove(i);
|
||||
mapView.invalidate();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Center the map at user's current location
|
||||
*/
|
||||
private void requestLocationPermissions() {
|
||||
locationPermissionsHelper = new LocationPermissionsHelper(
|
||||
this, locationManager, this);
|
||||
locationPermissionsHelper.requestForLocationAccess(R.string.location_permission_title,
|
||||
R.string.upload_map_location_access);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(final int requestCode,
|
||||
@NonNull final String[] permissions,
|
||||
@NonNull final int[] grantResults) {
|
||||
if (requestCode == Constants.RequestCodes.LOCATION
|
||||
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
onLocationPermissionGranted();
|
||||
} else {
|
||||
onLocationPermissionDenied(getString(R.string.upload_map_location_access));
|
||||
}
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
mapView.onResume();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
mapView.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLocationPermissionDenied(String toastMessage) {
|
||||
if (!ActivityCompat.shouldShowRequestPermissionRationale(this,
|
||||
permission.ACCESS_FINE_LOCATION)) {
|
||||
if (!locationPermissionsHelper.checkLocationPermission(this)) {
|
||||
if (store.getBoolean("isPermissionDenied", false)) {
|
||||
// means user has denied location permission twice or checked the "Don't show again"
|
||||
locationPermissionsHelper.showAppSettingsDialog(this,
|
||||
R.string.upload_map_location_access);
|
||||
} else {
|
||||
Toast.makeText(getBaseContext(), toastMessage, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
store.putBoolean("isPermissionDenied", true);
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(getBaseContext(), toastMessage, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLocationPermissionGranted() {
|
||||
if (moveToCurrentLocation || !(activity.equals("MediaActivity"))) {
|
||||
if (locationPermissionsHelper.isLocationAccessToAppsTurnedOn()) {
|
||||
locationManager.requestLocationUpdatesFromProvider(
|
||||
LocationManager.NETWORK_PROVIDER);
|
||||
locationManager.requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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() {
|
||||
}
|
||||
}
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
package fr.free.nrw.commons.LocationPicker;
|
||||
|
||||
import android.app.Application;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import fr.free.nrw.commons.CameraPosition;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* Observes live camera position data
|
||||
*/
|
||||
public class LocationPickerViewModel extends AndroidViewModel implements Callback<CameraPosition> {
|
||||
|
||||
/**
|
||||
* Wrapping CameraPosition with MutableLiveData
|
||||
*/
|
||||
private final MutableLiveData<CameraPosition> result = new MutableLiveData<>();
|
||||
|
||||
/**
|
||||
* Constructor for this class
|
||||
*
|
||||
* @param application Application
|
||||
*/
|
||||
public LocationPickerViewModel(@NonNull final Application application) {
|
||||
super(application);
|
||||
}
|
||||
|
||||
/**
|
||||
* Responses on camera position changing
|
||||
*
|
||||
* @param call Call<CameraPosition>
|
||||
* @param response Response<CameraPosition>
|
||||
*/
|
||||
@Override
|
||||
public void onResponse(final @NotNull Call<CameraPosition> call,
|
||||
final Response<CameraPosition> response) {
|
||||
if (response.body() == null) {
|
||||
result.setValue(null);
|
||||
return;
|
||||
}
|
||||
result.setValue(response.body());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(final @NotNull Call<CameraPosition> call, final @NotNull Throwable t) {
|
||||
Timber.e(t);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets live CameraPosition
|
||||
*
|
||||
* @return MutableLiveData<CameraPosition>
|
||||
*/
|
||||
public MutableLiveData<CameraPosition> getResult() {
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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<String>? = null
|
||||
// TODO added categories should be removed. It is added for a short fix. On category update,
|
||||
// categories should be re-fetched instead
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
24
app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.kt
Normal file
24
app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.kt
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
package fr.free.nrw.commons.auth
|
||||
|
||||
import android.accounts.Account
|
||||
import android.accounts.AccountManager
|
||||
import android.content.Context
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import fr.free.nrw.commons.BuildConfig.ACCOUNT_TYPE
|
||||
import timber.log.Timber
|
||||
|
||||
const val AUTH_TOKEN_TYPE: String = "CommonsAndroid"
|
||||
|
||||
fun getUserName(context: Context): String? {
|
||||
return account(context)?.name
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun account(context: Context): Account? = try {
|
||||
val accountManager = AccountManager.get(context)
|
||||
val accounts = accountManager.getAccountsByType(ACCOUNT_TYPE)
|
||||
if (accounts.isNotEmpty()) accounts[0] else null
|
||||
} catch (e: SecurityException) {
|
||||
Timber.e(e)
|
||||
null
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
404
app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.kt
Normal file
404
app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.kt
Normal file
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
95
app/src/main/java/fr/free/nrw/commons/auth/SessionManager.kt
Normal file
95
app/src/main/java/fr/free/nrw/commons/auth/SessionManager.kt
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
package fr.free.nrw.commons.auth
|
||||
|
||||
import android.accounts.Account
|
||||
import android.accounts.AccountManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.text.TextUtils
|
||||
import fr.free.nrw.commons.BuildConfig.ACCOUNT_TYPE
|
||||
import fr.free.nrw.commons.auth.login.LoginResult
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.Observable
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Manage the current logged in user session.
|
||||
*/
|
||||
@Singleton
|
||||
class SessionManager @Inject constructor(
|
||||
private val context: Context,
|
||||
@param:Named("default_preferences") private val defaultKvStore: JsonKvStore
|
||||
) {
|
||||
private val accountManager: AccountManager get() = AccountManager.get(context)
|
||||
|
||||
private var _currentAccount: Account? = null // Unlike a savings account... ;-)
|
||||
val currentAccount: Account? get() {
|
||||
if (_currentAccount == null) {
|
||||
val allAccounts = AccountManager.get(context).getAccountsByType(ACCOUNT_TYPE)
|
||||
if (allAccounts.isNotEmpty()) {
|
||||
_currentAccount = allAccounts[0]
|
||||
}
|
||||
}
|
||||
return _currentAccount
|
||||
}
|
||||
|
||||
val userName: String?
|
||||
get() = currentAccount?.name
|
||||
|
||||
var password: String?
|
||||
get() = currentAccount?.let { accountManager.getPassword(it) }
|
||||
private set(value) {
|
||||
currentAccount?.let { accountManager.setPassword(it, value) }
|
||||
}
|
||||
|
||||
val isUserLoggedIn: Boolean
|
||||
get() = defaultKvStore.getBoolean("isUserLoggedIn", false)
|
||||
|
||||
fun updateAccount(result: LoginResult) {
|
||||
if (createAccount(result.userName!!, result.password!!)) {
|
||||
password = result.password
|
||||
}
|
||||
}
|
||||
|
||||
fun doesAccountExist(): Boolean =
|
||||
currentAccount != null
|
||||
|
||||
fun setUserLoggedIn(isLoggedIn: Boolean) =
|
||||
defaultKvStore.putBoolean("isUserLoggedIn", isLoggedIn)
|
||||
|
||||
fun forceLogin(context: Context?) =
|
||||
context?.let { LoginActivity.startYourself(it) }
|
||||
|
||||
fun getPreference(key: String): Boolean =
|
||||
defaultKvStore.getBoolean(key)
|
||||
|
||||
fun logout(): Completable = Completable.fromObservable(
|
||||
Observable.empty<Any>()
|
||||
.doOnComplete {
|
||||
removeAccount()
|
||||
_currentAccount = null
|
||||
}
|
||||
)
|
||||
|
||||
private fun createAccount(userName: String, password: String): Boolean {
|
||||
var account = currentAccount
|
||||
if (account == null || TextUtils.isEmpty(account.name) || account.name != userName) {
|
||||
removeAccount()
|
||||
account = Account(userName, ACCOUNT_TYPE)
|
||||
return accountManager.addAccountExplicitly(account, password, null)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun removeAccount() {
|
||||
currentAccount?.let {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
|
||||
accountManager.removeAccountExplicitly(it)
|
||||
} else {
|
||||
accountManager.removeAccount(it, null, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
75
app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.kt
Normal file
75
app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.kt
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
package fr.free.nrw.commons.auth
|
||||
|
||||
import android.accounts.AbstractAccountAuthenticator
|
||||
import android.accounts.Account
|
||||
import android.accounts.AccountAuthenticatorResponse
|
||||
import android.accounts.AccountManager
|
||||
import android.accounts.NetworkErrorException
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.core.os.bundleOf
|
||||
import fr.free.nrw.commons.BuildConfig
|
||||
|
||||
private val SYNC_AUTHORITIES = arrayOf(
|
||||
BuildConfig.CONTRIBUTION_AUTHORITY, BuildConfig.MODIFICATION_AUTHORITY
|
||||
)
|
||||
|
||||
/**
|
||||
* Handles WikiMedia commons account Authentication
|
||||
*/
|
||||
class WikiAccountAuthenticator(
|
||||
private val context: Context
|
||||
) : AbstractAccountAuthenticator(context) {
|
||||
/**
|
||||
* Provides Bundle with edited Account Properties
|
||||
*/
|
||||
override fun editProperties(
|
||||
response: AccountAuthenticatorResponse,
|
||||
accountType: String
|
||||
) = bundleOf("test" to "editProperties")
|
||||
|
||||
// account type not supported returns bundle without loginActivity Intent, it just contains "test" key
|
||||
@Throws(NetworkErrorException::class)
|
||||
override fun addAccount(
|
||||
response: AccountAuthenticatorResponse,
|
||||
accountType: String,
|
||||
authTokenType: String?,
|
||||
requiredFeatures: Array<String>?,
|
||||
options: Bundle?
|
||||
) = if (BuildConfig.ACCOUNT_TYPE == accountType) {
|
||||
addAccount(response)
|
||||
} else {
|
||||
bundleOf("test" to "addAccount")
|
||||
}
|
||||
|
||||
@Throws(NetworkErrorException::class)
|
||||
override fun confirmCredentials(
|
||||
response: AccountAuthenticatorResponse, account: Account, options: Bundle?
|
||||
) = bundleOf("test" to "confirmCredentials")
|
||||
|
||||
@Throws(NetworkErrorException::class)
|
||||
override fun getAuthToken(
|
||||
response: AccountAuthenticatorResponse,
|
||||
account: Account,
|
||||
authTokenType: String,
|
||||
options: Bundle?
|
||||
) = bundleOf("test" to "getAuthToken")
|
||||
|
||||
override fun getAuthTokenLabel(authTokenType: String) =
|
||||
if (BuildConfig.ACCOUNT_TYPE == authTokenType) AUTH_TOKEN_TYPE else null
|
||||
|
||||
@Throws(NetworkErrorException::class)
|
||||
override fun updateCredentials(
|
||||
response: AccountAuthenticatorResponse,
|
||||
account: Account,
|
||||
authTokenType: String?,
|
||||
options: Bundle?
|
||||
) = bundleOf("test" to "updateCredentials")
|
||||
|
||||
@Throws(NetworkErrorException::class)
|
||||
override fun hasFeatures(
|
||||
response: AccountAuthenticatorResponse,
|
||||
account: Account, features: Array<String>
|
||||
) = bundleOf(AccountManager.KEY_BOOLEAN_RESULT to false)
|
||||
|
||||
/**
|
||||
* Provides a bundle containing a Parcel
|
||||
* the Parcel packs an Intent with LoginActivity and Authenticator response (requires valid account type)
|
||||
*/
|
||||
private fun addAccount(response: AccountAuthenticatorResponse): Bundle {
|
||||
val intent = Intent(context, LoginActivity::class.java)
|
||||
.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response)
|
||||
return bundleOf(AccountManager.KEY_INTENT to intent)
|
||||
}
|
||||
|
||||
@Throws(NetworkErrorException::class)
|
||||
override fun getAccountRemovalAllowed(
|
||||
response: AccountAuthenticatorResponse?,
|
||||
account: Account?
|
||||
): Bundle {
|
||||
val result = super.getAccountRemovalAllowed(response, account)
|
||||
|
||||
if (result.containsKey(AccountManager.KEY_BOOLEAN_RESULT)
|
||||
&& !result.containsKey(AccountManager.KEY_INTENT)
|
||||
) {
|
||||
val allowed = result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT)
|
||||
|
||||
if (allowed) {
|
||||
for (auth in SYNC_AUTHORITIES) {
|
||||
ContentResolver.cancelSync(account, auth)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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. ")
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
|
|
|
|||
|
|
@ -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<Intent> cameraPickLauncherForResult =
|
||||
registerForActivityResult(new StartActivityForResult(),
|
||||
result -> {
|
||||
contributionController.handleActivityResultWithCallback(requireActivity(), callbacks -> {
|
||||
contributionController.onPictureReturnedFromCamera(result, requireActivity(), callbacks);
|
||||
});
|
||||
});
|
||||
|
||||
private final ActivityResultLauncher<Intent> galleryPickLauncherForResult =
|
||||
registerForActivityResult(new StartActivityForResult(),
|
||||
result -> {
|
||||
contributionController.handleActivityResultWithCallback(requireActivity(), callbacks -> {
|
||||
contributionController.onPictureReturnedFromGallery(result, requireActivity(), callbacks);
|
||||
});
|
||||
});
|
||||
|
||||
private ActivityResultLauncher<String[]> inAppCameraLocationPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), new ActivityResultCallback<Map<String, Boolean>>() {
|
||||
@Override
|
||||
public void onActivityResult(Map<String, Boolean> result) {
|
||||
|
|
@ -45,7 +63,7 @@ public class BookmarkLocationsFragment extends DaggerFragment {
|
|||
contributionController.locationPermissionCallback.onLocationPermissionGranted();
|
||||
} else {
|
||||
if (shouldShowRequestPermissionRationale(permission.ACCESS_FINE_LOCATION)) {
|
||||
contributionController.handleShowRationaleFlowCameraLocation(getActivity(), inAppCameraLocationPermissionLauncher);
|
||||
contributionController.handleShowRationaleFlowCameraLocation(getActivity(), inAppCameraLocationPermissionLauncher, cameraPickLauncherForResult);
|
||||
} else {
|
||||
contributionController.locationPermissionCallback.onLocationPermissionDenied(getActivity().getString(R.string.in_app_camera_location_permission_denied));
|
||||
}
|
||||
|
|
@ -83,7 +101,9 @@ public class BookmarkLocationsFragment extends DaggerFragment {
|
|||
return Unit.INSTANCE;
|
||||
},
|
||||
commonPlaceClickActions,
|
||||
inAppCameraLocationPermissionLauncher
|
||||
inAppCameraLocationPermissionLauncher,
|
||||
galleryPickLauncherForResult,
|
||||
cameraPickLauncherForResult
|
||||
);
|
||||
binding.listView.setAdapter(adapter);
|
||||
}
|
||||
|
|
@ -109,11 +129,6 @@ public class BookmarkLocationsFragment extends DaggerFragment {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
contributionController.handleActivityResult(getActivity(), requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package fr.free.nrw.commons.bookmarks.pictures;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
|
|
@ -150,6 +151,7 @@ public class BookmarkPicturesDao {
|
|||
return false;
|
||||
}
|
||||
|
||||
@SuppressLint("Range")
|
||||
@NonNull
|
||||
Bookmark fromCursor(Cursor cursor) {
|
||||
String fileName = cursor.getString(cursor.getColumnIndex(Table.COLUMN_MEDIA_NAME));
|
||||
|
|
|
|||
|
|
@ -1,118 +0,0 @@
|
|||
package fr.free.nrw.commons.campaigns;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import fr.free.nrw.commons.campaigns.models.Campaign;
|
||||
import fr.free.nrw.commons.databinding.LayoutCampaginBinding;
|
||||
import fr.free.nrw.commons.theme.BaseActivity;
|
||||
import fr.free.nrw.commons.utils.DateUtil;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.util.Date;
|
||||
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.Utils;
|
||||
import fr.free.nrw.commons.contributions.MainActivity;
|
||||
import fr.free.nrw.commons.utils.CommonsDateUtil;
|
||||
import fr.free.nrw.commons.utils.SwipableCardView;
|
||||
import fr.free.nrw.commons.utils.ViewUtil;
|
||||
|
||||
/**
|
||||
* A view which represents a single campaign
|
||||
*/
|
||||
public class CampaignView extends SwipableCardView {
|
||||
Campaign campaign;
|
||||
private LayoutCampaginBinding binding;
|
||||
private ViewHolder viewHolder;
|
||||
|
||||
public static final String CAMPAIGNS_DEFAULT_PREFERENCE = "displayCampaignsCardView";
|
||||
public static final String WLM_CARD_PREFERENCE = "displayWLMCardView";
|
||||
|
||||
private String campaignPreference = CAMPAIGNS_DEFAULT_PREFERENCE;
|
||||
|
||||
public CampaignView(@NonNull Context context) {
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public CampaignView(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
public CampaignView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init();
|
||||
}
|
||||
|
||||
public void setCampaign(final Campaign campaign) {
|
||||
this.campaign = campaign;
|
||||
if (campaign != null) {
|
||||
if (campaign.isWLMCampaign()) {
|
||||
campaignPreference = WLM_CARD_PREFERENCE;
|
||||
}
|
||||
setVisibility(View.VISIBLE);
|
||||
viewHolder.init();
|
||||
} else {
|
||||
this.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public boolean onSwipe(final View view) {
|
||||
view.setVisibility(View.GONE);
|
||||
((BaseActivity) getContext()).defaultKvStore
|
||||
.putBoolean(CAMPAIGNS_DEFAULT_PREFERENCE, false);
|
||||
ViewUtil.showLongToast(getContext(),
|
||||
getResources().getString(R.string.nearby_campaign_dismiss_message));
|
||||
return true;
|
||||
}
|
||||
|
||||
private void init() {
|
||||
binding = LayoutCampaginBinding.inflate(LayoutInflater.from(getContext()), this, true);
|
||||
viewHolder = new ViewHolder();
|
||||
setOnClickListener(view -> {
|
||||
if (campaign != null) {
|
||||
if (campaign.isWLMCampaign()) {
|
||||
((MainActivity)(getContext())).showNearby();
|
||||
} else {
|
||||
Utils.handleWebUrl(getContext(), Uri.parse(campaign.getLink()));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public class ViewHolder {
|
||||
public void init() {
|
||||
if (campaign != null) {
|
||||
binding.ivCampaign.setImageDrawable(
|
||||
getResources().getDrawable(R.drawable.ic_campaign));
|
||||
|
||||
binding.tvTitle.setText(campaign.getTitle());
|
||||
binding.tvDescription.setText(campaign.getDescription());
|
||||
try {
|
||||
if (campaign.isWLMCampaign()) {
|
||||
binding.tvDates.setText(
|
||||
String.format("%1s - %2s", campaign.getStartDate(),
|
||||
campaign.getEndDate()));
|
||||
} else {
|
||||
final Date startDate = CommonsDateUtil.getIso8601DateFormatShort()
|
||||
.parse(campaign.getStartDate());
|
||||
final Date endDate = CommonsDateUtil.getIso8601DateFormatShort()
|
||||
.parse(campaign.getEndDate());
|
||||
binding.tvDates.setText(String.format("%1s - %2s", DateUtil.getExtraShortDateString(startDate),
|
||||
DateUtil.getExtraShortDateString(endDate)));
|
||||
}
|
||||
} catch (final ParseException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
121
app/src/main/java/fr/free/nrw/commons/campaigns/CampaignView.kt
Normal file
121
app/src/main/java/fr/free/nrw/commons/campaigns/CampaignView.kt
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
package fr.free.nrw.commons.campaigns
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import fr.free.nrw.commons.R
|
||||
import fr.free.nrw.commons.Utils
|
||||
import fr.free.nrw.commons.campaigns.models.Campaign
|
||||
import fr.free.nrw.commons.contributions.MainActivity
|
||||
import fr.free.nrw.commons.databinding.LayoutCampaginBinding
|
||||
import fr.free.nrw.commons.theme.BaseActivity
|
||||
import fr.free.nrw.commons.utils.CommonsDateUtil.getIso8601DateFormatShort
|
||||
import fr.free.nrw.commons.utils.DateUtil.getExtraShortDateString
|
||||
import fr.free.nrw.commons.utils.SwipableCardView
|
||||
import fr.free.nrw.commons.utils.ViewUtil.showLongToast
|
||||
import timber.log.Timber
|
||||
import java.text.ParseException
|
||||
|
||||
/**
|
||||
* A view which represents a single campaign
|
||||
*/
|
||||
class CampaignView : SwipableCardView {
|
||||
private var campaign: Campaign? = null
|
||||
private var binding: LayoutCampaginBinding? = null
|
||||
private var viewHolder: ViewHolder? = null
|
||||
private var campaignPreference = CAMPAIGNS_DEFAULT_PREFERENCE
|
||||
|
||||
constructor(context: Context) : super(context) {
|
||||
init()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
|
||||
init()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
|
||||
context, attrs, defStyleAttr) {
|
||||
init()
|
||||
}
|
||||
|
||||
fun setCampaign(campaign: Campaign?) {
|
||||
this.campaign = campaign
|
||||
if (campaign != null) {
|
||||
if (campaign.isWLMCampaign) {
|
||||
campaignPreference = WLM_CARD_PREFERENCE
|
||||
}
|
||||
visibility = VISIBLE
|
||||
viewHolder!!.init()
|
||||
} else {
|
||||
visibility = GONE
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSwipe(view: View): Boolean {
|
||||
view.visibility = GONE
|
||||
(context as BaseActivity).defaultKvStore.putBoolean(CAMPAIGNS_DEFAULT_PREFERENCE, false)
|
||||
showLongToast(
|
||||
context,
|
||||
resources.getString(R.string.nearby_campaign_dismiss_message)
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun init() {
|
||||
binding = LayoutCampaginBinding.inflate(
|
||||
LayoutInflater.from(context), this, true
|
||||
)
|
||||
viewHolder = ViewHolder()
|
||||
setOnClickListener {
|
||||
campaign?.let {
|
||||
if (it.isWLMCampaign) {
|
||||
((context) as MainActivity).showNearby()
|
||||
} else {
|
||||
Utils.handleWebUrl(context, Uri.parse(it.link))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class ViewHolder {
|
||||
fun init() {
|
||||
if (campaign != null) {
|
||||
binding!!.ivCampaign.setImageDrawable(
|
||||
ContextCompat.getDrawable(binding!!.root.context, R.drawable.ic_campaign)
|
||||
)
|
||||
binding!!.tvTitle.text = campaign!!.title
|
||||
binding!!.tvDescription.text = campaign!!.description
|
||||
try {
|
||||
if (campaign!!.isWLMCampaign) {
|
||||
binding!!.tvDates.text = String.format(
|
||||
"%1s - %2s", campaign!!.startDate,
|
||||
campaign!!.endDate
|
||||
)
|
||||
} else {
|
||||
val startDate = getIso8601DateFormatShort().parse(
|
||||
campaign?.startDate
|
||||
)
|
||||
val endDate = getIso8601DateFormatShort().parse(
|
||||
campaign?.endDate
|
||||
)
|
||||
binding!!.tvDates.text = String.format(
|
||||
"%1s - %2s", getExtraShortDateString(
|
||||
startDate!!
|
||||
), getExtraShortDateString(endDate!!)
|
||||
)
|
||||
}
|
||||
} catch (e: ParseException) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val CAMPAIGNS_DEFAULT_PREFERENCE: String = "displayCampaignsCardView"
|
||||
const val WLM_CARD_PREFERENCE: String = "displayWLMCardView"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,123 +0,0 @@
|
|||
package fr.free.nrw.commons.campaigns;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
|
||||
import fr.free.nrw.commons.campaigns.models.Campaign;
|
||||
import java.text.ParseException;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import fr.free.nrw.commons.BasePresenter;
|
||||
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
|
||||
import fr.free.nrw.commons.utils.CommonsDateUtil;
|
||||
import io.reactivex.Scheduler;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.SingleObserver;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD;
|
||||
import static fr.free.nrw.commons.di.CommonsApplicationModule.MAIN_THREAD;
|
||||
|
||||
/**
|
||||
* The presenter for the campaigns view, fetches the campaigns from the api and informs the view on
|
||||
* success and error
|
||||
*/
|
||||
@Singleton
|
||||
public class CampaignsPresenter implements BasePresenter<ICampaignsView> {
|
||||
private final OkHttpJsonApiClient okHttpJsonApiClient;
|
||||
private final Scheduler mainThreadScheduler;
|
||||
private final Scheduler ioScheduler;
|
||||
|
||||
private ICampaignsView view;
|
||||
private Disposable disposable;
|
||||
private Campaign campaign;
|
||||
|
||||
@Inject
|
||||
public CampaignsPresenter(OkHttpJsonApiClient okHttpJsonApiClient, @Named(IO_THREAD)Scheduler ioScheduler, @Named(MAIN_THREAD)Scheduler mainThreadScheduler) {
|
||||
this.okHttpJsonApiClient = okHttpJsonApiClient;
|
||||
this.mainThreadScheduler=mainThreadScheduler;
|
||||
this.ioScheduler=ioScheduler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachView(ICampaignsView view) {
|
||||
this.view = view;
|
||||
}
|
||||
|
||||
@Override public void onDetachView() {
|
||||
this.view = null;
|
||||
if (disposable != null) {
|
||||
disposable.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* make the api call to fetch the campaigns
|
||||
*/
|
||||
@SuppressLint("CheckResult")
|
||||
public void getCampaigns() {
|
||||
if (view != null && okHttpJsonApiClient != null) {
|
||||
//If we already have a campaign, lets not make another call
|
||||
if (this.campaign != null) {
|
||||
view.showCampaigns(campaign);
|
||||
return;
|
||||
}
|
||||
Single<CampaignResponseDTO> campaigns = okHttpJsonApiClient.getCampaigns();
|
||||
campaigns.observeOn(mainThreadScheduler)
|
||||
.subscribeOn(ioScheduler)
|
||||
.subscribeWith(new SingleObserver<CampaignResponseDTO>() {
|
||||
|
||||
@Override public void onSubscribe(Disposable d) {
|
||||
disposable = d;
|
||||
}
|
||||
|
||||
@Override public void onSuccess(CampaignResponseDTO campaignResponseDTO) {
|
||||
List<Campaign> campaigns = campaignResponseDTO.getCampaigns();
|
||||
if (campaigns == null || campaigns.isEmpty()) {
|
||||
Timber.e("The campaigns list is empty");
|
||||
view.showCampaigns(null);
|
||||
return;
|
||||
}
|
||||
Collections.sort(campaigns, (campaign, t1) -> {
|
||||
Date date1, date2;
|
||||
try {
|
||||
|
||||
date1 = CommonsDateUtil.getIso8601DateFormatShort().parse(campaign.getStartDate());
|
||||
date2 = CommonsDateUtil.getIso8601DateFormatShort().parse(t1.getStartDate());
|
||||
} catch (ParseException e) {
|
||||
e.printStackTrace();
|
||||
return -1;
|
||||
}
|
||||
return date1.compareTo(date2);
|
||||
});
|
||||
Date campaignEndDate, campaignStartDate;
|
||||
Date currentDate = new Date();
|
||||
try {
|
||||
for (Campaign aCampaign : campaigns) {
|
||||
campaignEndDate = CommonsDateUtil.getIso8601DateFormatShort().parse(aCampaign.getEndDate());
|
||||
campaignStartDate = CommonsDateUtil.getIso8601DateFormatShort().parse(aCampaign.getStartDate());
|
||||
if (campaignEndDate.compareTo(currentDate) >= 0
|
||||
&& campaignStartDate.compareTo(currentDate) <= 0) {
|
||||
campaign = aCampaign;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (ParseException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
view.showCampaigns(campaign);
|
||||
}
|
||||
|
||||
@Override public void onError(Throwable e) {
|
||||
Timber.e(e, "could not fetch campaigns");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
package fr.free.nrw.commons.campaigns
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import fr.free.nrw.commons.BasePresenter
|
||||
import fr.free.nrw.commons.campaigns.models.Campaign
|
||||
import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.IO_THREAD
|
||||
import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.MAIN_THREAD
|
||||
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
|
||||
import fr.free.nrw.commons.utils.CommonsDateUtil.getIso8601DateFormatShort
|
||||
import io.reactivex.Scheduler
|
||||
import io.reactivex.disposables.Disposable
|
||||
import timber.log.Timber
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* The presenter for the campaigns view, fetches the campaigns from the api and informs the view on
|
||||
* success and error
|
||||
*/
|
||||
@Singleton
|
||||
class CampaignsPresenter @Inject constructor(
|
||||
private val okHttpJsonApiClient: OkHttpJsonApiClient?,
|
||||
@param:Named(IO_THREAD) private val ioScheduler: Scheduler,
|
||||
@param:Named(MAIN_THREAD) private val mainThreadScheduler: Scheduler
|
||||
) : BasePresenter<ICampaignsView?> {
|
||||
private var view: ICampaignsView? = null
|
||||
private var disposable: Disposable? = null
|
||||
private var campaign: Campaign? = null
|
||||
|
||||
override fun onAttachView(view: ICampaignsView) {
|
||||
this.view = view
|
||||
}
|
||||
|
||||
override fun onDetachView() {
|
||||
view = null
|
||||
disposable?.dispose()
|
||||
}
|
||||
|
||||
/**
|
||||
* make the api call to fetch the campaigns
|
||||
*/
|
||||
@SuppressLint("CheckResult")
|
||||
fun getCampaigns() {
|
||||
if (view != null && okHttpJsonApiClient != null) {
|
||||
//If we already have a campaign, lets not make another call
|
||||
if (campaign != null) {
|
||||
view!!.showCampaigns(campaign)
|
||||
return
|
||||
}
|
||||
|
||||
okHttpJsonApiClient.getCampaigns()
|
||||
.observeOn(mainThreadScheduler)
|
||||
.subscribeOn(ioScheduler)
|
||||
.doOnSubscribe { disposable = it }
|
||||
.subscribe({ campaignResponseDTO ->
|
||||
val campaigns = campaignResponseDTO?.campaigns?.toMutableList()
|
||||
if (campaigns.isNullOrEmpty()) {
|
||||
Timber.e("The campaigns list is empty")
|
||||
view!!.showCampaigns(null)
|
||||
} else {
|
||||
sortCampaignsByStartDate(campaigns)
|
||||
campaign = findActiveCampaign(campaigns)
|
||||
view!!.showCampaigns(campaign)
|
||||
}
|
||||
}, {
|
||||
Timber.e(it, "could not fetch campaigns")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun sortCampaignsByStartDate(campaigns: MutableList<Campaign>) {
|
||||
val dateFormat: SimpleDateFormat = getIso8601DateFormatShort()
|
||||
campaigns.sortWith(Comparator { campaign: Campaign, other: Campaign ->
|
||||
val date1: Date?
|
||||
val date2: Date?
|
||||
try {
|
||||
date1 = campaign.startDate?.let { dateFormat.parse(it) }
|
||||
date2 = other.startDate?.let { dateFormat.parse(it) }
|
||||
} catch (e: ParseException) {
|
||||
Timber.e(e)
|
||||
return@Comparator -1
|
||||
}
|
||||
if (date1 != null && date2 != null) date1.compareTo(date2) else -1
|
||||
})
|
||||
}
|
||||
|
||||
private fun findActiveCampaign(campaigns: List<Campaign>) : Campaign? {
|
||||
val dateFormat: SimpleDateFormat = getIso8601DateFormatShort()
|
||||
val currentDate = Date()
|
||||
return try {
|
||||
campaigns.firstOrNull {
|
||||
val campaignStartDate = it.startDate?.let { s -> dateFormat.parse(s) }
|
||||
val campaignEndDate = it.endDate?.let { s -> dateFormat.parse(s) }
|
||||
campaignStartDate != null && campaignEndDate != null &&
|
||||
campaignEndDate >= currentDate && campaignStartDate <= currentDate
|
||||
}
|
||||
} catch (e: ParseException) {
|
||||
Timber.e(e, "could not find active campaign")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
package fr.free.nrw.commons.campaigns;
|
||||
|
||||
import fr.free.nrw.commons.MvpView;
|
||||
import fr.free.nrw.commons.campaigns.models.Campaign;
|
||||
|
||||
/**
|
||||
* Interface which defines the view contracts of the campaign view
|
||||
*/
|
||||
public interface ICampaignsView extends MvpView {
|
||||
void showCampaigns(Campaign campaign);
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package fr.free.nrw.commons.campaigns
|
||||
|
||||
import fr.free.nrw.commons.MvpView
|
||||
import fr.free.nrw.commons.campaigns.models.Campaign
|
||||
|
||||
/**
|
||||
* Interface which defines the view contracts of the campaign view
|
||||
*/
|
||||
interface ICampaignsView : MvpView {
|
||||
fun showCampaigns(campaign: Campaign?)
|
||||
}
|
||||
|
|
@ -78,7 +78,13 @@ class CategoriesModel
|
|||
|
||||
// Newly used category...
|
||||
if (category == null) {
|
||||
category = Category(null, item.name, item.description, item.thumbnail, Date(), 0)
|
||||
category = Category(
|
||||
null, item.name,
|
||||
item.description,
|
||||
item.thumbnail,
|
||||
Date(),
|
||||
0
|
||||
)
|
||||
}
|
||||
category.incTimesUsed()
|
||||
categoryDao.save(category)
|
||||
|
|
|
|||
|
|
@ -1,115 +0,0 @@
|
|||
package fr.free.nrw.commons.category;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Represents a category
|
||||
*/
|
||||
public class Category {
|
||||
private Uri contentUri;
|
||||
private String name;
|
||||
private String description;
|
||||
private String thumbnail;
|
||||
private Date lastUsed;
|
||||
private int timesUsed;
|
||||
|
||||
public Category() {
|
||||
}
|
||||
|
||||
public Category(Uri contentUri, String name, String description, String thumbnail, Date lastUsed, int timesUsed) {
|
||||
this.contentUri = contentUri;
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
this.thumbnail = thumbnail;
|
||||
this.lastUsed = lastUsed;
|
||||
this.timesUsed = timesUsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets name
|
||||
*
|
||||
* @return name
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies name
|
||||
*
|
||||
* @param name Category name
|
||||
*/
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets last used date
|
||||
*
|
||||
* @return Last used date
|
||||
*/
|
||||
public Date getLastUsed() {
|
||||
// warning: Date objects are mutable.
|
||||
return (Date)lastUsed.clone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates new last used date
|
||||
*/
|
||||
private void touch() {
|
||||
lastUsed = new Date();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets no. of times the category is used
|
||||
*
|
||||
* @return no. of times used
|
||||
*/
|
||||
public int getTimesUsed() {
|
||||
return timesUsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments timesUsed by 1 and sets last used date as now.
|
||||
*/
|
||||
public void incTimesUsed() {
|
||||
timesUsed++;
|
||||
touch();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the content URI for this category
|
||||
*
|
||||
* @return content URI
|
||||
*/
|
||||
public Uri getContentUri() {
|
||||
return contentUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies the content URI - marking this category as already saved in the database
|
||||
*
|
||||
* @param contentUri the content URI
|
||||
*/
|
||||
public void setContentUri(Uri contentUri) {
|
||||
this.contentUri = contentUri;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public String getThumbnail() {
|
||||
return thumbnail;
|
||||
}
|
||||
|
||||
public void setDescription(final String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public void setThumbnail(final String thumbnail) {
|
||||
this.thumbnail = thumbnail;
|
||||
}
|
||||
}
|
||||
17
app/src/main/java/fr/free/nrw/commons/category/Category.kt
Normal file
17
app/src/main/java/fr/free/nrw/commons/category/Category.kt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
package fr.free.nrw.commons.category
|
||||
|
||||
import android.net.Uri
|
||||
import java.util.Date
|
||||
|
||||
data class Category(
|
||||
var contentUri: Uri? = null,
|
||||
val name: String? = null,
|
||||
val description: String? = null,
|
||||
val thumbnail: String? = null,
|
||||
val lastUsed: Date? = null,
|
||||
var timesUsed: Int = 0
|
||||
) {
|
||||
fun incTimesUsed() {
|
||||
timesUsed++
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
package fr.free.nrw.commons.category;
|
||||
|
||||
public interface CategoryClickedListener {
|
||||
void categoryClicked(CategoryItem item);
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package fr.free.nrw.commons.category
|
||||
|
||||
interface CategoryClickedListener {
|
||||
fun categoryClicked(item: CategoryItem)
|
||||
}
|
||||
|
|
@ -124,7 +124,9 @@ class CategoryClient
|
|||
}.map {
|
||||
it
|
||||
.filter { page ->
|
||||
page.categoryInfo() == null || !page.categoryInfo().isHidden
|
||||
// Null check is not redundant because some values could be null
|
||||
// for mocks when running unit tests
|
||||
page.categoryInfo()?.isHidden != true
|
||||
}.map {
|
||||
CategoryItem(
|
||||
it.title().replace(CATEGORY_PREFIX, ""),
|
||||
|
|
|
|||
|
|
@ -1,169 +0,0 @@
|
|||
package fr.free.nrw.commons.category;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.UriMatcher;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteQueryBuilder;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import fr.free.nrw.commons.BuildConfig;
|
||||
import fr.free.nrw.commons.data.DBOpenHelper;
|
||||
import fr.free.nrw.commons.di.CommonsDaggerContentProvider;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static android.content.UriMatcher.NO_MATCH;
|
||||
import static fr.free.nrw.commons.category.CategoryDao.Table.ALL_FIELDS;
|
||||
import static fr.free.nrw.commons.category.CategoryDao.Table.COLUMN_ID;
|
||||
import static fr.free.nrw.commons.category.CategoryDao.Table.TABLE_NAME;
|
||||
|
||||
public class CategoryContentProvider extends CommonsDaggerContentProvider {
|
||||
|
||||
// For URI matcher
|
||||
private static final int CATEGORIES = 1;
|
||||
private static final int CATEGORIES_ID = 2;
|
||||
private static final String BASE_PATH = "categories";
|
||||
|
||||
public static final Uri BASE_URI = Uri.parse("content://" + BuildConfig.CATEGORY_AUTHORITY + "/" + BASE_PATH);
|
||||
|
||||
private static final UriMatcher uriMatcher = new UriMatcher(NO_MATCH);
|
||||
|
||||
static {
|
||||
uriMatcher.addURI(BuildConfig.CATEGORY_AUTHORITY, BASE_PATH, CATEGORIES);
|
||||
uriMatcher.addURI(BuildConfig.CATEGORY_AUTHORITY, BASE_PATH + "/#", CATEGORIES_ID);
|
||||
}
|
||||
|
||||
public static Uri uriForId(int id) {
|
||||
return Uri.parse(BASE_URI.toString() + "/" + id);
|
||||
}
|
||||
|
||||
@Inject DBOpenHelper dbOpenHelper;
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
@Override
|
||||
public Cursor query(@NonNull Uri uri, String[] projection, String selection,
|
||||
String[] selectionArgs, String sortOrder) {
|
||||
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
|
||||
queryBuilder.setTables(TABLE_NAME);
|
||||
|
||||
int uriType = uriMatcher.match(uri);
|
||||
|
||||
SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
|
||||
Cursor cursor;
|
||||
|
||||
switch (uriType) {
|
||||
case CATEGORIES:
|
||||
cursor = queryBuilder.query(db, projection, selection, selectionArgs,
|
||||
null, null, sortOrder);
|
||||
break;
|
||||
case CATEGORIES_ID:
|
||||
cursor = queryBuilder.query(db,
|
||||
ALL_FIELDS,
|
||||
"_id = ?",
|
||||
new String[]{uri.getLastPathSegment()},
|
||||
null,
|
||||
null,
|
||||
sortOrder
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown URI" + uri);
|
||||
}
|
||||
|
||||
cursor.setNotificationUri(getContext().getContentResolver(), uri);
|
||||
|
||||
return cursor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType(@NonNull Uri uri) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
@Override
|
||||
public Uri insert(@NonNull Uri uri, ContentValues contentValues) {
|
||||
int uriType = uriMatcher.match(uri);
|
||||
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
||||
long id;
|
||||
switch (uriType) {
|
||||
case CATEGORIES:
|
||||
id = sqlDB.insert(TABLE_NAME, null, contentValues);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown URI: " + uri);
|
||||
}
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
return Uri.parse(BASE_URI + "/" + id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(@NonNull Uri uri, String s, String[] strings) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
@Override
|
||||
public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) {
|
||||
Timber.d("Hello, bulk insert! (CategoryContentProvider)");
|
||||
int uriType = uriMatcher.match(uri);
|
||||
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
||||
sqlDB.beginTransaction();
|
||||
switch (uriType) {
|
||||
case CATEGORIES:
|
||||
for (ContentValues value : values) {
|
||||
Timber.d("Inserting! %s", value);
|
||||
sqlDB.insert(TABLE_NAME, null, value);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown URI: " + uri);
|
||||
}
|
||||
sqlDB.setTransactionSuccessful();
|
||||
sqlDB.endTransaction();
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
return values.length;
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
@Override
|
||||
public int update(@NonNull Uri uri, ContentValues contentValues, String selection,
|
||||
String[] selectionArgs) {
|
||||
/*
|
||||
SQL Injection warnings: First, note that we're not exposing this to the
|
||||
outside world (exported="false"). Even then, we should make sure to sanitize
|
||||
all user input appropriately. Input that passes through ContentValues
|
||||
should be fine. So only issues are those that pass in via concating.
|
||||
|
||||
In here, the only concat created argument is for id. It is cast to an int,
|
||||
and will error out otherwise.
|
||||
*/
|
||||
int uriType = uriMatcher.match(uri);
|
||||
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
||||
int rowsUpdated;
|
||||
switch (uriType) {
|
||||
case CATEGORIES_ID:
|
||||
if (TextUtils.isEmpty(selection)) {
|
||||
int id = Integer.valueOf(uri.getLastPathSegment());
|
||||
rowsUpdated = sqlDB.update(TABLE_NAME,
|
||||
contentValues,
|
||||
COLUMN_ID + " = ?",
|
||||
new String[]{String.valueOf(id)});
|
||||
} else {
|
||||
throw new IllegalArgumentException(
|
||||
"Parameter `selection` should be empty when updating an ID");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown URI: " + uri + " with type " + uriType);
|
||||
}
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
return rowsUpdated;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,205 @@
|
|||
package fr.free.nrw.commons.category
|
||||
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.UriMatcher
|
||||
import android.content.UriMatcher.NO_MATCH
|
||||
import android.database.Cursor
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteQueryBuilder
|
||||
import android.net.Uri
|
||||
import android.text.TextUtils
|
||||
import androidx.annotation.NonNull
|
||||
import fr.free.nrw.commons.BuildConfig
|
||||
import fr.free.nrw.commons.data.DBOpenHelper
|
||||
import fr.free.nrw.commons.di.CommonsDaggerContentProvider
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class CategoryContentProvider : CommonsDaggerContentProvider() {
|
||||
|
||||
private val uriMatcher = UriMatcher(NO_MATCH).apply {
|
||||
addURI(BuildConfig.CATEGORY_AUTHORITY, BASE_PATH, CATEGORIES)
|
||||
addURI(BuildConfig.CATEGORY_AUTHORITY, "${BASE_PATH}/#", CATEGORIES_ID)
|
||||
}
|
||||
|
||||
@Inject
|
||||
lateinit var dbOpenHelper: DBOpenHelper
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
override fun query(uri: Uri, projection: Array<String>?, selection: String?,
|
||||
selectionArgs: Array<String>?, sortOrder: String?): Cursor? {
|
||||
val queryBuilder = SQLiteQueryBuilder().apply {
|
||||
tables = TABLE_NAME
|
||||
}
|
||||
|
||||
val uriType = uriMatcher.match(uri)
|
||||
val db = dbOpenHelper.readableDatabase
|
||||
|
||||
val cursor: Cursor? = when (uriType) {
|
||||
CATEGORIES -> queryBuilder.query(
|
||||
db,
|
||||
projection,
|
||||
selection,
|
||||
selectionArgs,
|
||||
null,
|
||||
null,
|
||||
sortOrder
|
||||
)
|
||||
CATEGORIES_ID -> queryBuilder.query(
|
||||
db,
|
||||
ALL_FIELDS,
|
||||
"_id = ?",
|
||||
arrayOf(uri.lastPathSegment),
|
||||
null,
|
||||
null,
|
||||
sortOrder
|
||||
)
|
||||
else -> throw IllegalArgumentException("Unknown URI $uri")
|
||||
}
|
||||
|
||||
cursor?.setNotificationUri(context?.contentResolver, uri)
|
||||
return cursor
|
||||
}
|
||||
|
||||
override fun getType(uri: Uri): String? {
|
||||
return null
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
override fun insert(uri: Uri, contentValues: ContentValues?): Uri? {
|
||||
val uriType = uriMatcher.match(uri)
|
||||
val sqlDB = dbOpenHelper.writableDatabase
|
||||
val id: Long
|
||||
when (uriType) {
|
||||
CATEGORIES -> {
|
||||
id = sqlDB.insert(TABLE_NAME, null, contentValues)
|
||||
}
|
||||
else -> throw IllegalArgumentException("Unknown URI: $uri")
|
||||
}
|
||||
context?.contentResolver?.notifyChange(uri, null)
|
||||
return Uri.parse("${Companion.BASE_URI}/$id")
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
|
||||
// Not implemented
|
||||
return 0
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
override fun bulkInsert(uri: Uri, values: Array<ContentValues>): Int {
|
||||
Timber.d("Hello, bulk insert! (CategoryContentProvider)")
|
||||
val uriType = uriMatcher.match(uri)
|
||||
val sqlDB = dbOpenHelper.writableDatabase
|
||||
sqlDB.beginTransaction()
|
||||
when (uriType) {
|
||||
CATEGORIES -> {
|
||||
for (value in values) {
|
||||
Timber.d("Inserting! %s", value)
|
||||
sqlDB.insert(TABLE_NAME, null, value)
|
||||
}
|
||||
sqlDB.setTransactionSuccessful()
|
||||
}
|
||||
else -> throw IllegalArgumentException("Unknown URI: $uri")
|
||||
}
|
||||
sqlDB.endTransaction()
|
||||
context?.contentResolver?.notifyChange(uri, null)
|
||||
return values.size
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
override fun update(uri: Uri, contentValues: ContentValues?, selection: String?,
|
||||
selectionArgs: Array<String>?): Int {
|
||||
val uriType = uriMatcher.match(uri)
|
||||
val sqlDB = dbOpenHelper.writableDatabase
|
||||
val rowsUpdated: Int
|
||||
when (uriType) {
|
||||
CATEGORIES_ID -> {
|
||||
if (TextUtils.isEmpty(selection)) {
|
||||
val id = uri.lastPathSegment?.toInt()
|
||||
?: throw IllegalArgumentException("Invalid ID")
|
||||
rowsUpdated = sqlDB.update(TABLE_NAME,
|
||||
contentValues,
|
||||
"$COLUMN_ID = ?",
|
||||
arrayOf(id.toString()))
|
||||
} else {
|
||||
throw IllegalArgumentException(
|
||||
"Parameter `selection` should be empty when updating an ID")
|
||||
}
|
||||
}
|
||||
else -> throw IllegalArgumentException("Unknown URI: $uri with type $uriType")
|
||||
}
|
||||
context?.contentResolver?.notifyChange(uri, null)
|
||||
return rowsUpdated
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TABLE_NAME = "categories"
|
||||
|
||||
const val COLUMN_ID = "_id"
|
||||
const val COLUMN_NAME = "name"
|
||||
const val COLUMN_DESCRIPTION = "description"
|
||||
const val COLUMN_THUMBNAIL = "thumbnail"
|
||||
const val COLUMN_LAST_USED = "last_used"
|
||||
const val COLUMN_TIMES_USED = "times_used"
|
||||
|
||||
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
|
||||
val ALL_FIELDS = arrayOf(
|
||||
COLUMN_ID,
|
||||
COLUMN_NAME,
|
||||
COLUMN_DESCRIPTION,
|
||||
COLUMN_THUMBNAIL,
|
||||
COLUMN_LAST_USED,
|
||||
COLUMN_TIMES_USED
|
||||
)
|
||||
|
||||
const val DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS $TABLE_NAME"
|
||||
|
||||
const val CREATE_TABLE_STATEMENT = "CREATE TABLE $TABLE_NAME (" +
|
||||
"$COLUMN_ID INTEGER PRIMARY KEY," +
|
||||
"$COLUMN_NAME TEXT," +
|
||||
"$COLUMN_DESCRIPTION TEXT," +
|
||||
"$COLUMN_THUMBNAIL TEXT," +
|
||||
"$COLUMN_LAST_USED INTEGER," +
|
||||
"$COLUMN_TIMES_USED INTEGER" +
|
||||
");"
|
||||
|
||||
fun uriForId(id: Int): Uri {
|
||||
return Uri.parse("${BASE_URI}/$id")
|
||||
}
|
||||
|
||||
fun onCreate(db: SQLiteDatabase) {
|
||||
db.execSQL(CREATE_TABLE_STATEMENT)
|
||||
}
|
||||
|
||||
fun onDelete(db: SQLiteDatabase) {
|
||||
db.execSQL(DROP_TABLE_STATEMENT)
|
||||
onCreate(db)
|
||||
}
|
||||
|
||||
fun onUpdate(db: SQLiteDatabase, from: Int, to: Int) {
|
||||
if (from == to) return
|
||||
if (from < 4) {
|
||||
// doesn't exist yet
|
||||
onUpdate(db, from + 1, to)
|
||||
} else if (from == 4) {
|
||||
// table added in version 5
|
||||
onCreate(db)
|
||||
onUpdate(db, from + 1, to)
|
||||
} else if (from == 5) {
|
||||
onUpdate(db, from + 1, to)
|
||||
} else if (from == 17) {
|
||||
db.execSQL("ALTER TABLE $TABLE_NAME ADD COLUMN description TEXT;")
|
||||
db.execSQL("ALTER TABLE $TABLE_NAME ADD COLUMN thumbnail TEXT;")
|
||||
onUpdate(db, from + 1, to)
|
||||
}
|
||||
}
|
||||
|
||||
// For URI matcher
|
||||
private const val CATEGORIES = 1
|
||||
private const val CATEGORIES_ID = 2
|
||||
private const val BASE_PATH = "categories"
|
||||
val BASE_URI: Uri = Uri.parse("content://${BuildConfig.CATEGORY_AUTHORITY}/${Companion.BASE_PATH}")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,207 +0,0 @@
|
|||
package fr.free.nrw.commons.category;
|
||||
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.os.RemoteException;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Provider;
|
||||
|
||||
public class CategoryDao {
|
||||
|
||||
private final Provider<ContentProviderClient> clientProvider;
|
||||
|
||||
@Inject
|
||||
public CategoryDao(@Named("category") Provider<ContentProviderClient> clientProvider) {
|
||||
this.clientProvider = clientProvider;
|
||||
}
|
||||
|
||||
public void save(Category category) {
|
||||
ContentProviderClient db = clientProvider.get();
|
||||
try {
|
||||
if (category.getContentUri() == null) {
|
||||
category.setContentUri(db.insert(CategoryContentProvider.BASE_URI, toContentValues(category)));
|
||||
} else {
|
||||
db.update(category.getContentUri(), toContentValues(category), null, null);
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
db.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find persisted category in database, based on its name.
|
||||
*
|
||||
* @param name Category's name
|
||||
* @return category from database, or null if not found
|
||||
*/
|
||||
@Nullable
|
||||
Category find(String name) {
|
||||
Cursor cursor = null;
|
||||
ContentProviderClient db = clientProvider.get();
|
||||
try {
|
||||
cursor = db.query(
|
||||
CategoryContentProvider.BASE_URI,
|
||||
Table.ALL_FIELDS,
|
||||
Table.COLUMN_NAME + "=?",
|
||||
new String[]{name},
|
||||
null);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return fromCursor(cursor);
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
// This feels lazy, but to hell with checked exceptions. :)
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
db.release();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve recently-used categories, ordered by descending date.
|
||||
*
|
||||
* @return a list containing recent categories
|
||||
*/
|
||||
@NonNull
|
||||
List<CategoryItem> recentCategories(int limit) {
|
||||
List<CategoryItem> items = new ArrayList<>();
|
||||
Cursor cursor = null;
|
||||
ContentProviderClient db = clientProvider.get();
|
||||
try {
|
||||
cursor = db.query(
|
||||
CategoryContentProvider.BASE_URI,
|
||||
Table.ALL_FIELDS,
|
||||
null,
|
||||
new String[]{},
|
||||
Table.COLUMN_LAST_USED + " DESC");
|
||||
// fixme add a limit on the original query instead of falling out of the loop?
|
||||
while (cursor != null && cursor.moveToNext()
|
||||
&& cursor.getPosition() < limit) {
|
||||
if (fromCursor(cursor).getName() != null ) {
|
||||
items.add(new CategoryItem(fromCursor(cursor).getName(),
|
||||
fromCursor(cursor).getDescription(), fromCursor(cursor).getThumbnail(),
|
||||
false));
|
||||
}
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
db.release();
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
Category fromCursor(Cursor cursor) {
|
||||
// Hardcoding column positions!
|
||||
return new Category(
|
||||
CategoryContentProvider.uriForId(cursor.getInt(cursor.getColumnIndex(Table.COLUMN_ID))),
|
||||
cursor.getString(cursor.getColumnIndex(Table.COLUMN_NAME)),
|
||||
cursor.getString(cursor.getColumnIndex(Table.COLUMN_DESCRIPTION)),
|
||||
cursor.getString(cursor.getColumnIndex(Table.COLUMN_THUMBNAIL)),
|
||||
new Date(cursor.getLong(cursor.getColumnIndex(Table.COLUMN_LAST_USED))),
|
||||
cursor.getInt(cursor.getColumnIndex(Table.COLUMN_TIMES_USED))
|
||||
);
|
||||
}
|
||||
|
||||
private ContentValues toContentValues(Category category) {
|
||||
ContentValues cv = new ContentValues();
|
||||
cv.put(CategoryDao.Table.COLUMN_NAME, category.getName());
|
||||
cv.put(Table.COLUMN_DESCRIPTION, category.getDescription());
|
||||
cv.put(Table.COLUMN_THUMBNAIL, category.getThumbnail());
|
||||
cv.put(CategoryDao.Table.COLUMN_LAST_USED, category.getLastUsed().getTime());
|
||||
cv.put(CategoryDao.Table.COLUMN_TIMES_USED, category.getTimesUsed());
|
||||
return cv;
|
||||
}
|
||||
|
||||
public static class Table {
|
||||
public static final String TABLE_NAME = "categories";
|
||||
|
||||
public static final String COLUMN_ID = "_id";
|
||||
static final String COLUMN_NAME = "name";
|
||||
static final String COLUMN_DESCRIPTION = "description";
|
||||
static final String COLUMN_THUMBNAIL = "thumbnail";
|
||||
static final String COLUMN_LAST_USED = "last_used";
|
||||
static final String COLUMN_TIMES_USED = "times_used";
|
||||
|
||||
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
|
||||
public static final String[] ALL_FIELDS = {
|
||||
COLUMN_ID,
|
||||
COLUMN_NAME,
|
||||
COLUMN_DESCRIPTION,
|
||||
COLUMN_THUMBNAIL,
|
||||
COLUMN_LAST_USED,
|
||||
COLUMN_TIMES_USED
|
||||
};
|
||||
|
||||
static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME;
|
||||
|
||||
static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
|
||||
+ COLUMN_ID + " INTEGER PRIMARY KEY,"
|
||||
+ COLUMN_NAME + " STRING,"
|
||||
+ COLUMN_DESCRIPTION + " STRING,"
|
||||
+ COLUMN_THUMBNAIL + " STRING,"
|
||||
+ COLUMN_LAST_USED + " INTEGER,"
|
||||
+ COLUMN_TIMES_USED + " INTEGER"
|
||||
+ ");";
|
||||
|
||||
public static void onCreate(SQLiteDatabase db) {
|
||||
db.execSQL(CREATE_TABLE_STATEMENT);
|
||||
}
|
||||
|
||||
public static void onDelete(SQLiteDatabase db) {
|
||||
db.execSQL(DROP_TABLE_STATEMENT);
|
||||
onCreate(db);
|
||||
}
|
||||
|
||||
public static void onUpdate(SQLiteDatabase db, int from, int to) {
|
||||
if (from == to) {
|
||||
return;
|
||||
}
|
||||
if (from < 4) {
|
||||
// doesn't exist yet
|
||||
from++;
|
||||
onUpdate(db, from, to);
|
||||
return;
|
||||
}
|
||||
if (from == 4) {
|
||||
// table added in version 5
|
||||
onCreate(db);
|
||||
from++;
|
||||
onUpdate(db, from, to);
|
||||
return;
|
||||
}
|
||||
if (from == 5) {
|
||||
from++;
|
||||
onUpdate(db, from, to);
|
||||
return;
|
||||
}
|
||||
if (from == 17) {
|
||||
db.execSQL("ALTER TABLE categories ADD COLUMN description STRING;");
|
||||
db.execSQL("ALTER TABLE categories ADD COLUMN thumbnail STRING;");
|
||||
from++;
|
||||
onUpdate(db, from, to);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
194
app/src/main/java/fr/free/nrw/commons/category/CategoryDao.kt
Normal file
194
app/src/main/java/fr/free/nrw/commons/category/CategoryDao.kt
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
package fr.free.nrw.commons.category
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ContentProviderClient
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.os.RemoteException
|
||||
|
||||
import java.util.ArrayList
|
||||
import java.util.Date
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
import javax.inject.Provider
|
||||
|
||||
class CategoryDao @Inject constructor(
|
||||
@Named("category") private val clientProvider: Provider<ContentProviderClient>
|
||||
) {
|
||||
|
||||
fun save(category: Category) {
|
||||
val db = clientProvider.get()
|
||||
try {
|
||||
if (category.contentUri == null) {
|
||||
category.contentUri = db.insert(
|
||||
CategoryContentProvider.BASE_URI,
|
||||
toContentValues(category)
|
||||
)
|
||||
} else {
|
||||
db.update(
|
||||
category.contentUri!!,
|
||||
toContentValues(category),
|
||||
null,
|
||||
null
|
||||
)
|
||||
}
|
||||
} catch (e: RemoteException) {
|
||||
throw RuntimeException(e)
|
||||
} finally {
|
||||
db.release()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find persisted category in database, based on its name.
|
||||
*
|
||||
* @param name Category's name
|
||||
* @return category from database, or null if not found
|
||||
*/
|
||||
fun find(name: String): Category? {
|
||||
var cursor: Cursor? = null
|
||||
val db = clientProvider.get()
|
||||
try {
|
||||
cursor = db.query(
|
||||
CategoryContentProvider.BASE_URI,
|
||||
ALL_FIELDS,
|
||||
"${COLUMN_NAME}=?",
|
||||
arrayOf(name),
|
||||
null
|
||||
)
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return fromCursor(cursor)
|
||||
}
|
||||
} catch (e: RemoteException) {
|
||||
throw RuntimeException(e)
|
||||
} finally {
|
||||
cursor?.close()
|
||||
db.release()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve recently-used categories, ordered by descending date.
|
||||
*
|
||||
* @return a list containing recent categories
|
||||
*/
|
||||
fun recentCategories(limit: Int): List<CategoryItem> {
|
||||
val items = ArrayList<CategoryItem>()
|
||||
var cursor: Cursor? = null
|
||||
val db = clientProvider.get()
|
||||
try {
|
||||
cursor = db.query(
|
||||
CategoryContentProvider.BASE_URI,
|
||||
ALL_FIELDS,
|
||||
null,
|
||||
emptyArray(),
|
||||
"$COLUMN_LAST_USED DESC"
|
||||
)
|
||||
while (cursor != null && cursor.moveToNext() && cursor.position < limit) {
|
||||
val category = fromCursor(cursor)
|
||||
if (category.name != null) {
|
||||
items.add(
|
||||
CategoryItem(
|
||||
category.name,
|
||||
category.description,
|
||||
category.thumbnail,
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (e: RemoteException) {
|
||||
throw RuntimeException(e)
|
||||
} finally {
|
||||
cursor?.close()
|
||||
db.release()
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
@SuppressLint("Range")
|
||||
fun fromCursor(cursor: Cursor): Category {
|
||||
// Hardcoding column positions!
|
||||
return Category(
|
||||
CategoryContentProvider.uriForId(cursor.getInt(cursor.getColumnIndex(COLUMN_ID))),
|
||||
cursor.getString(cursor.getColumnIndex(COLUMN_NAME)),
|
||||
cursor.getString(cursor.getColumnIndex(COLUMN_DESCRIPTION)),
|
||||
cursor.getString(cursor.getColumnIndex(COLUMN_THUMBNAIL)),
|
||||
Date(cursor.getLong(cursor.getColumnIndex(COLUMN_LAST_USED))),
|
||||
cursor.getInt(cursor.getColumnIndex(COLUMN_TIMES_USED))
|
||||
)
|
||||
}
|
||||
|
||||
private fun toContentValues(category: Category): ContentValues {
|
||||
return ContentValues().apply {
|
||||
put(COLUMN_NAME, category.name)
|
||||
put(COLUMN_DESCRIPTION, category.description)
|
||||
put(COLUMN_THUMBNAIL, category.thumbnail)
|
||||
put(COLUMN_LAST_USED, category.lastUsed?.time)
|
||||
put(COLUMN_TIMES_USED, category.timesUsed)
|
||||
}
|
||||
}
|
||||
|
||||
companion object Table {
|
||||
const val TABLE_NAME = "categories"
|
||||
|
||||
const val COLUMN_ID = "_id"
|
||||
const val COLUMN_NAME = "name"
|
||||
const val COLUMN_DESCRIPTION = "description"
|
||||
const val COLUMN_THUMBNAIL = "thumbnail"
|
||||
const val COLUMN_LAST_USED = "last_used"
|
||||
const val COLUMN_TIMES_USED = "times_used"
|
||||
|
||||
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
|
||||
val ALL_FIELDS = arrayOf(
|
||||
COLUMN_ID,
|
||||
COLUMN_NAME,
|
||||
COLUMN_DESCRIPTION,
|
||||
COLUMN_THUMBNAIL,
|
||||
COLUMN_LAST_USED,
|
||||
COLUMN_TIMES_USED
|
||||
)
|
||||
|
||||
const val DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS $TABLE_NAME"
|
||||
|
||||
const val CREATE_TABLE_STATEMENT = "CREATE TABLE $TABLE_NAME (" +
|
||||
"$COLUMN_ID INTEGER PRIMARY KEY," +
|
||||
"$COLUMN_NAME STRING," +
|
||||
"$COLUMN_DESCRIPTION STRING," +
|
||||
"$COLUMN_THUMBNAIL STRING," +
|
||||
"$COLUMN_LAST_USED INTEGER," +
|
||||
"$COLUMN_TIMES_USED INTEGER" +
|
||||
");"
|
||||
|
||||
@SuppressLint("SQLiteString")
|
||||
fun onCreate(db: SQLiteDatabase) {
|
||||
db.execSQL(CREATE_TABLE_STATEMENT)
|
||||
}
|
||||
|
||||
fun onDelete(db: SQLiteDatabase) {
|
||||
db.execSQL(DROP_TABLE_STATEMENT)
|
||||
onCreate(db)
|
||||
}
|
||||
|
||||
@SuppressLint("SQLiteString")
|
||||
fun onUpdate(db: SQLiteDatabase, from: Int, to: Int) {
|
||||
if (from == to) return
|
||||
if (from < 4) {
|
||||
// doesn't exist yet
|
||||
onUpdate(db, from + 1, to)
|
||||
} else if (from == 4) {
|
||||
// table added in version 5
|
||||
onCreate(db)
|
||||
onUpdate(db, from + 1, to)
|
||||
} else if (from == 5) {
|
||||
onUpdate(db, from + 1, to)
|
||||
} else if (from == 17) {
|
||||
db.execSQL("ALTER TABLE $TABLE_NAME ADD COLUMN description STRING;")
|
||||
db.execSQL("ALTER TABLE $TABLE_NAME ADD COLUMN thumbnail STRING;")
|
||||
onUpdate(db, from + 1, to)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,236 +0,0 @@
|
|||
package fr.free.nrw.commons.category;
|
||||
|
||||
import static fr.free.nrw.commons.category.CategoryClientKt.CATEGORY_PREFIX;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.Utils;
|
||||
import fr.free.nrw.commons.ViewPagerAdapter;
|
||||
import fr.free.nrw.commons.databinding.ActivityCategoryDetailsBinding;
|
||||
import fr.free.nrw.commons.explore.categories.media.CategoriesMediaFragment;
|
||||
import fr.free.nrw.commons.explore.categories.parent.ParentCategoriesFragment;
|
||||
import fr.free.nrw.commons.explore.categories.sub.SubCategoriesFragment;
|
||||
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
|
||||
import fr.free.nrw.commons.theme.BaseActivity;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import fr.free.nrw.commons.wikidata.model.page.PageTitle;
|
||||
|
||||
/**
|
||||
* This activity displays details of a particular category
|
||||
* Its generic and simply takes the name of category name in its start intent to load all images, subcategories in
|
||||
* a particular category on wikimedia commons.
|
||||
*/
|
||||
|
||||
public class CategoryDetailsActivity extends BaseActivity
|
||||
implements MediaDetailPagerFragment.MediaDetailProvider, CategoryImagesCallback {
|
||||
|
||||
|
||||
private FragmentManager supportFragmentManager;
|
||||
private CategoriesMediaFragment categoriesMediaFragment;
|
||||
private MediaDetailPagerFragment mediaDetails;
|
||||
private String categoryName;
|
||||
ViewPagerAdapter viewPagerAdapter;
|
||||
|
||||
private ActivityCategoryDetailsBinding binding;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
binding = ActivityCategoryDetailsBinding.inflate(getLayoutInflater());
|
||||
final View view = binding.getRoot();
|
||||
setContentView(view);
|
||||
supportFragmentManager = getSupportFragmentManager();
|
||||
viewPagerAdapter = new ViewPagerAdapter(getSupportFragmentManager());
|
||||
binding.viewPager.setAdapter(viewPagerAdapter);
|
||||
binding.viewPager.setOffscreenPageLimit(2);
|
||||
binding.tabLayout.setupWithViewPager(binding.viewPager);
|
||||
setSupportActionBar(binding.toolbarBinding.toolbar);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
setTabs();
|
||||
setPageTitle();
|
||||
}
|
||||
|
||||
/**
|
||||
* This activity contains 3 tabs and a viewpager. This method is used to set the titles of tab,
|
||||
* Set the fragments according to the tab selected in the viewPager.
|
||||
*/
|
||||
private void setTabs() {
|
||||
List<Fragment> fragmentList = new ArrayList<>();
|
||||
List<String> titleList = new ArrayList<>();
|
||||
categoriesMediaFragment = new CategoriesMediaFragment();
|
||||
SubCategoriesFragment subCategoryListFragment = new SubCategoriesFragment();
|
||||
ParentCategoriesFragment parentCategoriesFragment = new ParentCategoriesFragment();
|
||||
categoryName = getIntent().getStringExtra("categoryName");
|
||||
if (getIntent() != null && categoryName != null) {
|
||||
Bundle arguments = new Bundle();
|
||||
arguments.putString("categoryName", categoryName);
|
||||
categoriesMediaFragment.setArguments(arguments);
|
||||
subCategoryListFragment.setArguments(arguments);
|
||||
parentCategoriesFragment.setArguments(arguments);
|
||||
}
|
||||
fragmentList.add(categoriesMediaFragment);
|
||||
titleList.add("MEDIA");
|
||||
fragmentList.add(subCategoryListFragment);
|
||||
titleList.add("SUBCATEGORIES");
|
||||
fragmentList.add(parentCategoriesFragment);
|
||||
titleList.add("PARENT CATEGORIES");
|
||||
viewPagerAdapter.setTabData(fragmentList, titleList);
|
||||
viewPagerAdapter.notifyDataSetChanged();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the passed categoryName from the intents and displays it as the page title
|
||||
*/
|
||||
private void setPageTitle() {
|
||||
if (getIntent() != null && getIntent().getStringExtra("categoryName") != null) {
|
||||
setTitle(getIntent().getStringExtra("categoryName"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called onClick of media inside category details (CategoryImageListFragment).
|
||||
*/
|
||||
@Override
|
||||
public void onMediaClicked(int position) {
|
||||
binding.tabLayout.setVisibility(View.GONE);
|
||||
binding.viewPager.setVisibility(View.GONE);
|
||||
binding.mediaContainer.setVisibility(View.VISIBLE);
|
||||
if (mediaDetails == null || !mediaDetails.isVisible()) {
|
||||
// set isFeaturedImage true for featured images, to include author field on media detail
|
||||
mediaDetails = MediaDetailPagerFragment.newInstance(false, true);
|
||||
FragmentManager supportFragmentManager = getSupportFragmentManager();
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.mediaContainer, mediaDetails)
|
||||
.addToBackStack(null)
|
||||
.commit();
|
||||
supportFragmentManager.executePendingTransactions();
|
||||
}
|
||||
mediaDetails.showImage(position);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Consumers should be simply using this method to use this activity.
|
||||
* @param context A Context of the application package implementing this class.
|
||||
* @param categoryName Name of the category for displaying its details
|
||||
*/
|
||||
public static void startYourself(Context context, String categoryName) {
|
||||
Intent intent = new Intent(context, CategoryDetailsActivity.class);
|
||||
intent.putExtra("categoryName", categoryName);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called mediaDetailPagerFragment. It returns the Media Object at that Index
|
||||
* @param i It is the index of which media object is to be returned which is same as
|
||||
* current index of viewPager.
|
||||
* @return Media Object
|
||||
*/
|
||||
@Override
|
||||
public Media getMediaAtPosition(int i) {
|
||||
return categoriesMediaFragment.getMediaAtPosition(i);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called on from getCount of MediaDetailPagerFragment
|
||||
* The viewpager will contain same number of media items as that of media elements in adapter.
|
||||
* @return Total Media count in the adapter
|
||||
*/
|
||||
@Override
|
||||
public int getTotalMediaCount() {
|
||||
return categoriesMediaFragment.getTotalMediaCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getContributionStateAt(int position) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload media detail fragment once media is nominated
|
||||
*
|
||||
* @param index item position that has been nominated
|
||||
*/
|
||||
@Override
|
||||
public void refreshNominatedMedia(int index) {
|
||||
if (getSupportFragmentManager().getBackStackEntryCount() == 1) {
|
||||
onBackPressed();
|
||||
onMediaClicked(index);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method inflates the menu in the toolbar
|
||||
*/
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
MenuInflater inflater = getMenuInflater();
|
||||
inflater.inflate(R.menu.fragment_category_detail, menu);
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method handles the logic on ItemSelect in toolbar menu
|
||||
* Currently only 1 choice is available to open category details page in browser
|
||||
*/
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
|
||||
// Handle item selection
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_browser_current_category:
|
||||
PageTitle title = Utils.getPageTitle(CATEGORY_PREFIX + categoryName);
|
||||
Utils.handleWebUrl(this, Uri.parse(title.getCanonicalUri()));
|
||||
return true;
|
||||
case android.R.id.home:
|
||||
onBackPressed();
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called on backPressed of anyFragment in the activity.
|
||||
* If condition is called when mediaDetailFragment is opened.
|
||||
*/
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (supportFragmentManager.getBackStackEntryCount() == 1){
|
||||
binding.tabLayout.setVisibility(View.VISIBLE);
|
||||
binding.viewPager.setVisibility(View.VISIBLE);
|
||||
binding.mediaContainer.setVisibility(View.GONE);
|
||||
}
|
||||
super.onBackPressed();
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called on success of API call for Images inside a category.
|
||||
* The viewpager will notified that number of items have changed.
|
||||
*/
|
||||
@Override
|
||||
public void viewPagerNotifyDataSetChanged() {
|
||||
if (mediaDetails!=null){
|
||||
mediaDetails.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,216 @@
|
|||
package fr.free.nrw.commons.category
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import fr.free.nrw.commons.Media
|
||||
import fr.free.nrw.commons.R
|
||||
import fr.free.nrw.commons.Utils
|
||||
import fr.free.nrw.commons.ViewPagerAdapter
|
||||
import fr.free.nrw.commons.databinding.ActivityCategoryDetailsBinding
|
||||
import fr.free.nrw.commons.explore.categories.media.CategoriesMediaFragment
|
||||
import fr.free.nrw.commons.explore.categories.parent.ParentCategoriesFragment
|
||||
import fr.free.nrw.commons.explore.categories.sub.SubCategoriesFragment
|
||||
import fr.free.nrw.commons.media.MediaDetailPagerFragment
|
||||
import fr.free.nrw.commons.theme.BaseActivity
|
||||
|
||||
|
||||
/**
|
||||
* This activity displays details of a particular category
|
||||
* Its generic and simply takes the name of category name in its start intent to load all images, subcategories in
|
||||
* a particular category on wikimedia commons.
|
||||
*/
|
||||
class CategoryDetailsActivity : BaseActivity(),
|
||||
MediaDetailPagerFragment.MediaDetailProvider,
|
||||
CategoryImagesCallback {
|
||||
|
||||
private lateinit var supportFragmentManager: FragmentManager
|
||||
private lateinit var categoriesMediaFragment: CategoriesMediaFragment
|
||||
private var mediaDetails: MediaDetailPagerFragment? = null
|
||||
private var categoryName: String? = null
|
||||
private lateinit var viewPagerAdapter: ViewPagerAdapter
|
||||
|
||||
private lateinit var binding: ActivityCategoryDetailsBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding = ActivityCategoryDetailsBinding.inflate(layoutInflater)
|
||||
val view = binding.root
|
||||
setContentView(view)
|
||||
supportFragmentManager = getSupportFragmentManager()
|
||||
viewPagerAdapter = ViewPagerAdapter(supportFragmentManager)
|
||||
binding.viewPager.adapter = viewPagerAdapter
|
||||
binding.viewPager.offscreenPageLimit = 2
|
||||
binding.tabLayout.setupWithViewPager(binding.viewPager)
|
||||
setSupportActionBar(binding.toolbarBinding.toolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
setTabs()
|
||||
setPageTitle()
|
||||
}
|
||||
|
||||
/**
|
||||
* This activity contains 3 tabs and a viewpager. This method is used to set the titles of tab,
|
||||
* Set the fragments according to the tab selected in the viewPager.
|
||||
*/
|
||||
private fun setTabs() {
|
||||
val fragmentList = mutableListOf<Fragment>()
|
||||
val titleList = mutableListOf<String>()
|
||||
categoriesMediaFragment = CategoriesMediaFragment()
|
||||
val subCategoryListFragment = SubCategoriesFragment()
|
||||
val parentCategoriesFragment = ParentCategoriesFragment()
|
||||
categoryName = intent?.getStringExtra("categoryName")
|
||||
if (intent != null && categoryName != null) {
|
||||
val arguments = Bundle().apply {
|
||||
putString("categoryName", categoryName)
|
||||
}
|
||||
categoriesMediaFragment.arguments = arguments
|
||||
subCategoryListFragment.arguments = arguments
|
||||
parentCategoriesFragment.arguments = arguments
|
||||
}
|
||||
fragmentList.add(categoriesMediaFragment)
|
||||
titleList.add("MEDIA")
|
||||
fragmentList.add(subCategoryListFragment)
|
||||
titleList.add("SUBCATEGORIES")
|
||||
fragmentList.add(parentCategoriesFragment)
|
||||
titleList.add("PARENT CATEGORIES")
|
||||
viewPagerAdapter.setTabData(fragmentList, titleList)
|
||||
viewPagerAdapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the passed categoryName from the intents and displays it as the page title
|
||||
*/
|
||||
private fun setPageTitle() {
|
||||
intent?.getStringExtra("categoryName")?.let {
|
||||
title = it
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called onClick of media inside category details (CategoryImageListFragment).
|
||||
*/
|
||||
override fun onMediaClicked(position: Int) {
|
||||
binding.tabLayout.visibility = View.GONE
|
||||
binding.viewPager.visibility = View.GONE
|
||||
binding.mediaContainer.visibility = View.VISIBLE
|
||||
if (mediaDetails == null || mediaDetails?.isVisible == false) {
|
||||
// set isFeaturedImage true for featured images, to include author field on media detail
|
||||
mediaDetails = MediaDetailPagerFragment.newInstance(false, true)
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.mediaContainer, mediaDetails!!)
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
supportFragmentManager.executePendingTransactions()
|
||||
}
|
||||
mediaDetails?.showImage(position)
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Consumers should be simply using this method to use this activity.
|
||||
* @param context A Context of the application package implementing this class.
|
||||
* @param categoryName Name of the category for displaying its details
|
||||
*/
|
||||
fun startYourself(context: Context?, categoryName: String) {
|
||||
val intent = Intent(context, CategoryDetailsActivity::class.java).apply {
|
||||
putExtra("categoryName", categoryName)
|
||||
}
|
||||
context?.startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called mediaDetailPagerFragment. It returns the Media Object at that Index
|
||||
* @param i It is the index of which media object is to be returned which is same as
|
||||
* current index of viewPager.
|
||||
* @return Media Object
|
||||
*/
|
||||
override fun getMediaAtPosition(i: Int): Media? {
|
||||
return categoriesMediaFragment.getMediaAtPosition(i)
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called on from getCount of MediaDetailPagerFragment
|
||||
* The viewpager will contain same number of media items as that of media elements in adapter.
|
||||
* @return Total Media count in the adapter
|
||||
*/
|
||||
override fun getTotalMediaCount(): Int {
|
||||
return categoriesMediaFragment.getTotalMediaCount()
|
||||
}
|
||||
|
||||
override fun getContributionStateAt(position: Int): Int? {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload media detail fragment once media is nominated
|
||||
*
|
||||
* @param index item position that has been nominated
|
||||
*/
|
||||
override fun refreshNominatedMedia(index: Int) {
|
||||
if (supportFragmentManager.backStackEntryCount == 1) {
|
||||
onBackPressed()
|
||||
onMediaClicked(index)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method inflates the menu in the toolbar
|
||||
*/
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
menuInflater.inflate(R.menu.fragment_category_detail, menu)
|
||||
return super.onCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
/**
|
||||
* This method handles the logic on ItemSelect in toolbar menu
|
||||
* Currently only 1 choice is available to open category details page in browser
|
||||
*/
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.menu_browser_current_category -> {
|
||||
val title = Utils.getPageTitle(CATEGORY_PREFIX + categoryName)
|
||||
Utils.handleWebUrl(this, Uri.parse(title.canonicalUri))
|
||||
true
|
||||
}
|
||||
android.R.id.home -> {
|
||||
onBackPressed()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called on backPressed of anyFragment in the activity.
|
||||
* If condition is called when mediaDetailFragment is opened.
|
||||
*/
|
||||
@Deprecated("This method has been deprecated in favor of using the" +
|
||||
"{@link OnBackPressedDispatcher} via {@link #getOnBackPressedDispatcher()}." +
|
||||
"The OnBackPressedDispatcher controls how back button events are dispatched" +
|
||||
"to one or more {@link OnBackPressedCallback} objects.")
|
||||
override fun onBackPressed() {
|
||||
if (supportFragmentManager.backStackEntryCount == 1) {
|
||||
binding.tabLayout.visibility = View.VISIBLE
|
||||
binding.viewPager.visibility = View.VISIBLE
|
||||
binding.mediaContainer.visibility = View.GONE
|
||||
}
|
||||
super.onBackPressed()
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called on success of API call for Images inside a category.
|
||||
* The viewpager will notified that number of items have changed.
|
||||
*/
|
||||
override fun viewPagerNotifyDataSetChanged() {
|
||||
mediaDetails?.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,123 +0,0 @@
|
|||
package fr.free.nrw.commons.category;
|
||||
|
||||
import static fr.free.nrw.commons.notification.NotificationHelper.NOTIFICATION_EDIT_CATEGORY;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import fr.free.nrw.commons.BuildConfig;
|
||||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.actions.PageEditClient;
|
||||
import fr.free.nrw.commons.notification.NotificationHelper;
|
||||
import fr.free.nrw.commons.utils.ViewUtilWrapper;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.Single;
|
||||
import java.util.List;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class CategoryEditHelper {
|
||||
private final NotificationHelper notificationHelper;
|
||||
public final PageEditClient pageEditClient;
|
||||
private final ViewUtilWrapper viewUtil;
|
||||
private final String username;
|
||||
|
||||
@Inject
|
||||
public CategoryEditHelper(NotificationHelper notificationHelper,
|
||||
@Named("commons-page-edit") PageEditClient pageEditClient,
|
||||
ViewUtilWrapper viewUtil,
|
||||
@Named("username") String username) {
|
||||
this.notificationHelper = notificationHelper;
|
||||
this.pageEditClient = pageEditClient;
|
||||
this.viewUtil = viewUtil;
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
/**
|
||||
* Public interface to edit categories
|
||||
* @param context
|
||||
* @param media
|
||||
* @param categories
|
||||
* @return
|
||||
*/
|
||||
public Single<Boolean> makeCategoryEdit(Context context, Media media, List<String> categories,
|
||||
final String wikiText) {
|
||||
viewUtil.showShortToast(context, context.getString(R.string.category_edit_helper_make_edit_toast));
|
||||
return addCategory(media, categories, wikiText)
|
||||
.flatMapSingle(result -> Single.just(showCategoryEditNotification(context, media, result)))
|
||||
.firstOrError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuilds the WikiText with new categpries and post it on server
|
||||
*
|
||||
* @param media
|
||||
* @param categories to be added
|
||||
* @return
|
||||
*/
|
||||
private Observable<Boolean> addCategory(Media media, List<String> categories,
|
||||
final String wikiText) {
|
||||
Timber.d("thread is category adding %s", Thread.currentThread().getName());
|
||||
String summary = "Adding categories";
|
||||
final StringBuilder buffer = new StringBuilder();
|
||||
final String wikiTextWithoutCategory;
|
||||
//If the picture was uploaded without a category, the wikitext will contain "Uncategorized" instead of "[[Category"
|
||||
if (wikiText.contains("Uncategorized")) {
|
||||
wikiTextWithoutCategory = wikiText.substring(0, wikiText.indexOf("Uncategorized"));
|
||||
} else if (wikiText.contains("[[Category")) {
|
||||
wikiTextWithoutCategory = wikiText.substring(0, wikiText.indexOf("[[Category"));
|
||||
} else {
|
||||
wikiTextWithoutCategory = "";
|
||||
}
|
||||
if (categories != null && !categories.isEmpty()) {
|
||||
//If the categories list is empty, when reading the categories of a picture,
|
||||
// the code will add "None selected" to categories list in order to see in picture's categories with "None selected".
|
||||
// So that after selected some category,"None selected" should be removed from list
|
||||
for (int i = 0; i < categories.size(); i++) {
|
||||
if (!categories.get(i).equals("None selected")//Not to add "None selected" as category to wikiText
|
||||
|| !wikiText.contains("Uncategorized")) {
|
||||
buffer.append("[[Category:").append(categories.get(i)).append("]]\n");
|
||||
}
|
||||
}
|
||||
categories.remove("None selected");
|
||||
} else {
|
||||
buffer.append("{{subst:unc}}");
|
||||
}
|
||||
final String appendText = wikiTextWithoutCategory + buffer;
|
||||
return pageEditClient.edit(media.getFilename(), appendText + "\n", summary);
|
||||
}
|
||||
|
||||
private boolean showCategoryEditNotification(Context context, Media media, boolean result) {
|
||||
String message;
|
||||
String title = context.getString(R.string.category_edit_helper_show_edit_title);
|
||||
|
||||
if (result) {
|
||||
title += ": " + context.getString(R.string.category_edit_helper_show_edit_title_success);
|
||||
StringBuilder categoriesInMessage = new StringBuilder();
|
||||
List<String> mediaCategoryList = media.getCategories();
|
||||
for (String category : mediaCategoryList) {
|
||||
categoriesInMessage.append(category);
|
||||
if (category.equals(mediaCategoryList.get(mediaCategoryList.size()-1))) {
|
||||
continue;
|
||||
}
|
||||
categoriesInMessage.append(",");
|
||||
}
|
||||
|
||||
message = context.getResources().getQuantityString(R.plurals.category_edit_helper_show_edit_message_if, mediaCategoryList.size(), categoriesInMessage.toString());
|
||||
} else {
|
||||
title += ": " + context.getString(R.string.category_edit_helper_show_edit_title);
|
||||
message = context.getString(R.string.category_edit_helper_edit_message_else) ;
|
||||
}
|
||||
|
||||
String urlForFile = BuildConfig.COMMONS_URL + "/wiki/" + media.getFilename();
|
||||
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(urlForFile));
|
||||
notificationHelper.showNotification(context, title, message, NOTIFICATION_EDIT_CATEGORY, browserIntent);
|
||||
return result;
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
boolean updateCategoryDisplay(List<String> categories);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
package fr.free.nrw.commons.category
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import fr.free.nrw.commons.BuildConfig
|
||||
import fr.free.nrw.commons.Media
|
||||
import fr.free.nrw.commons.R
|
||||
import fr.free.nrw.commons.actions.PageEditClient
|
||||
import fr.free.nrw.commons.notification.NotificationHelper
|
||||
import fr.free.nrw.commons.utils.ViewUtilWrapper
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
import timber.log.Timber
|
||||
|
||||
|
||||
class CategoryEditHelper @Inject constructor(
|
||||
private val notificationHelper: NotificationHelper,
|
||||
@Named("commons-page-edit") val pageEditClient: PageEditClient,
|
||||
private val viewUtil: ViewUtilWrapper,
|
||||
@Named("username") private val username: String
|
||||
) {
|
||||
|
||||
/**
|
||||
* Public interface to edit categories
|
||||
* @param context
|
||||
* @param media
|
||||
* @param categories
|
||||
* @return
|
||||
*/
|
||||
fun makeCategoryEdit(
|
||||
context: Context,
|
||||
media: Media,
|
||||
categories: List<String>,
|
||||
wikiText: String
|
||||
): Single<Boolean> {
|
||||
viewUtil.showShortToast(
|
||||
context,
|
||||
context.getString(R.string.category_edit_helper_make_edit_toast)
|
||||
)
|
||||
return addCategory(media, categories, wikiText)
|
||||
.flatMapSingle { result ->
|
||||
Single.just(showCategoryEditNotification(context, media, result))
|
||||
}
|
||||
.firstOrError()
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuilds the WikiText with new categories and post it on server
|
||||
*
|
||||
* @param media
|
||||
* @param categories to be added
|
||||
* @return
|
||||
*/
|
||||
private fun addCategory(
|
||||
media: Media,
|
||||
categories: List<String>?,
|
||||
wikiText: String
|
||||
): Observable<Boolean> {
|
||||
Timber.d("thread is category adding %s", Thread.currentThread().name)
|
||||
val summary = "Adding categories"
|
||||
val buffer = StringBuilder()
|
||||
|
||||
// If the picture was uploaded without a category, the wikitext will contain "Uncategorized" instead of "[[Category"
|
||||
val wikiTextWithoutCategory: String = when {
|
||||
wikiText.contains("Uncategorized") -> wikiText.substring(0, wikiText.indexOf("Uncategorized"))
|
||||
wikiText.contains("[[Category") -> wikiText.substring(0, wikiText.indexOf("[[Category"))
|
||||
else -> ""
|
||||
}
|
||||
|
||||
if (!categories.isNullOrEmpty()) {
|
||||
// If the categories list is empty, when reading the categories of a picture,
|
||||
// the code will add "None selected" to categories list in order to see in picture's categories with "None selected".
|
||||
// So that after selecting some category, "None selected" should be removed from list
|
||||
for (category in categories) {
|
||||
if (category != "None selected" || !wikiText.contains("Uncategorized")) {
|
||||
buffer.append("[[Category:").append(category).append("]]\n")
|
||||
}
|
||||
}
|
||||
categories.dropWhile {
|
||||
it == "None selected"
|
||||
}
|
||||
} else {
|
||||
buffer.append("{{subst:unc}}")
|
||||
}
|
||||
|
||||
val appendText = wikiTextWithoutCategory + buffer
|
||||
return pageEditClient.edit(media.filename!!, "$appendText\n", summary)
|
||||
}
|
||||
|
||||
private fun showCategoryEditNotification(
|
||||
context: Context,
|
||||
media: Media,
|
||||
result: Boolean
|
||||
): Boolean {
|
||||
val title: String
|
||||
val message: String
|
||||
|
||||
if (result) {
|
||||
title = context.getString(R.string.category_edit_helper_show_edit_title) + ": " +
|
||||
context.getString(R.string.category_edit_helper_show_edit_title_success)
|
||||
|
||||
val categoriesInMessage = StringBuilder()
|
||||
val mediaCategoryList = media.categories
|
||||
for ((index, category) in mediaCategoryList?.withIndex()!!) {
|
||||
categoriesInMessage.append(category)
|
||||
if (index != mediaCategoryList.size - 1) {
|
||||
categoriesInMessage.append(",")
|
||||
}
|
||||
}
|
||||
|
||||
message = context.resources.getQuantityString(
|
||||
R.plurals.category_edit_helper_show_edit_message_if,
|
||||
mediaCategoryList.size,
|
||||
categoriesInMessage.toString()
|
||||
)
|
||||
} else {
|
||||
title = context.getString(R.string.category_edit_helper_show_edit_title) + ": " +
|
||||
context.getString(R.string.category_edit_helper_show_edit_title)
|
||||
message = context.getString(R.string.category_edit_helper_edit_message_else)
|
||||
}
|
||||
|
||||
val urlForFile = "${BuildConfig.COMMONS_URL}/wiki/${media.filename}"
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(urlForFile))
|
||||
notificationHelper.showNotification(
|
||||
context,
|
||||
title,
|
||||
message,
|
||||
NOTIFICATION_EDIT_CATEGORY,
|
||||
browserIntent
|
||||
)
|
||||
return result
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
fun updateCategoryDisplay(categories: List<String>?): Boolean
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val NOTIFICATION_EDIT_CATEGORY = 1
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
package fr.free.nrw.commons.category;
|
||||
|
||||
/**
|
||||
* Callback for notifying the viewpager that the number of items have changed
|
||||
* and for requesting more images when the viewpager has been scrolled to its end.
|
||||
*/
|
||||
|
||||
public interface CategoryImagesCallback {
|
||||
void viewPagerNotifyDataSetChanged();
|
||||
void onMediaClicked(int position);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package fr.free.nrw.commons.category
|
||||
|
||||
interface CategoryImagesCallback {
|
||||
fun viewPagerNotifyDataSetChanged()
|
||||
|
||||
fun onMediaClicked(position: Int)
|
||||
}
|
||||
|
|
@ -1,119 +0,0 @@
|
|||
package fr.free.nrw.commons.category;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.facebook.drawee.view.SimpleDraweeView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.R;
|
||||
|
||||
/**
|
||||
* This is created to only display UI implementation. Needs to be changed in real implementation
|
||||
*/
|
||||
|
||||
public class GridViewAdapter extends ArrayAdapter {
|
||||
private List<Media> data;
|
||||
|
||||
public GridViewAdapter(Context context, int layoutResourceId, List<Media> data) {
|
||||
super(context, layoutResourceId, data);
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds more item to the list
|
||||
* Its triggered on scrolling down in the list
|
||||
* @param images
|
||||
*/
|
||||
public void addItems(List<Media> images) {
|
||||
if (data == null) {
|
||||
data = new ArrayList<>();
|
||||
}
|
||||
data.addAll(images);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the first item in the new list with old list and returns true if they are same
|
||||
* Its triggered on successful response of the fetch images API.
|
||||
* @param images
|
||||
*/
|
||||
public boolean containsAll(List<Media> images){
|
||||
if (images == null || images.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
if (data == null) {
|
||||
data = new ArrayList<>();
|
||||
return false;
|
||||
}
|
||||
if (data.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
String fileName = data.get(0).getFilename();
|
||||
String imageName = images.get(0).getFilename();
|
||||
return imageName.equals(fileName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return data == null || data.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the UI for the category image item
|
||||
* @param position
|
||||
* @param convertView
|
||||
* @param parent
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
|
||||
if (convertView == null) {
|
||||
convertView = LayoutInflater.from(getContext()).inflate(R.layout.layout_category_images, null);
|
||||
}
|
||||
|
||||
Media item = data.get(position);
|
||||
SimpleDraweeView imageView = convertView.findViewById(R.id.categoryImageView);
|
||||
TextView fileName = convertView.findViewById(R.id.categoryImageTitle);
|
||||
TextView uploader = convertView.findViewById(R.id.categoryImageAuthor);
|
||||
fileName.setText(item.getMostRelevantCaption());
|
||||
setUploaderView(item, uploader);
|
||||
imageView.setImageURI(item.getThumbUrl());
|
||||
return convertView;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the Media item at the given position
|
||||
*/
|
||||
@Nullable
|
||||
@Override
|
||||
public Media getItem(int position) {
|
||||
return data.get(position);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Shows author information if its present
|
||||
* @param item
|
||||
* @param uploader
|
||||
*/
|
||||
private void setUploaderView(Media item, TextView uploader) {
|
||||
if (!TextUtils.isEmpty(item.getAuthor())) {
|
||||
uploader.setVisibility(View.VISIBLE);
|
||||
uploader.setText(getContext().getString(R.string.image_uploaded_by, item.getUser()));
|
||||
} else {
|
||||
uploader.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
package fr.free.nrw.commons.category
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.TextView
|
||||
import com.facebook.drawee.view.SimpleDraweeView
|
||||
import fr.free.nrw.commons.Media
|
||||
import fr.free.nrw.commons.R
|
||||
|
||||
|
||||
/**
|
||||
* This is created to only display UI implementation. Needs to be changed in real implementation
|
||||
*/
|
||||
class GridViewAdapter(
|
||||
context: Context,
|
||||
layoutResourceId: Int,
|
||||
private var data: MutableList<Media>?
|
||||
) : ArrayAdapter<Media>(context, layoutResourceId, data ?: mutableListOf()) {
|
||||
|
||||
/**
|
||||
* Adds more items to the list
|
||||
* It's triggered on scrolling down in the list
|
||||
* @param images
|
||||
*/
|
||||
fun addItems(images: List<Media>) {
|
||||
if (data == null) {
|
||||
data = mutableListOf()
|
||||
}
|
||||
data?.addAll(images)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the first item in the new list with the old list and returns true if they are the same
|
||||
* It's triggered on a successful response of the fetch images API.
|
||||
* @param images
|
||||
*/
|
||||
fun containsAll(images: List<Media>?): Boolean {
|
||||
if (images.isNullOrEmpty()) {
|
||||
return false
|
||||
}
|
||||
if (data.isNullOrEmpty()) {
|
||||
data = mutableListOf()
|
||||
return false
|
||||
}
|
||||
val fileName = data?.get(0)?.filename
|
||||
val imageName = images[0].filename
|
||||
return imageName == fileName
|
||||
}
|
||||
|
||||
override fun isEmpty(): Boolean {
|
||||
return data.isNullOrEmpty()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the UI for the category image item
|
||||
* @param position
|
||||
* @param convertView
|
||||
* @param parent
|
||||
* @return
|
||||
*/
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
val view = convertView ?: LayoutInflater.from(context).inflate(
|
||||
R.layout.layout_category_images,
|
||||
parent,
|
||||
false
|
||||
)
|
||||
|
||||
val item = data?.get(position)
|
||||
val imageView = view.findViewById<SimpleDraweeView>(R.id.categoryImageView)
|
||||
val fileName = view.findViewById<TextView>(R.id.categoryImageTitle)
|
||||
val uploader = view.findViewById<TextView>(R.id.categoryImageAuthor)
|
||||
|
||||
item?.let {
|
||||
fileName.text = it.mostRelevantCaption
|
||||
setUploaderView(it, uploader)
|
||||
imageView.setImageURI(it.thumbUrl)
|
||||
}
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the Media item at the given position
|
||||
*/
|
||||
override fun getItem(position: Int): Media? {
|
||||
return data?.get(position)
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows author information if it's present
|
||||
* @param item
|
||||
* @param uploader
|
||||
*/
|
||||
@SuppressLint("StringFormatInvalid")
|
||||
private fun setUploaderView(item: Media, uploader: TextView) {
|
||||
if (!item.author.isNullOrEmpty()) {
|
||||
uploader.visibility = View.VISIBLE
|
||||
uploader.text = context.getString(
|
||||
R.string.image_uploaded_by,
|
||||
item.user
|
||||
)
|
||||
} else {
|
||||
uploader.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
package fr.free.nrw.commons.category;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface OnCategoriesSaveHandler {
|
||||
void onCategoriesSave(List<String> categories);
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package fr.free.nrw.commons.category
|
||||
|
||||
interface OnCategoriesSaveHandler {
|
||||
fun onCategoriesSave(categories: List<String>)
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package fr.free.nrw.commons.contributions
|
|||
import androidx.paging.PagedList.BoundaryCallback
|
||||
import fr.free.nrw.commons.auth.SessionManager
|
||||
import fr.free.nrw.commons.di.CommonsApplicationModule
|
||||
import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.IO_THREAD
|
||||
import fr.free.nrw.commons.media.MediaClient
|
||||
import io.reactivex.Scheduler
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
|
|
@ -20,7 +21,7 @@ class ContributionBoundaryCallback
|
|||
private val repository: ContributionsRepository,
|
||||
private val sessionManager: SessionManager,
|
||||
private val mediaClient: MediaClient,
|
||||
@param:Named(CommonsApplicationModule.IO_THREAD) private val ioThreadScheduler: Scheduler,
|
||||
@param:Named(IO_THREAD) private val ioThreadScheduler: Scheduler,
|
||||
) : BoundaryCallback<Contribution>() {
|
||||
private val compositeDisposable: CompositeDisposable = CompositeDisposable()
|
||||
var userName: String? = null
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import android.app.Activity;
|
|||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.widget.Toast;
|
||||
import androidx.activity.result.ActivityResult;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LiveData;
|
||||
|
|
@ -45,7 +46,8 @@ public class ContributionController {
|
|||
private boolean isInAppCameraUpload;
|
||||
public LocationPermissionCallback locationPermissionCallback;
|
||||
private LocationPermissionsHelper locationPermissionsHelper;
|
||||
LiveData<PagedList<Contribution>> failedAndPendingContributionList;
|
||||
// Temporarily disabled, see issue [https://github.com/commons-app/apps-android-commons/issues/5847]
|
||||
// LiveData<PagedList<Contribution>> failedAndPendingContributionList;
|
||||
LiveData<PagedList<Contribution>> pendingContributionList;
|
||||
LiveData<PagedList<Contribution>> failedContributionList;
|
||||
|
||||
|
|
@ -64,10 +66,11 @@ public class ContributionController {
|
|||
* Check for permissions and initiate camera click
|
||||
*/
|
||||
public void initiateCameraPick(Activity activity,
|
||||
ActivityResultLauncher<String[]> inAppCameraLocationPermissionLauncher) {
|
||||
ActivityResultLauncher<String[]> inAppCameraLocationPermissionLauncher,
|
||||
ActivityResultLauncher<Intent> resultLauncher) {
|
||||
boolean useExtStorage = defaultKvStore.getBoolean("useExternalStorage", true);
|
||||
if (!useExtStorage) {
|
||||
initiateCameraUpload(activity);
|
||||
initiateCameraUpload(activity, resultLauncher);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -75,17 +78,17 @@ public class ContributionController {
|
|||
() -> {
|
||||
if (defaultKvStore.getBoolean("inAppCameraFirstRun")) {
|
||||
defaultKvStore.putBoolean("inAppCameraFirstRun", false);
|
||||
askUserToAllowLocationAccess(activity, inAppCameraLocationPermissionLauncher);
|
||||
askUserToAllowLocationAccess(activity, inAppCameraLocationPermissionLauncher, resultLauncher);
|
||||
} else if (defaultKvStore.getBoolean("inAppCameraLocationPref")) {
|
||||
createDialogsAndHandleLocationPermissions(activity,
|
||||
inAppCameraLocationPermissionLauncher);
|
||||
inAppCameraLocationPermissionLauncher, resultLauncher);
|
||||
} else {
|
||||
initiateCameraUpload(activity);
|
||||
initiateCameraUpload(activity, resultLauncher);
|
||||
}
|
||||
},
|
||||
R.string.storage_permission_title,
|
||||
R.string.write_storage_permission_rationale,
|
||||
PermissionUtils.PERMISSIONS_STORAGE);
|
||||
PermissionUtils.getPERMISSIONS_STORAGE());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -94,7 +97,8 @@ public class ContributionController {
|
|||
* @param activity
|
||||
*/
|
||||
private void createDialogsAndHandleLocationPermissions(Activity activity,
|
||||
ActivityResultLauncher<String[]> inAppCameraLocationPermissionLauncher) {
|
||||
ActivityResultLauncher<String[]> inAppCameraLocationPermissionLauncher,
|
||||
ActivityResultLauncher<Intent> resultLauncher) {
|
||||
locationPermissionCallback = new LocationPermissionCallback() {
|
||||
@Override
|
||||
public void onLocationPermissionDenied(String toastMessage) {
|
||||
|
|
@ -103,16 +107,16 @@ public class ContributionController {
|
|||
toastMessage,
|
||||
Toast.LENGTH_LONG
|
||||
).show();
|
||||
initiateCameraUpload(activity);
|
||||
initiateCameraUpload(activity, resultLauncher);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLocationPermissionGranted() {
|
||||
if (!locationPermissionsHelper.isLocationAccessToAppsTurnedOn()) {
|
||||
showLocationOffDialog(activity, R.string.in_app_camera_needs_location,
|
||||
R.string.in_app_camera_location_unavailable);
|
||||
R.string.in_app_camera_location_unavailable, resultLauncher);
|
||||
} else {
|
||||
initiateCameraUpload(activity);
|
||||
initiateCameraUpload(activity, resultLauncher);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -135,9 +139,10 @@ public class ContributionController {
|
|||
* @param activity Activity reference
|
||||
* @param dialogTextResource Resource id of text to be shown in dialog
|
||||
* @param toastTextResource Resource id of text to be shown in toast
|
||||
* @param resultLauncher
|
||||
*/
|
||||
private void showLocationOffDialog(Activity activity, int dialogTextResource,
|
||||
int toastTextResource) {
|
||||
int toastTextResource, ActivityResultLauncher<Intent> resultLauncher) {
|
||||
DialogUtil
|
||||
.showAlertDialog(activity,
|
||||
activity.getString(R.string.ask_to_turn_location_on),
|
||||
|
|
@ -148,25 +153,26 @@ public class ContributionController {
|
|||
() -> {
|
||||
Toast.makeText(activity, activity.getString(toastTextResource),
|
||||
Toast.LENGTH_LONG).show();
|
||||
initiateCameraUpload(activity);
|
||||
initiateCameraUpload(activity, resultLauncher);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public void handleShowRationaleFlowCameraLocation(Activity activity,
|
||||
ActivityResultLauncher<String[]> inAppCameraLocationPermissionLauncher) {
|
||||
ActivityResultLauncher<String[]> inAppCameraLocationPermissionLauncher,
|
||||
ActivityResultLauncher<Intent> resultLauncher) {
|
||||
DialogUtil.showAlertDialog(activity, activity.getString(R.string.location_permission_title),
|
||||
activity.getString(R.string.in_app_camera_location_permission_rationale),
|
||||
activity.getString(android.R.string.ok),
|
||||
activity.getString(android.R.string.cancel),
|
||||
() -> {
|
||||
createDialogsAndHandleLocationPermissions(activity,
|
||||
inAppCameraLocationPermissionLauncher);
|
||||
inAppCameraLocationPermissionLauncher, resultLauncher);
|
||||
},
|
||||
() -> locationPermissionCallback.onLocationPermissionDenied(
|
||||
activity.getString(R.string.in_app_camera_location_permission_denied)),
|
||||
null,
|
||||
false);
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -181,7 +187,8 @@ public class ContributionController {
|
|||
* @param activity
|
||||
*/
|
||||
private void askUserToAllowLocationAccess(Activity activity,
|
||||
ActivityResultLauncher<String[]> inAppCameraLocationPermissionLauncher) {
|
||||
ActivityResultLauncher<String[]> inAppCameraLocationPermissionLauncher,
|
||||
ActivityResultLauncher<Intent> resultLauncher) {
|
||||
DialogUtil.showAlertDialog(activity,
|
||||
activity.getString(R.string.in_app_camera_location_permission_title),
|
||||
activity.getString(R.string.in_app_camera_location_access_explanation),
|
||||
|
|
@ -190,47 +197,45 @@ public class ContributionController {
|
|||
() -> {
|
||||
defaultKvStore.putBoolean("inAppCameraLocationPref", true);
|
||||
createDialogsAndHandleLocationPermissions(activity,
|
||||
inAppCameraLocationPermissionLauncher);
|
||||
inAppCameraLocationPermissionLauncher, resultLauncher);
|
||||
},
|
||||
() -> {
|
||||
ViewUtil.showLongToast(activity, R.string.in_app_camera_location_permission_denied);
|
||||
defaultKvStore.putBoolean("inAppCameraLocationPref", false);
|
||||
initiateCameraUpload(activity);
|
||||
initiateCameraUpload(activity, resultLauncher);
|
||||
},
|
||||
null,
|
||||
true);
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate gallery picker
|
||||
*/
|
||||
public void initiateGalleryPick(final Activity activity, final boolean allowMultipleUploads) {
|
||||
initiateGalleryUpload(activity, allowMultipleUploads);
|
||||
public void initiateGalleryPick(final Activity activity, ActivityResultLauncher<Intent> resultLauncher, final boolean allowMultipleUploads) {
|
||||
initiateGalleryUpload(activity, resultLauncher, allowMultipleUploads);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate gallery picker with permission
|
||||
*/
|
||||
public void initiateCustomGalleryPickWithPermission(final Activity activity) {
|
||||
public void initiateCustomGalleryPickWithPermission(final Activity activity, ActivityResultLauncher<Intent> resultLauncher) {
|
||||
setPickerConfiguration(activity, true);
|
||||
|
||||
PermissionUtils.checkPermissionsAndPerformAction(activity,
|
||||
() -> FilePicker.openCustomSelector(activity, 0),
|
||||
() -> FilePicker.openCustomSelector(activity, resultLauncher, 0),
|
||||
R.string.storage_permission_title,
|
||||
R.string.write_storage_permission_rationale,
|
||||
PermissionUtils.PERMISSIONS_STORAGE);
|
||||
PermissionUtils.getPERMISSIONS_STORAGE());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Open chooser for gallery uploads
|
||||
*/
|
||||
private void initiateGalleryUpload(final Activity activity,
|
||||
private void initiateGalleryUpload(final Activity activity, ActivityResultLauncher<Intent> resultLauncher,
|
||||
final boolean allowMultipleUploads) {
|
||||
setPickerConfiguration(activity, allowMultipleUploads);
|
||||
boolean openDocumentIntentPreferred = defaultKvStore.getBoolean(
|
||||
"openDocumentPhotoPickerPref", true);
|
||||
FilePicker.openGallery(activity, 0, openDocumentIntentPreferred);
|
||||
FilePicker.openGallery(activity, resultLauncher, 0, isDocumentPhotoPickerPreferred());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -247,22 +252,43 @@ public class ContributionController {
|
|||
/**
|
||||
* Initiate camera upload by opening camera
|
||||
*/
|
||||
private void initiateCameraUpload(Activity activity) {
|
||||
private void initiateCameraUpload(Activity activity, ActivityResultLauncher<Intent> resultLauncher) {
|
||||
setPickerConfiguration(activity, false);
|
||||
if (defaultKvStore.getBoolean("inAppCameraLocationPref", false)) {
|
||||
locationBeforeImageCapture = locationManager.getLastLocation();
|
||||
}
|
||||
isInAppCameraUpload = true;
|
||||
FilePicker.openCameraForImage(activity, 0);
|
||||
FilePicker.openCameraForImage(activity, resultLauncher, 0);
|
||||
}
|
||||
|
||||
private boolean isDocumentPhotoPickerPreferred(){
|
||||
return defaultKvStore.getBoolean(
|
||||
"openDocumentPhotoPickerPref", true);
|
||||
}
|
||||
|
||||
public void onPictureReturnedFromGallery(ActivityResult result, Activity activity, FilePicker.Callbacks callbacks){
|
||||
|
||||
if(isDocumentPhotoPickerPreferred()){
|
||||
FilePicker.onPictureReturnedFromDocuments(result, activity, callbacks);
|
||||
} else {
|
||||
FilePicker.onPictureReturnedFromGallery(result, activity, callbacks);
|
||||
}
|
||||
}
|
||||
|
||||
public void onPictureReturnedFromCustomSelector(ActivityResult result, Activity activity, @NonNull FilePicker.Callbacks callbacks) {
|
||||
FilePicker.onPictureReturnedFromCustomSelector(result, activity, callbacks);
|
||||
}
|
||||
|
||||
public void onPictureReturnedFromCamera(ActivityResult result, Activity activity, @NonNull FilePicker.Callbacks callbacks) {
|
||||
FilePicker.onPictureReturnedFromCamera(result, activity, callbacks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches callback for file picker.
|
||||
*/
|
||||
public void handleActivityResult(Activity activity, int requestCode, int resultCode,
|
||||
Intent data) {
|
||||
FilePicker.handleActivityResult(requestCode, resultCode, data, activity,
|
||||
new DefaultCallback() {
|
||||
public void handleActivityResultWithCallback(Activity activity, FilePicker.HandleActivityResult handleActivityResult) {
|
||||
|
||||
handleActivityResult.onHandleActivityResult(new DefaultCallback() {
|
||||
|
||||
@Override
|
||||
public void onCanceled(final ImageSource source, final int type) {
|
||||
|
|
@ -358,21 +384,22 @@ public class ContributionController {
|
|||
}
|
||||
|
||||
/**
|
||||
* Temporarily disabled, see issue [https://github.com/commons-app/apps-android-commons/issues/5847]
|
||||
* Fetches the contributions with the state "IN_PROGRESS", "QUEUED", "PAUSED" and "FAILED" and
|
||||
* then it populates the `failedAndPendingContributionList`.
|
||||
**/
|
||||
void getFailedAndPendingContributions() {
|
||||
final PagedList.Config pagedListConfig =
|
||||
(new PagedList.Config.Builder())
|
||||
.setPrefetchDistance(50)
|
||||
.setPageSize(10).build();
|
||||
Factory<Integer, Contribution> factory;
|
||||
factory = repository.fetchContributionsWithStates(
|
||||
Arrays.asList(Contribution.STATE_IN_PROGRESS, Contribution.STATE_QUEUED,
|
||||
Contribution.STATE_PAUSED, Contribution.STATE_FAILED));
|
||||
|
||||
LivePagedListBuilder livePagedListBuilder = new LivePagedListBuilder(factory,
|
||||
pagedListConfig);
|
||||
failedAndPendingContributionList = livePagedListBuilder.build();
|
||||
}
|
||||
// void getFailedAndPendingContributions() {
|
||||
// final PagedList.Config pagedListConfig =
|
||||
// (new PagedList.Config.Builder())
|
||||
// .setPrefetchDistance(50)
|
||||
// .setPageSize(10).build();
|
||||
// Factory<Integer, Contribution> factory;
|
||||
// factory = repository.fetchContributionsWithStates(
|
||||
// Arrays.asList(Contribution.STATE_IN_PROGRESS, Contribution.STATE_QUEUED,
|
||||
// Contribution.STATE_PAUSED, Contribution.STATE_FAILED));
|
||||
//
|
||||
// LivePagedListBuilder livePagedListBuilder = new LivePagedListBuilder(factory,
|
||||
// pagedListConfig);
|
||||
// failedAndPendingContributionList = livePagedListBuilder.build();
|
||||
// }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import static fr.free.nrw.commons.contributions.Contribution.STATE_FAILED;
|
|||
import static fr.free.nrw.commons.contributions.Contribution.STATE_PAUSED;
|
||||
import static fr.free.nrw.commons.nearby.fragments.NearbyParentFragment.WLM_URL;
|
||||
import static fr.free.nrw.commons.profile.ProfileActivity.KEY_USERNAME;
|
||||
import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK;
|
||||
import static fr.free.nrw.commons.utils.LengthUtils.computeBearing;
|
||||
import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween;
|
||||
|
||||
|
|
@ -23,12 +22,10 @@ import android.view.LayoutInflater;
|
|||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.MenuItem.OnMenuItemClickListener;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import androidx.activity.result.ActivityResultCallback;
|
||||
|
|
@ -39,7 +36,6 @@ import androidx.annotation.Nullable;
|
|||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager.OnBackStackChangedListener;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.Utils;
|
||||
import fr.free.nrw.commons.auth.SessionManager;
|
||||
import fr.free.nrw.commons.databinding.FragmentContributionsBinding;
|
||||
|
|
@ -293,7 +289,7 @@ public class ContributionsFragment
|
|||
});
|
||||
}
|
||||
notification.setOnClickListener(view -> {
|
||||
NotificationActivity.startYourself(getContext(), "unread");
|
||||
NotificationActivity.Companion.startYourself(getContext(), "unread");
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -307,16 +303,17 @@ public class ContributionsFragment
|
|||
}
|
||||
|
||||
/**
|
||||
* Temporarily disabled, see issue [https://github.com/commons-app/apps-android-commons/issues/5847]
|
||||
* Sets the visibility of the upload icon based on the number of failed and pending
|
||||
* contributions.
|
||||
*/
|
||||
public void setUploadIconVisibility() {
|
||||
contributionController.getFailedAndPendingContributions();
|
||||
contributionController.failedAndPendingContributionList.observe(getViewLifecycleOwner(),
|
||||
list -> {
|
||||
updateUploadIcon(list.size());
|
||||
});
|
||||
}
|
||||
// public void setUploadIconVisibility() {
|
||||
// contributionController.getFailedAndPendingContributions();
|
||||
// contributionController.failedAndPendingContributionList.observe(getViewLifecycleOwner(),
|
||||
// list -> {
|
||||
// updateUploadIcon(list.size());
|
||||
// });
|
||||
// }
|
||||
|
||||
/**
|
||||
* Sets the count for the upload icon based on the number of pending and failed contributions.
|
||||
|
|
@ -535,7 +532,8 @@ public class ContributionsFragment
|
|||
if (!isUserProfile) {
|
||||
setNotificationCount();
|
||||
fetchCampaigns();
|
||||
setUploadIconVisibility();
|
||||
// Temporarily disabled, see issue [https://github.com/commons-app/apps-android-commons/issues/5847]
|
||||
// setUploadIconVisibility();
|
||||
setUploadIconCount();
|
||||
}
|
||||
}
|
||||
|
|
@ -570,8 +568,8 @@ public class ContributionsFragment
|
|||
getString(R.string.nearby_card_permission_explanation),
|
||||
this::requestLocationPermission,
|
||||
this::displayYouWontSeeNearbyMessage,
|
||||
checkBoxView,
|
||||
false);
|
||||
checkBoxView
|
||||
);
|
||||
}
|
||||
|
||||
private void displayYouWontSeeNearbyMessage() {
|
||||
|
|
@ -761,19 +759,18 @@ public class ContributionsFragment
|
|||
}
|
||||
|
||||
/**
|
||||
* Updates the visibility of the pending uploads ImageView based on the given count.
|
||||
*
|
||||
* Temporarily disabled, see issue [https://github.com/commons-app/apps-android-commons/issues/5847]
|
||||
* @param count The number of pending uploads.
|
||||
*/
|
||||
public void updateUploadIcon(int count) {
|
||||
if (pendingUploadsImageView != null) {
|
||||
if (count != 0) {
|
||||
pendingUploadsImageView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
pendingUploadsImageView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
// public void updateUploadIcon(int count) {
|
||||
// if (pendingUploadsImageView != null) {
|
||||
// if (count != 0) {
|
||||
// pendingUploadsImageView.setVisibility(View.VISIBLE);
|
||||
// } else {
|
||||
// pendingUploadsImageView.setVisibility(View.GONE);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* Replace whatever is in the current contributionsFragmentContainer view with
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import static fr.free.nrw.commons.di.NetworkingModule.NAMED_LANGUAGE_WIKI_PEDIA_
|
|||
|
||||
import android.Manifest.permission;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
|
|
@ -20,6 +21,7 @@ import android.widget.LinearLayout;
|
|||
import androidx.activity.result.ActivityResultCallback;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions;
|
||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
|
@ -96,6 +98,30 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
|
|||
private int contributionsSize;
|
||||
private String userName;
|
||||
|
||||
private final ActivityResultLauncher<Intent> galleryPickLauncherForResult =
|
||||
registerForActivityResult(new StartActivityForResult(),
|
||||
result -> {
|
||||
controller.handleActivityResultWithCallback(requireActivity(), callbacks -> {
|
||||
controller.onPictureReturnedFromGallery(result, requireActivity(), callbacks);
|
||||
});
|
||||
});
|
||||
|
||||
private final ActivityResultLauncher<Intent> customSelectorLauncherForResult =
|
||||
registerForActivityResult(new StartActivityForResult(),
|
||||
result -> {
|
||||
controller.handleActivityResultWithCallback(requireActivity(), callbacks -> {
|
||||
controller.onPictureReturnedFromCustomSelector(result, requireActivity(), callbacks);
|
||||
});
|
||||
});
|
||||
|
||||
private final ActivityResultLauncher<Intent> cameraPickLauncherForResult =
|
||||
registerForActivityResult(new StartActivityForResult(),
|
||||
result -> {
|
||||
controller.handleActivityResultWithCallback(requireActivity(), callbacks -> {
|
||||
controller.onPictureReturnedFromCamera(result, requireActivity(), callbacks);
|
||||
});
|
||||
});
|
||||
|
||||
private ActivityResultLauncher<String[]> inAppCameraLocationPermissionLauncher = registerForActivityResult(
|
||||
new RequestMultiplePermissions(),
|
||||
new ActivityResultCallback<Map<String, Boolean>>() {
|
||||
|
|
@ -111,7 +137,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
|
|||
} else {
|
||||
if (shouldShowRequestPermissionRationale(permission.ACCESS_FINE_LOCATION)) {
|
||||
controller.handleShowRationaleFlowCameraLocation(getActivity(),
|
||||
inAppCameraLocationPermissionLauncher);
|
||||
inAppCameraLocationPermissionLauncher, cameraPickLauncherForResult);
|
||||
} else {
|
||||
controller.locationPermissionCallback.onLocationPermissionDenied(
|
||||
getActivity().getString(
|
||||
|
|
@ -322,7 +348,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
|
|||
private void setListeners() {
|
||||
binding.fabPlus.setOnClickListener(view -> animateFAB(isFabOpen));
|
||||
binding.fabCamera.setOnClickListener(view -> {
|
||||
controller.initiateCameraPick(getActivity(), inAppCameraLocationPermissionLauncher);
|
||||
controller.initiateCameraPick(getActivity(), inAppCameraLocationPermissionLauncher, cameraPickLauncherForResult);
|
||||
animateFAB(isFabOpen);
|
||||
});
|
||||
binding.fabCamera.setOnLongClickListener(view -> {
|
||||
|
|
@ -330,7 +356,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
|
|||
return true;
|
||||
});
|
||||
binding.fabGallery.setOnClickListener(view -> {
|
||||
controller.initiateGalleryPick(getActivity(), true);
|
||||
controller.initiateGalleryPick(getActivity(), galleryPickLauncherForResult, true);
|
||||
animateFAB(isFabOpen);
|
||||
});
|
||||
binding.fabGallery.setOnLongClickListener(view -> {
|
||||
|
|
@ -343,7 +369,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
|
|||
* Launch Custom Selector.
|
||||
*/
|
||||
protected void launchCustomSelector() {
|
||||
controller.initiateCustomGalleryPickWithPermission(getActivity());
|
||||
controller.initiateCustomGalleryPickWithPermission(getActivity(), customSelectorLauncherForResult);
|
||||
animateFAB(isFabOpen);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package fr.free.nrw.commons.contributions;
|
||||
|
||||
import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.paging.DataSource;
|
||||
|
|
@ -34,7 +36,7 @@ public class ContributionsListPresenter implements UserActionListener {
|
|||
final ContributionBoundaryCallback contributionBoundaryCallback,
|
||||
final ContributionsRemoteDataSource contributionsRemoteDataSource,
|
||||
final ContributionsRepository repository,
|
||||
@Named(CommonsApplicationModule.IO_THREAD) final Scheduler ioThreadScheduler) {
|
||||
@Named(IO_THREAD) final Scheduler ioThreadScheduler) {
|
||||
this.contributionBoundaryCallback = contributionBoundaryCallback;
|
||||
this.repository = repository;
|
||||
this.ioThreadScheduler = ioThreadScheduler;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package fr.free.nrw.commons.contributions;
|
||||
|
||||
import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD;
|
||||
import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK;
|
||||
|
||||
import androidx.work.ExistingWorkPolicy;
|
||||
|
|
@ -31,7 +32,7 @@ public class ContributionsPresenter implements UserActionListener {
|
|||
@Inject
|
||||
ContributionsPresenter(ContributionsRepository repository,
|
||||
UploadRepository uploadRepository,
|
||||
@Named(CommonsApplicationModule.IO_THREAD) Scheduler ioThreadScheduler) {
|
||||
@Named(IO_THREAD) Scheduler ioThreadScheduler) {
|
||||
this.contributionsRepository = repository;
|
||||
this.uploadRepository = uploadRepository;
|
||||
this.ioThreadScheduler = ioThreadScheduler;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package fr.free.nrw.commons.contributions
|
||||
|
||||
import androidx.paging.ItemKeyedDataSource
|
||||
import fr.free.nrw.commons.di.CommonsApplicationModule
|
||||
import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.IO_THREAD
|
||||
import fr.free.nrw.commons.media.MediaClient
|
||||
import io.reactivex.Scheduler
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
|
|
@ -16,7 +16,7 @@ class ContributionsRemoteDataSource
|
|||
@Inject
|
||||
constructor(
|
||||
private val mediaClient: MediaClient,
|
||||
@param:Named(CommonsApplicationModule.IO_THREAD) private val ioThreadScheduler: Scheduler,
|
||||
@param:Named(IO_THREAD) private val ioThreadScheduler: Scheduler,
|
||||
) : ItemKeyedDataSource<Int, Contribution>() {
|
||||
private val compositeDisposable: CompositeDisposable = CompositeDisposable()
|
||||
var userName: String? = null
|
||||
|
|
|
|||
|
|
@ -1,13 +1,10 @@
|
|||
package fr.free.nrw.commons.contributions;
|
||||
|
||||
import android.Manifest.permission;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Build.VERSION;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
|
|
@ -16,10 +13,8 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import androidx.work.ExistingWorkPolicy;
|
||||
import fr.free.nrw.commons.databinding.MainBinding;
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.WelcomeActivity;
|
||||
import fr.free.nrw.commons.auth.SessionManager;
|
||||
|
|
@ -41,7 +36,6 @@ import fr.free.nrw.commons.notification.NotificationController;
|
|||
import fr.free.nrw.commons.quiz.QuizChecker;
|
||||
import fr.free.nrw.commons.settings.SettingsFragment;
|
||||
import fr.free.nrw.commons.theme.BaseActivity;
|
||||
import fr.free.nrw.commons.upload.UploadActivity;
|
||||
import fr.free.nrw.commons.upload.UploadProgressActivity;
|
||||
import fr.free.nrw.commons.upload.worker.WorkRequestHelper;
|
||||
import fr.free.nrw.commons.utils.PermissionUtils;
|
||||
|
|
@ -420,7 +414,7 @@ public class MainActivity extends BaseActivity
|
|||
return true;
|
||||
case R.id.notifications:
|
||||
// Starts notification activity on click to notification icon
|
||||
NotificationActivity.startYourself(this, "unread");
|
||||
NotificationActivity.Companion.startYourself(this, "unread");
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
|
|
@ -438,13 +432,6 @@ public class MainActivity extends BaseActivity
|
|||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
Timber.d(data != null ? data.toString() : "onActivityResult data is null");
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
controller.handleActivityResult(this, requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ class WikipediaInstructionsDialogFragment : DialogFragment() {
|
|||
) = DialogAddToWikipediaInstructionsBinding
|
||||
.inflate(inflater, container, false)
|
||||
.apply {
|
||||
val contribution: Contribution? = arguments!!.getParcelable(ARG_CONTRIBUTION)
|
||||
val contribution: Contribution? = requireArguments().getParcelable(ARG_CONTRIBUTION)
|
||||
tvWikicode.setText(contribution?.media?.wikiCode)
|
||||
instructionsCancel.setOnClickListener { dismiss() }
|
||||
instructionsConfirm.setOnClickListener {
|
||||
|
|
|
|||
|
|
@ -1,187 +0,0 @@
|
|||
package fr.free.nrw.commons.coordinates;
|
||||
|
||||
import static fr.free.nrw.commons.notification.NotificationHelper.NOTIFICATION_EDIT_COORDINATES;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import fr.free.nrw.commons.BuildConfig;
|
||||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.actions.PageEditClient;
|
||||
import fr.free.nrw.commons.notification.NotificationHelper;
|
||||
import fr.free.nrw.commons.utils.ViewUtilWrapper;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import java.util.Objects;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* Helper class for edit and update given coordinates and showing notification about new coordinates
|
||||
* upgradation
|
||||
*/
|
||||
public class CoordinateEditHelper {
|
||||
|
||||
/**
|
||||
* notificationHelper: helps creating notification
|
||||
*/
|
||||
private final NotificationHelper notificationHelper;
|
||||
/**
|
||||
* * pageEditClient: methods provided by this member posts the edited coordinates
|
||||
* to the Media wiki api
|
||||
*/
|
||||
public final PageEditClient pageEditClient;
|
||||
/**
|
||||
* viewUtil: helps to show Toast
|
||||
*/
|
||||
private final ViewUtilWrapper viewUtil;
|
||||
|
||||
@Inject
|
||||
public CoordinateEditHelper(final NotificationHelper notificationHelper,
|
||||
@Named("commons-page-edit") final PageEditClient pageEditClient,
|
||||
final ViewUtilWrapper viewUtil) {
|
||||
this.notificationHelper = notificationHelper;
|
||||
this.pageEditClient = pageEditClient;
|
||||
this.viewUtil = viewUtil;
|
||||
}
|
||||
|
||||
/**
|
||||
* Public interface to edit coordinates
|
||||
* @param context to be added
|
||||
* @param media to be added
|
||||
* @param Accuracy to be added
|
||||
* @return Single<Boolean>
|
||||
*/
|
||||
public Single<Boolean> makeCoordinatesEdit(final Context context, final Media media,
|
||||
final String Latitude, final String Longitude, final String Accuracy) {
|
||||
viewUtil.showShortToast(context,
|
||||
context.getString(R.string.coordinates_edit_helper_make_edit_toast));
|
||||
return addCoordinates(media, Latitude, Longitude, Accuracy)
|
||||
.flatMapSingle(result -> Single.just(showCoordinatesEditNotification(context, media,
|
||||
Latitude, Longitude, Accuracy, result)))
|
||||
.firstOrError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces new coordinates
|
||||
* @param media to be added
|
||||
* @param Latitude to be added
|
||||
* @param Longitude to be added
|
||||
* @param Accuracy to be added
|
||||
* @return Observable<Boolean>
|
||||
*/
|
||||
private Observable<Boolean> addCoordinates(final Media media, final String Latitude,
|
||||
final String Longitude, final String Accuracy) {
|
||||
Timber.d("thread is coordinates adding %s", Thread.currentThread().getName());
|
||||
final String summary = "Adding Coordinates";
|
||||
|
||||
final StringBuilder buffer = new StringBuilder();
|
||||
|
||||
final String wikiText = pageEditClient.getCurrentWikiText(media.getFilename())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.blockingGet();
|
||||
|
||||
if (Latitude != null) {
|
||||
buffer.append("\n{{Location|").append(Latitude).append("|").append(Longitude)
|
||||
.append("|").append(Accuracy).append("}}");
|
||||
}
|
||||
|
||||
final String editedLocation = buffer.toString();
|
||||
final String appendText = getFormattedWikiText(wikiText, editedLocation);
|
||||
|
||||
return pageEditClient.edit(Objects.requireNonNull(media.getFilename())
|
||||
, appendText, summary);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helps to get formatted wikitext with upgraded location
|
||||
* @param wikiText current wikitext
|
||||
* @param editedLocation new location
|
||||
* @return String
|
||||
*/
|
||||
private String getFormattedWikiText(final String wikiText, final String editedLocation){
|
||||
|
||||
if (wikiText.contains("filedesc") && wikiText.contains("Location")) {
|
||||
|
||||
final String fromLocationToEnd = wikiText.substring(wikiText.indexOf("{{Location"));
|
||||
final String firstHalf = wikiText.substring(0, wikiText.indexOf("{{Location"));
|
||||
final String lastHalf = fromLocationToEnd.substring(
|
||||
fromLocationToEnd.indexOf("}}") + 2);
|
||||
|
||||
final int startOfSecondSection = StringUtils.ordinalIndexOf(wikiText,
|
||||
"==", 3);
|
||||
final StringBuilder buffer = new StringBuilder();
|
||||
if (wikiText.charAt(wikiText.indexOf("{{Location")-1) == '\n') {
|
||||
buffer.append(editedLocation.substring(1));
|
||||
} else {
|
||||
buffer.append(editedLocation);
|
||||
}
|
||||
if (startOfSecondSection != -1 && wikiText.charAt(startOfSecondSection-1)!= '\n') {
|
||||
buffer.append("\n");
|
||||
}
|
||||
|
||||
return firstHalf + buffer + lastHalf;
|
||||
|
||||
}
|
||||
if (wikiText.contains("filedesc") && !wikiText.contains("Location")) {
|
||||
|
||||
final int startOfSecondSection = StringUtils.ordinalIndexOf(wikiText,
|
||||
"==", 3);
|
||||
|
||||
if (startOfSecondSection != -1) {
|
||||
final String firstHalf = wikiText.substring(0, startOfSecondSection);
|
||||
final String lastHalf = wikiText.substring(startOfSecondSection);
|
||||
final String buffer = editedLocation.substring(1)
|
||||
+ "\n";
|
||||
return firstHalf + buffer + lastHalf;
|
||||
}
|
||||
|
||||
return wikiText + editedLocation;
|
||||
}
|
||||
return "== {{int:filedesc}} ==" + editedLocation + wikiText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update coordinates and shows notification about coordinates update
|
||||
* @param context to be added
|
||||
* @param media to be added
|
||||
* @param latitude to be added
|
||||
* @param longitude to be added
|
||||
* @param Accuracy to be added
|
||||
* @param result to be added
|
||||
* @return boolean
|
||||
*/
|
||||
private boolean showCoordinatesEditNotification(final Context context, final Media media,
|
||||
final String latitude, final String longitude, final String Accuracy,
|
||||
final boolean result) {
|
||||
final String message;
|
||||
String title = context.getString(R.string.coordinates_edit_helper_show_edit_title);
|
||||
|
||||
if (result) {
|
||||
media.setCoordinates(
|
||||
new fr.free.nrw.commons.location.LatLng(Double.parseDouble(latitude),
|
||||
Double.parseDouble(longitude),
|
||||
Float.parseFloat(Accuracy)));
|
||||
title += ": " + context
|
||||
.getString(R.string.coordinates_edit_helper_show_edit_title_success);
|
||||
final StringBuilder coordinatesInMessage = new StringBuilder();
|
||||
final String mediaCoordinate = String.valueOf(media.getCoordinates());
|
||||
coordinatesInMessage.append(mediaCoordinate);
|
||||
message = context.getString(R.string.coordinates_edit_helper_show_edit_message,
|
||||
coordinatesInMessage.toString());
|
||||
} else {
|
||||
title += ": " + context.getString(R.string.coordinates_edit_helper_show_edit_title);
|
||||
message = context.getString(R.string.coordinates_edit_helper_edit_message_else) ;
|
||||
}
|
||||
|
||||
final String urlForFile = BuildConfig.COMMONS_URL + "/wiki/" + media.getFilename();
|
||||
final Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(urlForFile));
|
||||
notificationHelper.showNotification(context, title, message, NOTIFICATION_EDIT_COORDINATES,
|
||||
browserIntent);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
package fr.free.nrw.commons.coordinates
|
||||
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import fr.free.nrw.commons.BuildConfig
|
||||
import fr.free.nrw.commons.Media
|
||||
import fr.free.nrw.commons.R
|
||||
import fr.free.nrw.commons.actions.PageEditClient
|
||||
import fr.free.nrw.commons.notification.NotificationHelper
|
||||
import fr.free.nrw.commons.notification.NotificationHelper.Companion.NOTIFICATION_EDIT_COORDINATES
|
||||
import fr.free.nrw.commons.utils.ViewUtilWrapper
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import java.util.Objects
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import timber.log.Timber
|
||||
|
||||
|
||||
/**
|
||||
* Helper class for edit and update given coordinates and showing notification about new coordinates
|
||||
* upgradation
|
||||
*/
|
||||
class CoordinateEditHelper @Inject constructor(
|
||||
private val notificationHelper: NotificationHelper,
|
||||
@Named("commons-page-edit") private val pageEditClient: PageEditClient,
|
||||
private val viewUtil: ViewUtilWrapper
|
||||
) {
|
||||
|
||||
/**
|
||||
* Public interface to edit coordinates
|
||||
* @param context to be added
|
||||
* @param media to be added
|
||||
* @param latitude to be added
|
||||
* @param longitude to be added
|
||||
* @param accuracy to be added
|
||||
* @return Single<Boolean>
|
||||
*/
|
||||
fun makeCoordinatesEdit(
|
||||
context: Context,
|
||||
media: Media,
|
||||
latitude: String,
|
||||
longitude: String,
|
||||
accuracy: String
|
||||
): Single<Boolean>? {
|
||||
viewUtil.showShortToast(
|
||||
context,
|
||||
context.getString(R.string.coordinates_edit_helper_make_edit_toast)
|
||||
)
|
||||
return addCoordinates(media, latitude, longitude, accuracy)
|
||||
?.flatMapSingle { result ->
|
||||
Single.just(showCoordinatesEditNotification(context, media, latitude, longitude, accuracy, result))
|
||||
}
|
||||
?.firstOrError()
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces new coordinates
|
||||
* @param media to be added
|
||||
* @param Latitude to be added
|
||||
* @param Longitude to be added
|
||||
* @param Accuracy to be added
|
||||
* @return Observable<Boolean>
|
||||
*/
|
||||
private fun addCoordinates(
|
||||
media: Media,
|
||||
Latitude: String,
|
||||
Longitude: String,
|
||||
Accuracy: String
|
||||
): Observable<Boolean>? {
|
||||
Timber.d("thread is coordinates adding %s", Thread.currentThread().getName())
|
||||
val summary = "Adding Coordinates"
|
||||
|
||||
val buffer = StringBuilder()
|
||||
|
||||
val wikiText = media.filename?.let {
|
||||
pageEditClient.getCurrentWikiText(it)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.blockingGet()
|
||||
}
|
||||
|
||||
if (Latitude != null) {
|
||||
buffer.append("\n{{Location|").append(Latitude).append("|").append(Longitude)
|
||||
.append("|").append(Accuracy).append("}}")
|
||||
}
|
||||
|
||||
val editedLocation = buffer.toString()
|
||||
val appendText = wikiText?.let { getFormattedWikiText(it, editedLocation) }
|
||||
|
||||
return Objects.requireNonNull(media.filename)
|
||||
?.let { pageEditClient.edit(it, appendText!!, summary) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Helps to get formatted wikitext with upgraded location
|
||||
* @param wikiText current wikitext
|
||||
* @param editedLocation new location
|
||||
* @return String
|
||||
*/
|
||||
private fun getFormattedWikiText(wikiText: String, editedLocation: String): String {
|
||||
if (wikiText.contains("filedesc") && wikiText.contains("Location")) {
|
||||
val fromLocationToEnd = wikiText.substring(wikiText.indexOf("{{Location"))
|
||||
val firstHalf = wikiText.substring(0, wikiText.indexOf("{{Location"))
|
||||
val lastHalf = fromLocationToEnd.substring(fromLocationToEnd.indexOf("}}") + 2)
|
||||
|
||||
val startOfSecondSection = StringUtils.ordinalIndexOf(wikiText, "==", 3)
|
||||
val buffer = StringBuilder()
|
||||
if (wikiText[wikiText.indexOf("{{Location") - 1] == '\n') {
|
||||
buffer.append(editedLocation.substring(1))
|
||||
} else {
|
||||
buffer.append(editedLocation)
|
||||
}
|
||||
if (startOfSecondSection != -1 && wikiText[startOfSecondSection - 1] != '\n') {
|
||||
buffer.append("\n")
|
||||
}
|
||||
|
||||
return firstHalf + buffer + lastHalf
|
||||
}
|
||||
if (wikiText.contains("filedesc") && !wikiText.contains("Location")) {
|
||||
val startOfSecondSection = StringUtils.ordinalIndexOf(wikiText, "==", 3)
|
||||
|
||||
if (startOfSecondSection != -1) {
|
||||
val firstHalf = wikiText.substring(0, startOfSecondSection)
|
||||
val lastHalf = wikiText.substring(startOfSecondSection)
|
||||
val buffer = editedLocation.substring(1) + "\n"
|
||||
return firstHalf + buffer + lastHalf
|
||||
}
|
||||
|
||||
return wikiText + editedLocation
|
||||
}
|
||||
return "== {{int:filedesc}} ==$editedLocation$wikiText"
|
||||
}
|
||||
|
||||
/**
|
||||
* Update coordinates and shows notification about coordinates update
|
||||
* @param context to be added
|
||||
* @param media to be added
|
||||
* @param latitude to be added
|
||||
* @param longitude to be added
|
||||
* @param Accuracy to be added
|
||||
* @param result to be added
|
||||
* @return boolean
|
||||
*/
|
||||
private fun showCoordinatesEditNotification(
|
||||
context: Context,
|
||||
media: Media,
|
||||
latitude: String,
|
||||
longitude: String,
|
||||
Accuracy: String,
|
||||
result: Boolean
|
||||
): Boolean {
|
||||
val message: String
|
||||
var title = context.getString(R.string.coordinates_edit_helper_show_edit_title)
|
||||
|
||||
if (result) {
|
||||
media.coordinates = fr.free.nrw.commons.location.LatLng(
|
||||
latitude.toDouble(),
|
||||
longitude.toDouble(),
|
||||
Accuracy.toFloat()
|
||||
)
|
||||
title += ": " + context.getString(R.string.coordinates_edit_helper_show_edit_title_success)
|
||||
val coordinatesInMessage = StringBuilder()
|
||||
val mediaCoordinate = media.coordinates.toString()
|
||||
coordinatesInMessage.append(mediaCoordinate)
|
||||
message = context.getString(
|
||||
R.string.coordinates_edit_helper_show_edit_message,
|
||||
coordinatesInMessage.toString()
|
||||
)
|
||||
} else {
|
||||
title += ": " + context.getString(R.string.coordinates_edit_helper_show_edit_title)
|
||||
message = context.getString(R.string.coordinates_edit_helper_edit_message_else)
|
||||
}
|
||||
|
||||
val urlForFile = BuildConfig.COMMONS_URL + "/wiki/" + media.filename
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(urlForFile))
|
||||
notificationHelper.showNotification(
|
||||
context,
|
||||
title,
|
||||
message,
|
||||
NOTIFICATION_EDIT_COORDINATES,
|
||||
browserIntent
|
||||
)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,248 @@
|
|||
package fr.free.nrw.commons.customselector.helper
|
||||
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
import android.media.MediaScannerConnection
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.MediaStore
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.IntentSenderRequest
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import fr.free.nrw.commons.R
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
|
||||
object FolderDeletionHelper {
|
||||
|
||||
/**
|
||||
* Prompts the user to confirm deletion of a specified folder and, if confirmed, deletes it.
|
||||
*
|
||||
* @param context The context used to show the confirmation dialog and manage deletion.
|
||||
* @param folder The folder to be deleted.
|
||||
* @param onDeletionComplete Callback invoked with `true` if the folder was
|
||||
* @param trashFolderLauncher An ActivityResultLauncher for handling the result of the trash request.
|
||||
* successfully deleted, `false` otherwise.
|
||||
*/
|
||||
fun confirmAndDeleteFolder(
|
||||
context: Context,
|
||||
folder: File,
|
||||
trashFolderLauncher: ActivityResultLauncher<IntentSenderRequest>,
|
||||
onDeletionComplete: (Boolean) -> Unit) {
|
||||
|
||||
//don't show this dialog on API 30+, it's handled automatically using MediaStore
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
val success = trashImagesInFolder(context, folder, trashFolderLauncher)
|
||||
onDeletionComplete(success)
|
||||
} else {
|
||||
val imagePaths = listImagesInFolder(context, folder)
|
||||
val imageCount = imagePaths.size
|
||||
val folderPath = folder.absolutePath
|
||||
|
||||
AlertDialog.Builder(context)
|
||||
.setTitle(context.getString(R.string.custom_selector_confirm_deletion_title))
|
||||
.setMessage(
|
||||
context.getString(
|
||||
R.string.custom_selector_confirm_deletion_message,
|
||||
folderPath,
|
||||
imageCount
|
||||
)
|
||||
)
|
||||
.setPositiveButton(context.getString(R.string.custom_selector_delete)) { _, _ ->
|
||||
|
||||
//proceed with deletion if user confirms
|
||||
val success = deleteImagesLegacy(imagePaths)
|
||||
onDeletionComplete(success)
|
||||
}
|
||||
.setNegativeButton(context.getString(R.string.custom_selector_cancel)) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
onDeletionComplete(false)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves all images in a specified folder (but not within its subfolders) to the trash on
|
||||
* devices running Android 11 (API level 30) and above.
|
||||
*
|
||||
* @param context The context used to access the content resolver.
|
||||
* @param folder The folder whose top-level images are to be moved to the trash.
|
||||
* @param trashFolderLauncher An ActivityResultLauncher for handling the result of the trash
|
||||
* request.
|
||||
* @return `true` if the trash request was initiated successfully, `false` otherwise.
|
||||
*/
|
||||
private fun trashImagesInFolder(
|
||||
context: Context,
|
||||
folder: File,
|
||||
trashFolderLauncher: ActivityResultLauncher<IntentSenderRequest>): Boolean
|
||||
{
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return false
|
||||
|
||||
val contentResolver = context.contentResolver
|
||||
val folderPath = folder.absolutePath
|
||||
val urisToTrash = mutableListOf<Uri>()
|
||||
|
||||
val mediaUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
|
||||
|
||||
// select images contained in the folder but not within subfolders
|
||||
val selection =
|
||||
"${MediaStore.MediaColumns.DATA} LIKE ? AND ${MediaStore.MediaColumns.DATA} NOT LIKE ?"
|
||||
val selectionArgs = arrayOf("$folderPath/%", "$folderPath/%/%")
|
||||
|
||||
contentResolver.query(
|
||||
mediaUri, arrayOf(MediaStore.MediaColumns._ID), selection,
|
||||
selectionArgs, null
|
||||
)?.use { cursor ->
|
||||
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)
|
||||
while (cursor.moveToNext()) {
|
||||
val id = cursor.getLong(idColumn)
|
||||
val fileUri = ContentUris.withAppendedId(mediaUri, id)
|
||||
urisToTrash.add(fileUri)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//proceed with trashing if we have valid URIs
|
||||
if (urisToTrash.isNotEmpty()) {
|
||||
try {
|
||||
val trashRequest = MediaStore.createTrashRequest(contentResolver, urisToTrash, true)
|
||||
val intentSenderRequest =
|
||||
IntentSenderRequest.Builder(trashRequest.intentSender).build()
|
||||
trashFolderLauncher.launch(intentSenderRequest)
|
||||
return true
|
||||
} catch (e: SecurityException) {
|
||||
Timber.tag("DeleteFolder").e(
|
||||
context.getString(
|
||||
R.string.custom_selector_error_trashing_folder_contents,
|
||||
e.message
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Lists all image file paths in the specified folder, excluding any subfolders.
|
||||
*
|
||||
* @param context The context used to access the content resolver.
|
||||
* @param folder The folder whose top-level images are to be listed.
|
||||
* @return A list of file paths (as Strings) pointing to the images in the specified folder.
|
||||
*/
|
||||
private fun listImagesInFolder(context: Context, folder: File): List<String> {
|
||||
val contentResolver = context.contentResolver
|
||||
val folderPath = folder.absolutePath
|
||||
val mediaUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
|
||||
val selection =
|
||||
"${MediaStore.MediaColumns.DATA} LIKE ? AND ${MediaStore.MediaColumns.DATA} NOT LIKE ?"
|
||||
val selectionArgs = arrayOf("$folderPath/%", "$folderPath/%/%")
|
||||
val imagePaths = mutableListOf<String>()
|
||||
|
||||
contentResolver.query(
|
||||
mediaUri, arrayOf(MediaStore.MediaColumns.DATA), selection,
|
||||
selectionArgs, null
|
||||
)?.use { cursor ->
|
||||
val dataColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA)
|
||||
while (cursor.moveToNext()) {
|
||||
val imagePath = cursor.getString(dataColumn)
|
||||
imagePaths.add(imagePath)
|
||||
}
|
||||
}
|
||||
return imagePaths
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Refreshes the MediaStore for a specified folder, updating the system to recognize any changes
|
||||
*
|
||||
* @param context The context used to access the MediaScannerConnection.
|
||||
* @param folder The folder to refresh in the MediaStore.
|
||||
*/
|
||||
fun refreshMediaStore(context: Context, folder: File) {
|
||||
MediaScannerConnection.scanFile(
|
||||
context,
|
||||
arrayOf(folder.absolutePath),
|
||||
null
|
||||
) { _, _ -> }
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Deletes a list of image files specified by their paths, on
|
||||
* Android 10 (API level 29) and below.
|
||||
*
|
||||
* @param imagePaths A list of absolute file paths to image files that need to be deleted.
|
||||
* @return `true` if all the images are successfully deleted, `false` otherwise.
|
||||
*/
|
||||
private fun deleteImagesLegacy(imagePaths: List<String>): Boolean {
|
||||
var result = true
|
||||
imagePaths.forEach {
|
||||
val imageFile = File(it)
|
||||
val deleted = imageFile.exists() && imageFile.delete()
|
||||
result = result && deleted
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves the absolute path of a folder given its unique identifier (bucket ID).
|
||||
*
|
||||
* @param context The context used to access the content resolver.
|
||||
* @param folderId The unique identifier (bucket ID) of the folder.
|
||||
* @return The absolute path of the folder as a `String`, or `null` if the folder is not found.
|
||||
*/
|
||||
fun getFolderPath(context: Context, folderId: Long): String? {
|
||||
val projection = arrayOf(MediaStore.Images.Media.DATA)
|
||||
val selection = "${MediaStore.Images.Media.BUCKET_ID} = ?"
|
||||
val selectionArgs = arrayOf(folderId.toString())
|
||||
|
||||
context.contentResolver.query(
|
||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
||||
projection,
|
||||
selection,
|
||||
selectionArgs,
|
||||
null
|
||||
)?.use { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
val fullPath = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA))
|
||||
return File(fullPath).parent
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays an error message to the user and logs it for debugging purposes.
|
||||
*
|
||||
* @param context The context used to display the Toast.
|
||||
* @param message The error message to display and log.
|
||||
* @param folderName The name of the folder to delete.
|
||||
*/
|
||||
fun showError(context: Context, message: String, folderName: String) {
|
||||
Toast.makeText(context,
|
||||
context.getString(R.string.custom_selector_folder_deleted_failure, folderName),
|
||||
Toast.LENGTH_SHORT).show()
|
||||
Timber.tag("DeleteFolder").e(message)
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a success message to the user.
|
||||
*
|
||||
* @param context The context used to display the Toast.
|
||||
* @param message The success message to display.
|
||||
* @param folderName The name of the folder to delete.
|
||||
*/
|
||||
fun showSuccess(context: Context, message: String, folderName: String) {
|
||||
Toast.makeText(context,
|
||||
context.getString(R.string.custom_selector_folder_deleted_success, folderName),
|
||||
Toast.LENGTH_SHORT).show()
|
||||
Timber.tag("DeleteFolder").d(message)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -12,7 +12,11 @@ import android.view.View
|
|||
import android.view.Window
|
||||
import android.widget.Button
|
||||
import android.widget.ImageButton
|
||||
import android.widget.PopupMenu
|
||||
import android.widget.TextView
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
|
|
@ -41,14 +45,13 @@ import fr.free.nrw.commons.R
|
|||
import fr.free.nrw.commons.customselector.database.NotForUploadStatus
|
||||
import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao
|
||||
import fr.free.nrw.commons.customselector.helper.CustomSelectorConstants
|
||||
import fr.free.nrw.commons.customselector.helper.CustomSelectorConstants.SHOULD_REFRESH
|
||||
import fr.free.nrw.commons.customselector.helper.FolderDeletionHelper
|
||||
import fr.free.nrw.commons.customselector.listeners.FolderClickListener
|
||||
import fr.free.nrw.commons.customselector.listeners.ImageSelectListener
|
||||
import fr.free.nrw.commons.customselector.model.Image
|
||||
import fr.free.nrw.commons.databinding.ActivityCustomSelectorBinding
|
||||
import fr.free.nrw.commons.databinding.CustomSelectorBottomLayoutBinding
|
||||
import fr.free.nrw.commons.databinding.CustomSelectorToolbarBinding
|
||||
import fr.free.nrw.commons.filepicker.Constants
|
||||
import fr.free.nrw.commons.media.ZoomableActivity
|
||||
import fr.free.nrw.commons.theme.BaseActivity
|
||||
import fr.free.nrw.commons.upload.FileUtilsWrapper
|
||||
|
|
@ -147,6 +150,23 @@ class CustomSelectorActivity :
|
|||
|
||||
private var showPartialAccessIndicator by mutableStateOf(false)
|
||||
|
||||
/**
|
||||
* Show delete button in folder
|
||||
*/
|
||||
private var showOverflowMenu = false
|
||||
|
||||
/**
|
||||
* Waits for confirmation of delete folder
|
||||
*/
|
||||
private val startForFolderDeletionResult = registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()){
|
||||
result -> onDeleteFolderResultReceived(result)
|
||||
}
|
||||
|
||||
private val startForResult = registerForActivityResult(StartActivityForResult()){ result ->
|
||||
onFullScreenDataReceived(result)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* onCreate Activity, sets theme, initialises the view model, setup view.
|
||||
*/
|
||||
|
|
@ -225,23 +245,24 @@ class CustomSelectorActivity :
|
|||
/**
|
||||
* When data will be send from full screen mode, it will be passed to fragment
|
||||
*/
|
||||
override fun onActivityResult(
|
||||
requestCode: Int,
|
||||
resultCode: Int,
|
||||
data: Intent?,
|
||||
) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
if (requestCode == Constants.RequestCodes.RECEIVE_DATA_FROM_FULL_SCREEN_MODE &&
|
||||
resultCode == Activity.RESULT_OK
|
||||
) {
|
||||
private fun onFullScreenDataReceived(result: ActivityResult){
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
val selectedImages: ArrayList<Image> =
|
||||
data!!
|
||||
result.data!!
|
||||
.getParcelableArrayListExtra(CustomSelectorConstants.NEW_SELECTED_IMAGES)!!
|
||||
val shouldRefresh = data.getBooleanExtra(SHOULD_REFRESH, false)
|
||||
imageFragment?.passSelectedImages(selectedImages, shouldRefresh)
|
||||
viewModel?.selectedImages?.value = selectedImages
|
||||
}
|
||||
}
|
||||
|
||||
private fun onDeleteFolderResultReceived(result: ActivityResult){
|
||||
if (result.resultCode == Activity.RESULT_OK){
|
||||
FolderDeletionHelper.showSuccess(this, "Folder deleted successfully", bucketName)
|
||||
navigateToCustomSelector()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Show Custom Selector Welcome Dialog.
|
||||
*/
|
||||
|
|
@ -423,10 +444,97 @@ class CustomSelectorActivity :
|
|||
val limitError: ImageButton = findViewById(R.id.image_limit_error)
|
||||
limitError.visibility = View.INVISIBLE
|
||||
limitError.setOnClickListener { displayUploadLimitWarning() }
|
||||
|
||||
val overflowMenu: ImageButton = findViewById(R.id.menu_overflow)
|
||||
if(defaultKvStore.getBoolean("displayDeletionButton")) {
|
||||
overflowMenu.visibility = if (showOverflowMenu) View.VISIBLE else View.INVISIBLE
|
||||
overflowMenu.setOnClickListener { showPopupMenu(overflowMenu) }
|
||||
}else{
|
||||
overflowMenu.visibility = View.GONE
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun showPopupMenu(anchorView: View) {
|
||||
val popupMenu = PopupMenu(this, anchorView)
|
||||
popupMenu.menuInflater.inflate(R.menu.menu_custom_selector, popupMenu.menu)
|
||||
|
||||
popupMenu.setOnMenuItemClickListener { item ->
|
||||
when (item.itemId) {
|
||||
R.id.action_delete_folder -> {
|
||||
deleteFolder()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
popupMenu.show()
|
||||
}
|
||||
|
||||
/**
|
||||
* override on folder click, change the toolbar title on folder click.
|
||||
* Deletes folder based on Android API version.
|
||||
*/
|
||||
private fun deleteFolder() {
|
||||
val folderPath = FolderDeletionHelper.getFolderPath(this, bucketId) ?: run {
|
||||
FolderDeletionHelper.showError(this, "Failed to retrieve folder path", bucketName)
|
||||
return
|
||||
}
|
||||
|
||||
val folder = File(folderPath)
|
||||
if (!folder.exists() || !folder.isDirectory) {
|
||||
FolderDeletionHelper.showError(this,"Folder not found or is not a directory", bucketName)
|
||||
return
|
||||
}
|
||||
|
||||
FolderDeletionHelper.confirmAndDeleteFolder(this, folder, startForFolderDeletionResult) { success ->
|
||||
if (success) {
|
||||
//for API 30+, navigation is handled in 'onDeleteFolderResultReceived'
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||
FolderDeletionHelper.showSuccess(this, "Folder deleted successfully", bucketName)
|
||||
navigateToCustomSelector()
|
||||
}
|
||||
} else {
|
||||
FolderDeletionHelper.showError(this, "Failed to delete folder", bucketName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Navigates back to the main `FolderFragment`, refreshes the MediaStore, resets UI states,
|
||||
* and reloads folder data.
|
||||
*/
|
||||
private fun navigateToCustomSelector() {
|
||||
|
||||
val folderPath = FolderDeletionHelper.getFolderPath(this, bucketId) ?: ""
|
||||
val folder = File(folderPath)
|
||||
|
||||
supportFragmentManager.popBackStack(null,
|
||||
androidx.fragment.app.FragmentManager.POP_BACK_STACK_INCLUSIVE)
|
||||
|
||||
//refresh MediaStore for the deleted folder path to ensure metadata updates
|
||||
FolderDeletionHelper.refreshMediaStore(this, folder)
|
||||
|
||||
//replace the current fragment with FolderFragment to go back to the main screen
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.fragment_container, FolderFragment.newInstance())
|
||||
.commitAllowingStateLoss()
|
||||
|
||||
//reset toolbar and flags
|
||||
isImageFragmentOpen = false
|
||||
showOverflowMenu = false
|
||||
setUpToolbar()
|
||||
changeTitle(getString(R.string.custom_selector_title), 0)
|
||||
|
||||
//fetch updated folder data
|
||||
fetchData()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* override on folder click,
|
||||
* change the toolbar title on folder click, make overflow menu visible
|
||||
*/
|
||||
override fun onFolderClick(
|
||||
folderId: Long,
|
||||
|
|
@ -444,6 +552,11 @@ class CustomSelectorActivity :
|
|||
bucketId = folderId
|
||||
bucketName = folderName
|
||||
isImageFragmentOpen = true
|
||||
|
||||
//show the overflow menu only when a folder is clicked
|
||||
showOverflowMenu = true
|
||||
setUpToolbar()
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -511,7 +624,7 @@ class CustomSelectorActivity :
|
|||
selectedImages,
|
||||
)
|
||||
intent.putExtra(CustomSelectorConstants.BUCKET_ID, bucketId)
|
||||
startActivityForResult(intent, Constants.RequestCodes.RECEIVE_DATA_FROM_FULL_SCREEN_MODE)
|
||||
startForResult.launch(intent)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -559,6 +672,10 @@ class CustomSelectorActivity :
|
|||
isImageFragmentOpen = false
|
||||
changeTitle(getString(R.string.custom_selector_title), 0)
|
||||
}
|
||||
|
||||
//hide overflow menu when not in folder
|
||||
showOverflowMenu = false
|
||||
setUpToolbar()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package fr.free.nrw.commons.customselector.ui.selector
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Context.MODE_PRIVATE
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
|
|
@ -279,11 +280,17 @@ class ImageFragment :
|
|||
filteredImages = ImageHelper.filterImages(images, bucketId)
|
||||
allImages = ArrayList(filteredImages)
|
||||
imageAdapter.init(filteredImages, allImages, TreeMap(), uploadingContributions)
|
||||
viewModel?.selectedImages?.value?.let { selectedImages ->
|
||||
imageAdapter.setSelectedImages(selectedImages)
|
||||
}
|
||||
imageAdapter.notifyDataSetChanged()
|
||||
selectorRV?.let {
|
||||
it.visibility = View.VISIBLE
|
||||
lastItemId?.let { pos ->
|
||||
(it.layoutManager as GridLayoutManager)
|
||||
.scrollToPosition(ImageHelper.getIndexFromId(filteredImages, pos))
|
||||
if (switch?.isChecked == false) {
|
||||
lastItemId?.let { pos ->
|
||||
(it.layoutManager as GridLayoutManager)
|
||||
.scrollToPosition(ImageHelper.getIndexFromId(filteredImages, pos))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -340,7 +347,7 @@ class ImageFragment :
|
|||
context
|
||||
.getSharedPreferences(
|
||||
"CustomSelector",
|
||||
BaseActivity.MODE_PRIVATE,
|
||||
MODE_PRIVATE,
|
||||
)?.let { prefs ->
|
||||
prefs.edit()?.let { editor ->
|
||||
editor.putLong("ItemId", imageAdapter.getImageIdAt(position))?.apply()
|
||||
|
|
@ -382,14 +389,6 @@ class ImageFragment :
|
|||
selectedImages: ArrayList<Image>,
|
||||
shouldRefresh: Boolean,
|
||||
) {
|
||||
imageAdapter.setSelectedImages(selectedImages)
|
||||
|
||||
val uploadingContributions = getUploadingContributions()
|
||||
|
||||
if (!showAlreadyActionedImages && shouldRefresh) {
|
||||
imageAdapter.init(filteredImages, allImages, TreeMap(), uploadingContributions)
|
||||
imageAdapter.setSelectedImages(selectedImages)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import fr.free.nrw.commons.utils.CustomSelectorUtils
|
|||
import fr.free.nrw.commons.utils.CustomSelectorUtils.Companion.checkWhetherFileExistsOnCommonsUsingSHA1
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Calendar
|
||||
|
|
|
|||
|
|
@ -1,63 +0,0 @@
|
|||
package fr.free.nrw.commons.data;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao;
|
||||
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao;
|
||||
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao;
|
||||
import fr.free.nrw.commons.category.CategoryDao;
|
||||
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao;
|
||||
import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao;
|
||||
|
||||
public class DBOpenHelper extends SQLiteOpenHelper {
|
||||
|
||||
private static final String DATABASE_NAME = "commons.db";
|
||||
private static final int DATABASE_VERSION = 20;
|
||||
public static final String CONTRIBUTIONS_TABLE = "contributions";
|
||||
private final String DROP_TABLE_STATEMENT="DROP TABLE IF EXISTS %s";
|
||||
|
||||
/**
|
||||
* Do not use directly - @Inject an instance where it's needed and let
|
||||
* dependency injection take care of managing this as a singleton.
|
||||
*/
|
||||
public DBOpenHelper(Context context) {
|
||||
super(context, DATABASE_NAME, null, DATABASE_VERSION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase sqLiteDatabase) {
|
||||
CategoryDao.Table.onCreate(sqLiteDatabase);
|
||||
BookmarkPicturesDao.Table.onCreate(sqLiteDatabase);
|
||||
BookmarkLocationsDao.Table.onCreate(sqLiteDatabase);
|
||||
BookmarkItemsDao.Table.onCreate(sqLiteDatabase);
|
||||
RecentSearchesDao.Table.onCreate(sqLiteDatabase);
|
||||
RecentLanguagesDao.Table.onCreate(sqLiteDatabase);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int from, int to) {
|
||||
CategoryDao.Table.onUpdate(sqLiteDatabase, from, to);
|
||||
BookmarkPicturesDao.Table.onUpdate(sqLiteDatabase, from, to);
|
||||
BookmarkLocationsDao.Table.onUpdate(sqLiteDatabase, from, to);
|
||||
BookmarkItemsDao.Table.onUpdate(sqLiteDatabase, from, to);
|
||||
RecentSearchesDao.Table.onUpdate(sqLiteDatabase, from, to);
|
||||
RecentLanguagesDao.Table.onUpdate(sqLiteDatabase, from, to);
|
||||
deleteTable(sqLiteDatabase,CONTRIBUTIONS_TABLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete table in the given db
|
||||
* @param db
|
||||
* @param tableName
|
||||
*/
|
||||
public void deleteTable(SQLiteDatabase db, String tableName) {
|
||||
try {
|
||||
db.execSQL(String.format(DROP_TABLE_STATEMENT, tableName));
|
||||
onCreate(db);
|
||||
} catch (SQLiteException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
62
app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.kt
Normal file
62
app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.kt
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
package fr.free.nrw.commons.data
|
||||
|
||||
import android.content.Context
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteException
|
||||
import android.database.sqlite.SQLiteOpenHelper
|
||||
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao
|
||||
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao
|
||||
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao
|
||||
import fr.free.nrw.commons.category.CategoryDao
|
||||
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao
|
||||
import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao
|
||||
|
||||
|
||||
class DBOpenHelper(
|
||||
context: Context
|
||||
): SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
|
||||
|
||||
companion object {
|
||||
private const val DATABASE_NAME = "commons.db"
|
||||
private const val DATABASE_VERSION = 20
|
||||
const val CONTRIBUTIONS_TABLE = "contributions"
|
||||
private const val DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS %s"
|
||||
}
|
||||
|
||||
/**
|
||||
* Do not use directly - @Inject an instance where it's needed and let
|
||||
* dependency injection take care of managing this as a singleton.
|
||||
*/
|
||||
override fun onCreate(db: SQLiteDatabase) {
|
||||
CategoryDao.Table.onCreate(db)
|
||||
BookmarkPicturesDao.Table.onCreate(db)
|
||||
BookmarkLocationsDao.Table.onCreate(db)
|
||||
BookmarkItemsDao.Table.onCreate(db)
|
||||
RecentSearchesDao.Table.onCreate(db)
|
||||
RecentLanguagesDao.Table.onCreate(db)
|
||||
}
|
||||
|
||||
override fun onUpgrade(db: SQLiteDatabase, from: Int, to: Int) {
|
||||
CategoryDao.Table.onUpdate(db, from, to)
|
||||
BookmarkPicturesDao.Table.onUpdate(db, from, to)
|
||||
BookmarkLocationsDao.Table.onUpdate(db, from, to)
|
||||
BookmarkItemsDao.Table.onUpdate(db, from, to)
|
||||
RecentSearchesDao.Table.onUpdate(db, from, to)
|
||||
RecentLanguagesDao.Table.onUpdate(db, from, to)
|
||||
deleteTable(db, CONTRIBUTIONS_TABLE)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete table in the given db
|
||||
* @param db
|
||||
* @param tableName
|
||||
*/
|
||||
fun deleteTable(db: SQLiteDatabase, tableName: String) {
|
||||
try {
|
||||
db.execSQL(String.format(DROP_TABLE_STATEMENT, tableName))
|
||||
onCreate(db)
|
||||
} catch (e: SQLiteException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,162 +0,0 @@
|
|||
package fr.free.nrw.commons.db;
|
||||
|
||||
import android.net.Uri;
|
||||
import androidx.room.TypeConverter;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.contributions.ChunkInfo;
|
||||
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
||||
import fr.free.nrw.commons.location.LatLng;
|
||||
import fr.free.nrw.commons.nearby.Sitelinks;
|
||||
import fr.free.nrw.commons.upload.WikidataPlace;
|
||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* This class supplies converters to write/read types to/from the database.
|
||||
*/
|
||||
public class Converters {
|
||||
|
||||
public static Gson getGson() {
|
||||
return ApplicationlessInjection.getInstance(CommonsApplication.getInstance()).getCommonsApplicationComponent().gson();
|
||||
}
|
||||
|
||||
/**
|
||||
* convert DepictedItem object to string
|
||||
* input Example -> DepictedItem depictedItem=new DepictedItem ()
|
||||
* output Example -> string
|
||||
*/
|
||||
@TypeConverter
|
||||
public static String depictsItemToString(DepictedItem objects) {
|
||||
return writeObjectToString(objects);
|
||||
}
|
||||
|
||||
/**
|
||||
* convert string to DepictedItem object
|
||||
* output Example -> DepictedItem depictedItem=new DepictedItem ()
|
||||
* input Example -> string
|
||||
*/
|
||||
@TypeConverter
|
||||
public static DepictedItem stringToDepicts(String objectList) {
|
||||
return readObjectWithTypeToken(objectList, new TypeToken<DepictedItem>() {
|
||||
});
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
public static Date fromTimestamp(Long value) {
|
||||
return value == null ? null : new Date(value);
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
public static Long dateToTimestamp(Date date) {
|
||||
return date == null ? null : date.getTime();
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
public static Uri fromString(String value) {
|
||||
return value == null ? null : Uri.parse(value);
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
public static String uriToString(Uri uri) {
|
||||
return uri == null ? null : uri.toString();
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
public static String listObjectToString(List<String> objectList) {
|
||||
return writeObjectToString(objectList);
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
public static List<String> stringToListObject(String objectList) {
|
||||
return readObjectWithTypeToken(objectList, new TypeToken<List<String>>() {});
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
public static String mapObjectToString(Map<String,String> objectList) {
|
||||
return writeObjectToString(objectList);
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
public static String mapObjectToString2(Map<String,Boolean> objectList) {
|
||||
return writeObjectToString(objectList);
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
public static Map<String,String> stringToMap(String objectList) {
|
||||
return readObjectWithTypeToken(objectList, new TypeToken<Map<String,String>>(){});
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
public static Map<String,Boolean> stringToMap2(String objectList) {
|
||||
return readObjectWithTypeToken(objectList, new TypeToken<Map<String,Boolean>>(){});
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
public static String latlngObjectToString(LatLng latlng) {
|
||||
return writeObjectToString(latlng);
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
public static LatLng stringToLatLng(String objectList) {
|
||||
return readObjectFromString(objectList,LatLng.class);
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
public static String wikidataPlaceToString(WikidataPlace wikidataPlace) {
|
||||
return writeObjectToString(wikidataPlace);
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
public static WikidataPlace stringToWikidataPlace(String wikidataPlace) {
|
||||
return readObjectFromString(wikidataPlace, WikidataPlace.class);
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
public static String chunkInfoToString(ChunkInfo chunkInfo) {
|
||||
return writeObjectToString(chunkInfo);
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
public static ChunkInfo stringToChunkInfo(String chunkInfo) {
|
||||
return readObjectFromString(chunkInfo, ChunkInfo.class);
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
public static String depictionListToString(List<DepictedItem> depictedItems) {
|
||||
return writeObjectToString(depictedItems);
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
public static List<DepictedItem> stringToList(String depictedItems) {
|
||||
return readObjectWithTypeToken(depictedItems, new TypeToken<List<DepictedItem>>() {});
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
public static Sitelinks sitelinksFromString(String value) {
|
||||
Type type = new TypeToken<Sitelinks>() {}.getType();
|
||||
return new Gson().fromJson(value, type);
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
public static String fromSitelinks(Sitelinks sitelinks) {
|
||||
Gson gson = new Gson();
|
||||
return gson.toJson(sitelinks);
|
||||
}
|
||||
|
||||
private static String writeObjectToString(Object object) {
|
||||
return object == null ? null : getGson().toJson(object);
|
||||
}
|
||||
|
||||
private static<T> T readObjectFromString(String objectAsString, Class<T> clazz) {
|
||||
return objectAsString == null ? null : getGson().fromJson(objectAsString, clazz);
|
||||
}
|
||||
|
||||
private static <T> T readObjectWithTypeToken(String objectList, TypeToken<T> typeToken) {
|
||||
return objectList == null ? null : getGson().fromJson(objectList, typeToken.getType());
|
||||
}
|
||||
}
|
||||
182
app/src/main/java/fr/free/nrw/commons/db/Converters.kt
Normal file
182
app/src/main/java/fr/free/nrw/commons/db/Converters.kt
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
package fr.free.nrw.commons.db
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.room.TypeConverter
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import fr.free.nrw.commons.CommonsApplication
|
||||
import fr.free.nrw.commons.contributions.ChunkInfo
|
||||
import fr.free.nrw.commons.di.ApplicationlessInjection
|
||||
import fr.free.nrw.commons.location.LatLng
|
||||
import fr.free.nrw.commons.nearby.Sitelinks
|
||||
import fr.free.nrw.commons.upload.WikidataPlace
|
||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
|
||||
import java.util.Date
|
||||
|
||||
/**
|
||||
* This object supplies converters to write/read types to/from the database.
|
||||
*/
|
||||
object Converters {
|
||||
|
||||
fun getGson(): Gson {
|
||||
return ApplicationlessInjection
|
||||
.getInstance(CommonsApplication.instance)
|
||||
.commonsApplicationComponent
|
||||
.gson()
|
||||
}
|
||||
|
||||
/**
|
||||
* convert DepictedItem object to string
|
||||
* input Example -> DepictedItem depictedItem=new DepictedItem ()
|
||||
* output Example -> string
|
||||
*/
|
||||
@TypeConverter
|
||||
@JvmStatic
|
||||
fun depictsItemToString(objects: DepictedItem?): String? {
|
||||
return writeObjectToString(objects)
|
||||
}
|
||||
|
||||
/**
|
||||
* convert string to DepictedItem object
|
||||
* output Example -> DepictedItem depictedItem=new DepictedItem ()
|
||||
* input Example -> string
|
||||
*/
|
||||
@TypeConverter
|
||||
@JvmStatic
|
||||
fun stringToDepicts(objectList: String?): DepictedItem? {
|
||||
return readObjectWithTypeToken(objectList, object : TypeToken<DepictedItem>() {})
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
@JvmStatic
|
||||
fun fromTimestamp(value: Long?): Date? {
|
||||
return value?.let { Date(it) }
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
@JvmStatic
|
||||
fun dateToTimestamp(date: Date?): Long? {
|
||||
return date?.time
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
@JvmStatic
|
||||
fun fromString(value: String?): Uri? {
|
||||
return value?.let { Uri.parse(it) }
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
@JvmStatic
|
||||
fun uriToString(uri: Uri?): String? {
|
||||
return uri?.toString()
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
@JvmStatic
|
||||
fun listObjectToString(objectList: List<String>?): String? {
|
||||
return writeObjectToString(objectList)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
@JvmStatic
|
||||
fun stringToListObject(objectList: String?): List<String>? {
|
||||
return readObjectWithTypeToken(objectList, object : TypeToken<List<String>>() {})
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
@JvmStatic
|
||||
fun mapObjectToString(objectList: Map<String, String>?): String? {
|
||||
return writeObjectToString(objectList)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
@JvmStatic
|
||||
fun mapObjectToString2(objectList: Map<String, Boolean>?): String? {
|
||||
return writeObjectToString(objectList)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
@JvmStatic
|
||||
fun stringToMap(objectList: String?): Map<String, String>? {
|
||||
return readObjectWithTypeToken(objectList, object : TypeToken<Map<String, String>>() {})
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
@JvmStatic
|
||||
fun stringToMap2(objectList: String?): Map<String, Boolean>? {
|
||||
return readObjectWithTypeToken(objectList, object : TypeToken<Map<String, Boolean>>() {})
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
@JvmStatic
|
||||
fun latlngObjectToString(latlng: LatLng?): String? {
|
||||
return writeObjectToString(latlng)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
@JvmStatic
|
||||
fun stringToLatLng(objectList: String?): LatLng? {
|
||||
return readObjectFromString(objectList, LatLng::class.java)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
@JvmStatic
|
||||
fun wikidataPlaceToString(wikidataPlace: WikidataPlace?): String? {
|
||||
return writeObjectToString(wikidataPlace)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
@JvmStatic
|
||||
fun stringToWikidataPlace(wikidataPlace: String?): WikidataPlace? {
|
||||
return readObjectFromString(wikidataPlace, WikidataPlace::class.java)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
@JvmStatic
|
||||
fun chunkInfoToString(chunkInfo: ChunkInfo?): String? {
|
||||
return writeObjectToString(chunkInfo)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
@JvmStatic
|
||||
fun stringToChunkInfo(chunkInfo: String?): ChunkInfo? {
|
||||
return readObjectFromString(chunkInfo, ChunkInfo::class.java)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
@JvmStatic
|
||||
fun depictionListToString(depictedItems: List<DepictedItem>?): String? {
|
||||
return writeObjectToString(depictedItems)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
@JvmStatic
|
||||
fun stringToList(depictedItems: String?): List<DepictedItem>? {
|
||||
return readObjectWithTypeToken(depictedItems, object : TypeToken<List<DepictedItem>>() {})
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
@JvmStatic
|
||||
fun sitelinksFromString(value: String?): Sitelinks? {
|
||||
val type = object : TypeToken<Sitelinks>() {}.type
|
||||
return Gson().fromJson(value, type)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
@JvmStatic
|
||||
fun fromSitelinks(sitelinks: Sitelinks?): String? {
|
||||
return Gson().toJson(sitelinks)
|
||||
}
|
||||
|
||||
private fun writeObjectToString(`object`: Any?): String? {
|
||||
return `object`?.let { getGson().toJson(it) }
|
||||
}
|
||||
|
||||
private fun <T> readObjectFromString(objectAsString: String?, clazz: Class<T>): T? {
|
||||
return objectAsString?.let { getGson().fromJson(it, clazz) }
|
||||
}
|
||||
|
||||
private fun <T> readObjectWithTypeToken(objectList: String?, typeToken: TypeToken<T>): T? {
|
||||
return objectList?.let { getGson().fromJson(it, typeToken.type) }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,277 +0,0 @@
|
|||
package fr.free.nrw.commons.delete;
|
||||
|
||||
import static fr.free.nrw.commons.notification.NotificationHelper.NOTIFICATION_DELETE;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import static fr.free.nrw.commons.utils.LangCodeUtils.getLocalizedResources;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import fr.free.nrw.commons.BuildConfig;
|
||||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.actions.PageEditClient;
|
||||
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException;
|
||||
import fr.free.nrw.commons.notification.NotificationHelper;
|
||||
import fr.free.nrw.commons.review.ReviewController;
|
||||
import fr.free.nrw.commons.utils.ViewUtilWrapper;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.SingleSource;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.Callable;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* Refactored async task to Rx
|
||||
*/
|
||||
@Singleton
|
||||
public class DeleteHelper {
|
||||
private final NotificationHelper notificationHelper;
|
||||
private final PageEditClient pageEditClient;
|
||||
private final ViewUtilWrapper viewUtil;
|
||||
private final String username;
|
||||
private AlertDialog d;
|
||||
private DialogInterface.OnMultiChoiceClickListener listener;
|
||||
|
||||
@Inject
|
||||
public DeleteHelper(NotificationHelper notificationHelper,
|
||||
@Named("commons-page-edit") PageEditClient pageEditClient,
|
||||
ViewUtilWrapper viewUtil,
|
||||
@Named("username") String username) {
|
||||
this.notificationHelper = notificationHelper;
|
||||
this.pageEditClient = pageEditClient;
|
||||
this.viewUtil = viewUtil;
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
/**
|
||||
* Public interface to nominate a particular media file for deletion
|
||||
* @param context
|
||||
* @param media
|
||||
* @param reason
|
||||
* @return
|
||||
*/
|
||||
public Single<Boolean> makeDeletion(Context context, Media media, String reason) {
|
||||
viewUtil.showShortToast(context, "Trying to nominate " + media.getDisplayTitle() + " for deletion");
|
||||
|
||||
return delete(media, reason)
|
||||
.flatMapSingle(result -> Single.just(showDeletionNotification(context, media, result)))
|
||||
.firstOrError()
|
||||
.onErrorResumeNext(throwable -> {
|
||||
if (throwable instanceof InvalidLoginTokenException) {
|
||||
return Single.error(throwable);
|
||||
}
|
||||
return Single.error(throwable);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes several API calls to nominate the file for deletion
|
||||
* @param media
|
||||
* @param reason
|
||||
* @return
|
||||
*/
|
||||
private Observable<Boolean> delete(Media media, String reason) {
|
||||
Timber.d("thread is delete %s", Thread.currentThread().getName());
|
||||
String summary = "Nominating " + media.getFilename() + " for deletion.";
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
String fileDeleteString = "{{delete|reason=" + reason +
|
||||
"|subpage=" + media.getFilename() +
|
||||
"|day=" + calendar.get(Calendar.DAY_OF_MONTH) +
|
||||
"|month=" + calendar.getDisplayName(Calendar.MONTH, Calendar.LONG, Locale.ENGLISH) +
|
||||
"|year=" + calendar.get(Calendar.YEAR) +
|
||||
"}}";
|
||||
|
||||
String subpageString = "=== [[:" + media.getFilename() + "]] ===\n" +
|
||||
reason +
|
||||
" ~~~~";
|
||||
|
||||
String logPageString = "\n{{Commons:Deletion requests/" + media.getFilename() +
|
||||
"}}\n";
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd", Locale.getDefault());
|
||||
String date = sdf.format(calendar.getTime());
|
||||
|
||||
String userPageString = "\n{{subst:idw|" + media.getFilename() +
|
||||
"}} ~~~~";
|
||||
|
||||
String creator = media.getAuthor();
|
||||
if (creator == null || creator.isEmpty()) {
|
||||
throw new RuntimeException("Failed to nominate for deletion");
|
||||
}
|
||||
|
||||
return pageEditClient.prependEdit(media.getFilename(), fileDeleteString + "\n", summary)
|
||||
.onErrorResumeNext(throwable -> {
|
||||
if (throwable instanceof InvalidLoginTokenException) {
|
||||
return Observable.error(throwable);
|
||||
}
|
||||
return Observable.error(throwable);
|
||||
})
|
||||
.flatMap(result -> {
|
||||
if (result) {
|
||||
return pageEditClient.edit("Commons:Deletion_requests/" + media.getFilename(), subpageString + "\n", summary);
|
||||
}
|
||||
return Observable.error(new RuntimeException("Failed to nominate for deletion"));
|
||||
})
|
||||
.flatMap(result -> {
|
||||
if (result) {
|
||||
return pageEditClient.appendEdit("Commons:Deletion_requests/" + date, logPageString + "\n", summary);
|
||||
}
|
||||
return Observable.error(new RuntimeException("Failed to nominate for deletion"));
|
||||
})
|
||||
.flatMap(result -> {
|
||||
if (result) {
|
||||
return pageEditClient.appendEdit("User_Talk:" + creator, userPageString + "\n", summary);
|
||||
}
|
||||
return Observable.error(new RuntimeException("Failed to nominate for deletion"));
|
||||
});
|
||||
}
|
||||
|
||||
private boolean showDeletionNotification(Context context, Media media, boolean result) {
|
||||
String message;
|
||||
String title = context.getString(R.string.delete_helper_show_deletion_title);
|
||||
|
||||
if (result) {
|
||||
title += ": " + context.getString(R.string.delete_helper_show_deletion_title_success);
|
||||
message = context.getString((R.string.delete_helper_show_deletion_message_if),media.getDisplayTitle());
|
||||
} else {
|
||||
title += ": " + context.getString(R.string.delete_helper_show_deletion_title_failed);
|
||||
message = context.getString(R.string.delete_helper_show_deletion_message_else) ;
|
||||
}
|
||||
|
||||
String urlForDelete = BuildConfig.COMMONS_URL + "/wiki/Commons:Deletion_requests/" + media.getFilename();
|
||||
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(urlForDelete));
|
||||
notificationHelper.showNotification(context, title, message, NOTIFICATION_DELETE, browserIntent);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a reason needs to be asked before nominating for deletion
|
||||
* @param media
|
||||
* @param context
|
||||
* @param question
|
||||
* @param problem
|
||||
*/
|
||||
@SuppressLint("CheckResult")
|
||||
public void askReasonAndExecute(Media media,
|
||||
Context context,
|
||||
String question,
|
||||
ReviewController.DeleteReason problem,
|
||||
ReviewController.ReviewCallback reviewCallback) {
|
||||
AlertDialog.Builder alert = new AlertDialog.Builder(context);
|
||||
alert.setTitle(question);
|
||||
|
||||
boolean[] checkedItems = {false, false, false, false};
|
||||
ArrayList<Integer> mUserReason = new ArrayList<>();
|
||||
|
||||
final String[] reasonList;
|
||||
final String[] reasonListEnglish;
|
||||
|
||||
if (problem == ReviewController.DeleteReason.SPAM) {
|
||||
reasonList = new String[] {
|
||||
context.getString(R.string.delete_helper_ask_spam_selfie),
|
||||
context.getString(R.string.delete_helper_ask_spam_blurry),
|
||||
context.getString(R.string.delete_helper_ask_spam_nonsense)
|
||||
};
|
||||
reasonListEnglish = new String[] {
|
||||
getLocalizedResources(context, Locale.ENGLISH).getString(R.string.delete_helper_ask_spam_selfie),
|
||||
getLocalizedResources(context, Locale.ENGLISH).getString(R.string.delete_helper_ask_spam_blurry),
|
||||
getLocalizedResources(context, Locale.ENGLISH).getString(R.string.delete_helper_ask_spam_nonsense)
|
||||
};
|
||||
} else if (problem == ReviewController.DeleteReason.COPYRIGHT_VIOLATION) {
|
||||
reasonList = new String[] {
|
||||
context.getString(R.string.delete_helper_ask_reason_copyright_press_photo),
|
||||
context.getString(R.string.delete_helper_ask_reason_copyright_internet_photo),
|
||||
context.getString(R.string.delete_helper_ask_reason_copyright_logo),
|
||||
context.getString(R.string.delete_helper_ask_reason_copyright_no_freedom_of_panorama)
|
||||
};
|
||||
reasonListEnglish = new String[] {
|
||||
getLocalizedResources(context, Locale.ENGLISH).getString(R.string.delete_helper_ask_reason_copyright_press_photo),
|
||||
getLocalizedResources(context, Locale.ENGLISH).getString(R.string.delete_helper_ask_reason_copyright_internet_photo),
|
||||
getLocalizedResources(context, Locale.ENGLISH).getString(R.string.delete_helper_ask_reason_copyright_logo),
|
||||
getLocalizedResources(context, Locale.ENGLISH).getString(R.string.delete_helper_ask_reason_copyright_no_freedom_of_panorama)
|
||||
};
|
||||
} else {
|
||||
reasonList = new String[] {};
|
||||
reasonListEnglish = new String[] {};
|
||||
}
|
||||
|
||||
alert.setMultiChoiceItems(reasonList, checkedItems, listener = (dialogInterface, position, isChecked) -> {
|
||||
|
||||
if (isChecked) {
|
||||
mUserReason.add(position);
|
||||
} else {
|
||||
mUserReason.remove((Integer.valueOf(position)));
|
||||
}
|
||||
|
||||
// disable the OK button if no reason selected
|
||||
((AlertDialog) dialogInterface).getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(
|
||||
!mUserReason.isEmpty());
|
||||
});
|
||||
|
||||
alert.setPositiveButton(context.getString(R.string.ok), (dialogInterface, i) -> {
|
||||
reviewCallback.disableButtons();
|
||||
|
||||
|
||||
String reason = getLocalizedResources(context, Locale.ENGLISH).getString(R.string.delete_helper_ask_alert_set_positive_button_reason) + " ";
|
||||
|
||||
for (int j = 0; j < mUserReason.size(); j++) {
|
||||
reason = reason + reasonListEnglish[mUserReason.get(j)];
|
||||
if (j != mUserReason.size() - 1) {
|
||||
reason = reason + ", ";
|
||||
}
|
||||
}
|
||||
|
||||
Timber.d("thread is askReasonAndExecute %s", Thread.currentThread().getName());
|
||||
|
||||
String finalReason = reason;
|
||||
|
||||
Single.defer((Callable<SingleSource<Boolean>>) () ->
|
||||
makeDeletion(context, media, finalReason))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(aBoolean -> {
|
||||
reviewCallback.onSuccess();
|
||||
}, throwable -> {
|
||||
if (throwable instanceof InvalidLoginTokenException) {
|
||||
reviewCallback.onTokenException((InvalidLoginTokenException) throwable);
|
||||
} else {
|
||||
reviewCallback.onFailure();
|
||||
}
|
||||
reviewCallback.enableButtons();
|
||||
});
|
||||
});
|
||||
alert.setNegativeButton(context.getString(R.string.cancel), (dialog, which) -> reviewCallback.onFailure());
|
||||
d = alert.create();
|
||||
d.show();
|
||||
|
||||
// disable the OK button by default
|
||||
d.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the instance of shown AlertDialog,
|
||||
* used for taking reference during unit test
|
||||
* */
|
||||
public AlertDialog getDialog(){
|
||||
return d;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the instance of shown DialogInterface.OnMultiChoiceClickListener,
|
||||
* used for taking reference during unit test
|
||||
* */
|
||||
public DialogInterface.OnMultiChoiceClickListener getListener(){
|
||||
return listener;
|
||||
}
|
||||
}
|
||||
334
app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.kt
Normal file
334
app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.kt
Normal file
|
|
@ -0,0 +1,334 @@
|
|||
package fr.free.nrw.commons.delete
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import fr.free.nrw.commons.BuildConfig
|
||||
import fr.free.nrw.commons.Media
|
||||
import fr.free.nrw.commons.R
|
||||
import fr.free.nrw.commons.actions.PageEditClient
|
||||
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException
|
||||
import fr.free.nrw.commons.notification.NotificationHelper
|
||||
import fr.free.nrw.commons.notification.NotificationHelper.Companion.NOTIFICATION_DELETE
|
||||
import fr.free.nrw.commons.review.ReviewController
|
||||
import fr.free.nrw.commons.utils.LangCodeUtils.getLocalizedResources
|
||||
import fr.free.nrw.commons.utils.ViewUtilWrapper
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import timber.log.Timber
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Refactored async task to Rx
|
||||
*/
|
||||
@Singleton
|
||||
class DeleteHelper @Inject constructor(
|
||||
private val notificationHelper: NotificationHelper,
|
||||
@Named("commons-page-edit") private val pageEditClient: PageEditClient,
|
||||
private val viewUtil: ViewUtilWrapper,
|
||||
@Named("username") private val username: String
|
||||
) {
|
||||
private var d: AlertDialog? = null
|
||||
private var listener: DialogInterface.OnMultiChoiceClickListener? = null
|
||||
|
||||
/**
|
||||
* Public interface to nominate a particular media file for deletion
|
||||
* @param context
|
||||
* @param media
|
||||
* @param reason
|
||||
* @return
|
||||
*/
|
||||
fun makeDeletion(
|
||||
context: Context?,
|
||||
media: Media?,
|
||||
reason: String?
|
||||
): Single<Boolean>? {
|
||||
|
||||
if(context == null && media == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
viewUtil.showShortToast(
|
||||
context!!,
|
||||
"Trying to nominate ${media?.displayTitle} for deletion"
|
||||
)
|
||||
|
||||
return reason?.let {
|
||||
delete(media!!, it)
|
||||
.flatMapSingle { result ->
|
||||
Single.just(showDeletionNotification(context, media, result))
|
||||
}
|
||||
.firstOrError()
|
||||
.onErrorResumeNext { throwable ->
|
||||
if (throwable is InvalidLoginTokenException) {
|
||||
Single.error(throwable)
|
||||
} else {
|
||||
Single.error(throwable)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes several API calls to nominate the file for deletion
|
||||
* @param media
|
||||
* @param reason
|
||||
* @return
|
||||
*/
|
||||
private fun delete(media: Media, reason: String): Observable<Boolean> {
|
||||
Timber.d("thread is delete %s", Thread.currentThread().name)
|
||||
val summary = "Nominating ${media.filename} for deletion."
|
||||
val calendar = Calendar.getInstance()
|
||||
val fileDeleteString = """
|
||||
{{delete|reason=$reason|subpage=${media.filename}|day=
|
||||
${calendar.get(Calendar.DAY_OF_MONTH)}|month=${
|
||||
calendar.getDisplayName(
|
||||
Calendar.MONTH,
|
||||
Calendar.LONG,
|
||||
Locale.ENGLISH
|
||||
)
|
||||
}|year=${calendar.get(Calendar.YEAR)}}}
|
||||
""".trimIndent()
|
||||
|
||||
val subpageString = """
|
||||
=== [[:${media.filename}]] ===
|
||||
$reason ~~~~
|
||||
""".trimIndent()
|
||||
|
||||
val logPageString = "\n{{Commons:Deletion requests/${media.filename}}}\n"
|
||||
val sdf = SimpleDateFormat("yyyy/MM/dd", Locale.getDefault())
|
||||
val date = sdf.format(calendar.time)
|
||||
|
||||
val userPageString = "\n{{subst:idw|${media.filename}}} ~~~~"
|
||||
|
||||
val creator = media.author
|
||||
?: throw RuntimeException("Failed to nominate for deletion")
|
||||
|
||||
return pageEditClient.prependEdit(
|
||||
media.filename!!,
|
||||
"$fileDeleteString\n",
|
||||
summary
|
||||
)
|
||||
.onErrorResumeNext { throwable: Throwable ->
|
||||
if (throwable is InvalidLoginTokenException) {
|
||||
Observable.error(throwable)
|
||||
} else {
|
||||
Observable.error(throwable)
|
||||
}
|
||||
}
|
||||
.flatMap { result: Boolean ->
|
||||
if (result) {
|
||||
pageEditClient.edit(
|
||||
"Commons:Deletion_requests/${media.filename}",
|
||||
"$subpageString\n",
|
||||
summary
|
||||
)
|
||||
} else {
|
||||
Observable.error(RuntimeException("Failed to nominate for deletion"))
|
||||
}
|
||||
}
|
||||
.flatMap { result: Boolean ->
|
||||
if (result) {
|
||||
pageEditClient.appendEdit(
|
||||
"Commons:Deletion_requests/$date",
|
||||
"$logPageString\n",
|
||||
summary
|
||||
)
|
||||
} else {
|
||||
Observable.error(RuntimeException("Failed to nominate for deletion"))
|
||||
}
|
||||
}
|
||||
.flatMap { result: Boolean ->
|
||||
if (result) {
|
||||
pageEditClient.appendEdit("User_Talk:$creator", "$userPageString\n", summary)
|
||||
} else {
|
||||
Observable.error(RuntimeException("Failed to nominate for deletion"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("StringFormatInvalid")
|
||||
private fun showDeletionNotification(
|
||||
context: Context,
|
||||
media: Media,
|
||||
result: Boolean
|
||||
): Boolean {
|
||||
val title: String
|
||||
val message: String
|
||||
var baseTitle = context.getString(R.string.delete_helper_show_deletion_title)
|
||||
|
||||
if (result) {
|
||||
baseTitle += ": ${
|
||||
context.getString(R.string.delete_helper_show_deletion_title_success)
|
||||
}"
|
||||
title = baseTitle
|
||||
message = context
|
||||
.getString(R.string.delete_helper_show_deletion_message_if, media.displayTitle)
|
||||
} else {
|
||||
baseTitle += ": ${context.getString(R.string.delete_helper_show_deletion_title_failed)}"
|
||||
title = baseTitle
|
||||
message = context.getString(R.string.delete_helper_show_deletion_message_else)
|
||||
}
|
||||
|
||||
val urlForDelete = "${BuildConfig.COMMONS_URL}/wiki/Commons:Deletion_requests/${
|
||||
media.filename
|
||||
}"
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(urlForDelete))
|
||||
notificationHelper
|
||||
.showNotification(context, title, message, NOTIFICATION_DELETE, browserIntent)
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a reason needs to be asked before nominating for deletion
|
||||
* @param media
|
||||
* @param context
|
||||
* @param question
|
||||
* @param problem
|
||||
*/
|
||||
@SuppressLint("CheckResult")
|
||||
fun askReasonAndExecute(
|
||||
media: Media?,
|
||||
context: Context,
|
||||
question: String,
|
||||
problem: ReviewController.DeleteReason,
|
||||
reviewCallback: ReviewController.ReviewCallback
|
||||
) {
|
||||
val alert = AlertDialog.Builder(context)
|
||||
alert.setCancelable(false)
|
||||
alert.setTitle(question)
|
||||
|
||||
val checkedItems = booleanArrayOf(false, false, false, false)
|
||||
val mUserReason = arrayListOf<Int>()
|
||||
|
||||
val reasonList: Array<String>
|
||||
val reasonListEnglish: Array<String>
|
||||
|
||||
when (problem) {
|
||||
ReviewController.DeleteReason.SPAM -> {
|
||||
reasonList = arrayOf(
|
||||
context.getString(R.string.delete_helper_ask_spam_selfie),
|
||||
context.getString(R.string.delete_helper_ask_spam_blurry),
|
||||
context.getString(R.string.delete_helper_ask_spam_nonsense)
|
||||
)
|
||||
reasonListEnglish = arrayOf(
|
||||
getLocalizedResources(context, Locale.ENGLISH)
|
||||
.getString(R.string.delete_helper_ask_spam_selfie),
|
||||
getLocalizedResources(context, Locale.ENGLISH)
|
||||
.getString(R.string.delete_helper_ask_spam_blurry),
|
||||
getLocalizedResources(context, Locale.ENGLISH)
|
||||
.getString(R.string.delete_helper_ask_spam_nonsense)
|
||||
)
|
||||
}
|
||||
ReviewController.DeleteReason.COPYRIGHT_VIOLATION -> {
|
||||
reasonList = arrayOf(
|
||||
context.getString(R.string.delete_helper_ask_reason_copyright_press_photo),
|
||||
context.getString(R.string.delete_helper_ask_reason_copyright_internet_photo),
|
||||
context.getString(R.string.delete_helper_ask_reason_copyright_logo),
|
||||
context.getString(
|
||||
R.string.delete_helper_ask_reason_copyright_no_freedom_of_panorama
|
||||
)
|
||||
)
|
||||
reasonListEnglish = arrayOf(
|
||||
getLocalizedResources(context, Locale.ENGLISH)
|
||||
.getString(R.string.delete_helper_ask_reason_copyright_press_photo),
|
||||
getLocalizedResources(context, Locale.ENGLISH)
|
||||
.getString(R.string.delete_helper_ask_reason_copyright_internet_photo),
|
||||
getLocalizedResources(context, Locale.ENGLISH)
|
||||
.getString(R.string.delete_helper_ask_reason_copyright_logo),
|
||||
getLocalizedResources(context, Locale.ENGLISH)
|
||||
.getString(
|
||||
R.string.delete_helper_ask_reason_copyright_no_freedom_of_panorama
|
||||
)
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
reasonList = emptyArray()
|
||||
reasonListEnglish = emptyArray()
|
||||
}
|
||||
}
|
||||
|
||||
alert.setMultiChoiceItems(
|
||||
reasonList,
|
||||
checkedItems
|
||||
) { dialogInterface, position, isChecked ->
|
||||
if (isChecked) {
|
||||
mUserReason.add(position)
|
||||
} else {
|
||||
mUserReason.remove(position)
|
||||
}
|
||||
|
||||
// Safely enable or disable the OK button based on selection
|
||||
val dialog = dialogInterface as? AlertDialog
|
||||
dialog?.getButton(AlertDialog.BUTTON_POSITIVE)?.isEnabled = mUserReason.isNotEmpty()
|
||||
}
|
||||
|
||||
alert.setPositiveButton(context.getString(R.string.ok)) { _, _ ->
|
||||
reviewCallback.disableButtons()
|
||||
|
||||
val reason = buildString {
|
||||
append(
|
||||
getLocalizedResources(context, Locale.ENGLISH)
|
||||
.getString(R.string.delete_helper_ask_alert_set_positive_button_reason)
|
||||
)
|
||||
append(" ")
|
||||
|
||||
mUserReason.forEachIndexed { index, position ->
|
||||
append(reasonListEnglish[position])
|
||||
if (index != mUserReason.lastIndex) {
|
||||
append(", ")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timber.d("thread is askReasonAndExecute %s", Thread.currentThread().name)
|
||||
|
||||
if (media != null) {
|
||||
Single.defer { makeDeletion(context, media, reason) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({ reviewCallback.onSuccess() }, { throwable ->
|
||||
when (throwable) {
|
||||
is InvalidLoginTokenException ->
|
||||
reviewCallback.onTokenException(throwable)
|
||||
else -> reviewCallback.onFailure()
|
||||
}
|
||||
reviewCallback.enableButtons()
|
||||
})
|
||||
}
|
||||
}
|
||||
alert.setNegativeButton(
|
||||
context.getString(R.string.cancel)
|
||||
) { _, _ -> reviewCallback.onFailure() }
|
||||
|
||||
d = alert.create()
|
||||
d?.setOnShowListener {
|
||||
// Safely initialize the OK button state after the dialog is fully shown
|
||||
d?.getButton(AlertDialog.BUTTON_POSITIVE)?.isEnabled = false
|
||||
}
|
||||
d?.show()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* returns the instance of shown AlertDialog,
|
||||
* used for taking reference during unit test
|
||||
*/
|
||||
fun getDialog(): AlertDialog? = d
|
||||
|
||||
/**
|
||||
* returns the instance of shown DialogInterface.OnMultiChoiceClickListener,
|
||||
* used for taking reference during unit test
|
||||
*/
|
||||
fun getListener(): DialogInterface.OnMultiChoiceClickListener? = listener
|
||||
}
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
package fr.free.nrw.commons.delete;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import fr.free.nrw.commons.utils.DateUtil;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.profile.achievements.FeedbackResponse;
|
||||
import fr.free.nrw.commons.auth.SessionManager;
|
||||
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
|
||||
import fr.free.nrw.commons.utils.ViewUtilWrapper;
|
||||
import io.reactivex.Single;
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* This class handles the reason for deleting a Media object
|
||||
*/
|
||||
@Singleton
|
||||
public class ReasonBuilder {
|
||||
|
||||
private SessionManager sessionManager;
|
||||
private OkHttpJsonApiClient okHttpJsonApiClient;
|
||||
private Context context;
|
||||
private ViewUtilWrapper viewUtilWrapper;
|
||||
|
||||
@Inject
|
||||
public ReasonBuilder(Context context,
|
||||
SessionManager sessionManager,
|
||||
OkHttpJsonApiClient okHttpJsonApiClient,
|
||||
ViewUtilWrapper viewUtilWrapper) {
|
||||
this.context = context;
|
||||
this.sessionManager = sessionManager;
|
||||
this.okHttpJsonApiClient = okHttpJsonApiClient;
|
||||
this.viewUtilWrapper = viewUtilWrapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* To process the reason and append the media's upload date and uploaded_by_me string
|
||||
* @param media
|
||||
* @param reason
|
||||
* @return
|
||||
*/
|
||||
public Single<String> getReason(Media media, String reason) {
|
||||
return fetchArticleNumber(media, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* get upload date for the passed Media
|
||||
*/
|
||||
private String prettyUploadedDate(Media media) {
|
||||
Date date = media.getDateUploaded();
|
||||
if (date == null || date.toString() == null || date.toString().isEmpty()) {
|
||||
return "Uploaded date not available";
|
||||
}
|
||||
return DateUtil.getDateStringWithSkeletonPattern(date,"dd MMM yyyy");
|
||||
}
|
||||
|
||||
private Single<String> fetchArticleNumber(Media media, String reason) {
|
||||
if (checkAccount()) {
|
||||
return okHttpJsonApiClient
|
||||
.getAchievements(sessionManager.getUserName())
|
||||
.map(feedbackResponse -> appendArticlesUsed(feedbackResponse, media, reason));
|
||||
}
|
||||
return Single.just("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes the uploaded_by_me string, the upload date, name of articles using images
|
||||
* and appends it to the received reason
|
||||
* @param feedBack object
|
||||
* @param media whose upload data is to be fetched
|
||||
* @param reason
|
||||
*/
|
||||
private String appendArticlesUsed(FeedbackResponse feedBack, Media media, String reason) {
|
||||
String reason1Template = context.getString(R.string.uploaded_by_myself);
|
||||
reason += String.format(Locale.getDefault(), reason1Template, prettyUploadedDate(media), feedBack.getArticlesUsingImages());
|
||||
Timber.i("New Reason %s", reason);
|
||||
return reason;
|
||||
}
|
||||
|
||||
/**
|
||||
* check to ensure that user is logged in
|
||||
* @return
|
||||
*/
|
||||
private boolean checkAccount(){
|
||||
if (!sessionManager.doesAccountExist()) {
|
||||
Timber.d("Current account is null");
|
||||
viewUtilWrapper.showLongToast(context, context.getResources().getString(R.string.user_not_logged_in));
|
||||
sessionManager.forceLogin(context);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
package fr.free.nrw.commons.delete
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
|
||||
import fr.free.nrw.commons.utils.DateUtil
|
||||
import java.util.Locale
|
||||
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
import fr.free.nrw.commons.Media
|
||||
import fr.free.nrw.commons.R
|
||||
import fr.free.nrw.commons.profile.achievements.FeedbackResponse
|
||||
import fr.free.nrw.commons.auth.SessionManager
|
||||
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
|
||||
import fr.free.nrw.commons.utils.ViewUtilWrapper
|
||||
import io.reactivex.Single
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* This class handles the reason for deleting a Media object
|
||||
*/
|
||||
@Singleton
|
||||
class ReasonBuilder @Inject constructor(
|
||||
private val context: Context,
|
||||
private val sessionManager: SessionManager,
|
||||
private val okHttpJsonApiClient: OkHttpJsonApiClient,
|
||||
private val viewUtilWrapper: ViewUtilWrapper
|
||||
) {
|
||||
|
||||
/**
|
||||
* To process the reason and append the media's upload date and uploaded_by_me string
|
||||
* @param media
|
||||
* @param reason
|
||||
* @return
|
||||
*/
|
||||
fun getReason(media: Media?, reason: String?): Single<String> {
|
||||
if (media == null || reason == null) {
|
||||
return Single.just("Not known")
|
||||
}
|
||||
return fetchArticleNumber(media, reason)
|
||||
}
|
||||
|
||||
/**
|
||||
* get upload date for the passed Media
|
||||
*/
|
||||
private fun prettyUploadedDate(media: Media): String {
|
||||
val date = media.dateUploaded
|
||||
return if (date == null || date.toString().isEmpty()) {
|
||||
"Uploaded date not available"
|
||||
} else {
|
||||
DateUtil.getDateStringWithSkeletonPattern(date, "dd MMM yyyy")
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchArticleNumber(media: Media, reason: String): Single<String> {
|
||||
return if (checkAccount()) {
|
||||
okHttpJsonApiClient
|
||||
.getAchievements(sessionManager.userName)
|
||||
.map { feedbackResponse -> appendArticlesUsed(feedbackResponse, media, reason) }
|
||||
} else {
|
||||
Single.just("")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes the uploaded_by_me string, the upload date, name of articles using images
|
||||
* and appends it to the received reason
|
||||
* @param feedBack object
|
||||
* @param media whose upload data is to be fetched
|
||||
* @param reason
|
||||
*/
|
||||
@SuppressLint("StringFormatInvalid")
|
||||
private fun appendArticlesUsed(feedBack: FeedbackResponse, media: Media, reason: String): String {
|
||||
val reason1Template = context.getString(R.string.uploaded_by_myself)
|
||||
return reason + String.format(Locale.getDefault(), reason1Template, prettyUploadedDate(media), feedBack.articlesUsingImages)
|
||||
.also { Timber.i("New Reason %s", it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* check to ensure that user is logged in
|
||||
* @return
|
||||
*/
|
||||
private fun checkAccount(): Boolean {
|
||||
return if (!sessionManager.doesAccountExist()) {
|
||||
Timber.d("Current account is null")
|
||||
viewUtilWrapper.showLongToast(context, context.getString(R.string.user_not_logged_in))
|
||||
sessionManager.forceLogin(context)
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,8 @@ import android.os.Bundle
|
|||
import android.os.Parcelable
|
||||
import android.speech.RecognizerIntent
|
||||
import android.view.View
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import fr.free.nrw.commons.CommonsApplication
|
||||
|
|
@ -70,10 +72,14 @@ class DescriptionEditActivity :
|
|||
|
||||
private lateinit var binding: ActivityDescriptionEditBinding
|
||||
|
||||
private val requestCodeForVoiceInput = 1213
|
||||
|
||||
private var descriptionAndCaptions: ArrayList<UploadMediaDetail>? = null
|
||||
|
||||
private val voiceInputResultLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) { result: ActivityResult ->
|
||||
onVoiceInput(result)
|
||||
}
|
||||
|
||||
@Inject lateinit var descriptionEditHelper: DescriptionEditHelper
|
||||
|
||||
@Inject lateinit var sessionManager: SessionManager
|
||||
|
|
@ -115,6 +121,7 @@ class DescriptionEditActivity :
|
|||
savedLanguageValue,
|
||||
descriptionAndCaptions,
|
||||
recentLanguagesDao,
|
||||
voiceInputResultLauncher
|
||||
)
|
||||
uploadMediaDetailAdapter.setCallback { titleStringID: Int, messageStringId: Int ->
|
||||
showInfoAlert(
|
||||
|
|
@ -142,13 +149,21 @@ class DescriptionEditActivity :
|
|||
getString(titleStringID),
|
||||
getString(messageStringId),
|
||||
getString(android.R.string.ok),
|
||||
null,
|
||||
true,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
override fun onPrimaryCaptionTextChange(isNotEmpty: Boolean) {}
|
||||
|
||||
private fun onVoiceInput(result: ActivityResult) {
|
||||
if (result.resultCode == RESULT_OK && result.data != null) {
|
||||
val resultData = result.data!!.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)
|
||||
uploadMediaDetailAdapter.handleSpeechResult(resultData!![0])
|
||||
} else {
|
||||
Timber.e("Error %s", result.resultCode)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds new language item to RecyclerView
|
||||
*/
|
||||
|
|
@ -221,7 +236,7 @@ class DescriptionEditActivity :
|
|||
) {
|
||||
try {
|
||||
descriptionEditHelper
|
||||
?.addDescription(
|
||||
.addDescription(
|
||||
applicationContext,
|
||||
media,
|
||||
updatedWikiText,
|
||||
|
|
@ -234,7 +249,7 @@ class DescriptionEditActivity :
|
|||
)
|
||||
}
|
||||
} catch (e: InvalidLoginTokenException) {
|
||||
val username: String? = sessionManager?.userName
|
||||
val username: String? = sessionManager.userName
|
||||
val logoutListener =
|
||||
CommonsApplication.BaseLogoutListener(
|
||||
this,
|
||||
|
|
@ -242,7 +257,7 @@ class DescriptionEditActivity :
|
|||
username,
|
||||
)
|
||||
|
||||
val commonsApplication = CommonsApplication.getInstance()
|
||||
val commonsApplication = CommonsApplication.instance
|
||||
if (commonsApplication != null) {
|
||||
commonsApplication.clearApplicationData(this, logoutListener)
|
||||
}
|
||||
|
|
@ -252,11 +267,11 @@ class DescriptionEditActivity :
|
|||
for (mediaDetail in uploadMediaDetails) {
|
||||
try {
|
||||
compositeDisposable.add(
|
||||
descriptionEditHelper!!
|
||||
descriptionEditHelper
|
||||
.addCaption(
|
||||
applicationContext,
|
||||
media,
|
||||
mediaDetail.languageCode,
|
||||
mediaDetail.languageCode!!,
|
||||
mediaDetail.captionText,
|
||||
).subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
|
|
@ -275,7 +290,7 @@ class DescriptionEditActivity :
|
|||
username,
|
||||
)
|
||||
|
||||
val commonsApplication = CommonsApplication.getInstance()
|
||||
val commonsApplication = CommonsApplication.instance
|
||||
if (commonsApplication != null) {
|
||||
commonsApplication.clearApplicationData(this, logoutListener)
|
||||
}
|
||||
|
|
@ -288,26 +303,10 @@ class DescriptionEditActivity :
|
|||
progressDialog!!.isIndeterminate = true
|
||||
progressDialog!!.setTitle(getString(R.string.updating_caption_title))
|
||||
progressDialog!!.setMessage(getString(R.string.updating_caption_message))
|
||||
progressDialog!!.setCanceledOnTouchOutside(false)
|
||||
progressDialog!!.setCancelable(false)
|
||||
progressDialog!!.show()
|
||||
}
|
||||
|
||||
override fun onActivityResult(
|
||||
requestCode: Int,
|
||||
resultCode: Int,
|
||||
data: Intent?,
|
||||
) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
if (requestCode == requestCodeForVoiceInput) {
|
||||
if (resultCode == RESULT_OK && data != null) {
|
||||
val result = data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)
|
||||
uploadMediaDetailAdapter.handleSpeechResult(result!![0])
|
||||
} else {
|
||||
Timber.e("Error %s", resultCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,137 +0,0 @@
|
|||
package fr.free.nrw.commons.description;
|
||||
|
||||
import static fr.free.nrw.commons.notification.NotificationHelper.NOTIFICATION_EDIT_DESCRIPTION;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import fr.free.nrw.commons.BuildConfig;
|
||||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.actions.PageEditClient;
|
||||
import fr.free.nrw.commons.notification.NotificationHelper;
|
||||
import io.reactivex.Single;
|
||||
import java.util.Objects;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* Helper class for edit and update given descriptions and showing notification upgradation
|
||||
*/
|
||||
public class DescriptionEditHelper {
|
||||
|
||||
/**
|
||||
* notificationHelper: helps creating notification
|
||||
*/
|
||||
private final NotificationHelper notificationHelper;
|
||||
/**
|
||||
* * pageEditClient: methods provided by this member posts the edited descriptions
|
||||
* to the Media wiki api
|
||||
*/
|
||||
public final PageEditClient pageEditClient;
|
||||
|
||||
@Inject
|
||||
public DescriptionEditHelper(final NotificationHelper notificationHelper,
|
||||
@Named("commons-page-edit") final PageEditClient pageEditClient) {
|
||||
this.notificationHelper = notificationHelper;
|
||||
this.pageEditClient = pageEditClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces new descriptions
|
||||
*
|
||||
* @param context context
|
||||
* @param media to be added
|
||||
* @param appendText to be added
|
||||
* @return Observable<Boolean>
|
||||
*/
|
||||
public Single<Boolean> addDescription(final Context context, final Media media,
|
||||
final String appendText) {
|
||||
Timber.d("thread is description adding %s", Thread.currentThread().getName());
|
||||
final String summary = "Updating Description";
|
||||
|
||||
return pageEditClient.edit(Objects.requireNonNull(media.getFilename()),
|
||||
appendText, summary)
|
||||
.flatMapSingle(result -> Single.just(showDescriptionEditNotification(context,
|
||||
media, result)))
|
||||
.firstOrError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds new captions
|
||||
*
|
||||
* @param context context
|
||||
* @param media to be added
|
||||
* @param language to be added
|
||||
* @param value to be added
|
||||
* @return Observable<Boolean>
|
||||
*/
|
||||
public Single<Boolean> addCaption(final Context context, final Media media,
|
||||
final String language, final String value) {
|
||||
Timber.d("thread is caption adding %s", Thread.currentThread().getName());
|
||||
final String summary = "Updating Caption";
|
||||
|
||||
return pageEditClient.setCaptions(summary, Objects.requireNonNull(media.getFilename()),
|
||||
language, value)
|
||||
.flatMapSingle(result -> Single.just(showCaptionEditNotification(context,
|
||||
media, result)))
|
||||
.firstOrError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update captions and shows notification about captions update
|
||||
* @param context to be added
|
||||
* @param media to be added
|
||||
* @param result to be added
|
||||
* @return boolean
|
||||
*/
|
||||
private boolean showCaptionEditNotification(final Context context, final Media media,
|
||||
final int result) {
|
||||
final String message;
|
||||
String title = context.getString(R.string.caption_edit_helper_show_edit_title);
|
||||
|
||||
if (result == 1) {
|
||||
title += ": " + context
|
||||
.getString(R.string.coordinates_edit_helper_show_edit_title_success);
|
||||
message = context.getString(R.string.caption_edit_helper_show_edit_message);
|
||||
} else {
|
||||
title += ": " + context.getString(R.string.caption_edit_helper_show_edit_title);
|
||||
message = context.getString(R.string.caption_edit_helper_edit_message_else) ;
|
||||
}
|
||||
|
||||
final String urlForFile = BuildConfig.COMMONS_URL + "/wiki/" + media.getFilename();
|
||||
final Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(urlForFile));
|
||||
notificationHelper.showNotification(context, title, message, NOTIFICATION_EDIT_DESCRIPTION,
|
||||
browserIntent);
|
||||
return result == 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update descriptions and shows notification about descriptions update
|
||||
* @param context to be added
|
||||
* @param media to be added
|
||||
* @param result to be added
|
||||
* @return boolean
|
||||
*/
|
||||
private boolean showDescriptionEditNotification(final Context context, final Media media,
|
||||
final boolean result) {
|
||||
final String message;
|
||||
String title = context.getString(R.string.description_edit_helper_show_edit_title);
|
||||
|
||||
if (result) {
|
||||
title += ": " + context
|
||||
.getString(R.string.coordinates_edit_helper_show_edit_title_success);
|
||||
message = context.getString(R.string.description_edit_helper_show_edit_message);
|
||||
} else {
|
||||
title += ": " + context.getString(R.string.description_edit_helper_show_edit_title);
|
||||
message = context.getString(R.string.description_edit_helper_edit_message_else) ;
|
||||
}
|
||||
|
||||
final String urlForFile = BuildConfig.COMMONS_URL + "/wiki/" + media.getFilename();
|
||||
final Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(urlForFile));
|
||||
notificationHelper.showNotification(context, title, message, NOTIFICATION_EDIT_DESCRIPTION,
|
||||
browserIntent);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
package fr.free.nrw.commons.description
|
||||
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import fr.free.nrw.commons.BuildConfig
|
||||
import fr.free.nrw.commons.Media
|
||||
import fr.free.nrw.commons.R
|
||||
import fr.free.nrw.commons.actions.PageEditClient
|
||||
import fr.free.nrw.commons.notification.NotificationHelper
|
||||
import fr.free.nrw.commons.notification.NotificationHelper.Companion.NOTIFICATION_EDIT_DESCRIPTION
|
||||
import io.reactivex.Single
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Helper class for edit and update given descriptions and showing notification upgradation
|
||||
*/
|
||||
class DescriptionEditHelper @Inject constructor(
|
||||
/**
|
||||
* notificationHelper: helps creating notification
|
||||
*/
|
||||
private val notificationHelper: NotificationHelper,
|
||||
|
||||
/**
|
||||
* pageEditClient: methods provided by this member posts the edited descriptions
|
||||
* to the Media wiki api
|
||||
*/
|
||||
@Named("commons-page-edit") val pageEditClient: PageEditClient
|
||||
) {
|
||||
|
||||
/**
|
||||
* Replaces new descriptions
|
||||
*
|
||||
* @param context context
|
||||
* @param media to be added
|
||||
* @param appendText to be added
|
||||
* @return Single<Boolean>
|
||||
*/
|
||||
fun addDescription(context: Context, media: Media, appendText: String): Single<Boolean> {
|
||||
Timber.d("thread is description adding %s", Thread.currentThread().name)
|
||||
val summary = "Updating Description"
|
||||
|
||||
return pageEditClient.edit(
|
||||
requireNotNull(media.filename),
|
||||
appendText,
|
||||
summary
|
||||
).flatMapSingle { result ->
|
||||
Single.just(showDescriptionEditNotification(context, media, result))
|
||||
}.firstOrError()
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds new captions
|
||||
*
|
||||
* @param context context
|
||||
* @param media to be added
|
||||
* @param language to be added
|
||||
* @param value to be added
|
||||
* @return Single<Boolean>
|
||||
*/
|
||||
fun addCaption(
|
||||
context: Context,
|
||||
media: Media,
|
||||
language: String,
|
||||
value: String
|
||||
): Single<Boolean> {
|
||||
Timber.d("thread is caption adding %s", Thread.currentThread().name)
|
||||
val summary = "Updating Caption"
|
||||
|
||||
return pageEditClient.setCaptions(
|
||||
summary,
|
||||
requireNotNull(media.filename),
|
||||
language,
|
||||
value
|
||||
).flatMapSingle { result ->
|
||||
Single.just(showCaptionEditNotification(context, media, result))
|
||||
}.firstOrError()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update captions and shows notification about captions update
|
||||
* @param context to be added
|
||||
* @param media to be added
|
||||
* @param result to be added
|
||||
* @return boolean
|
||||
*/
|
||||
private fun showCaptionEditNotification(context: Context, media: Media, result: Int): Boolean {
|
||||
val message: String
|
||||
var title = context.getString(R.string.caption_edit_helper_show_edit_title)
|
||||
|
||||
if (result == 1) {
|
||||
title += ": " + context.getString(
|
||||
R.string.coordinates_edit_helper_show_edit_title_success
|
||||
)
|
||||
message = context.getString(R.string.caption_edit_helper_show_edit_message)
|
||||
} else {
|
||||
title += ": " + context.getString(R.string.caption_edit_helper_show_edit_title)
|
||||
message = context.getString(R.string.caption_edit_helper_edit_message_else)
|
||||
}
|
||||
|
||||
val urlForFile = "${BuildConfig.COMMONS_URL}/wiki/${media.filename}"
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(urlForFile))
|
||||
notificationHelper.showNotification(
|
||||
context,
|
||||
title,
|
||||
message,
|
||||
NOTIFICATION_EDIT_DESCRIPTION,
|
||||
browserIntent
|
||||
)
|
||||
return result == 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Update descriptions and shows notification about descriptions update
|
||||
* @param context to be added
|
||||
* @param media to be added
|
||||
* @param result to be added
|
||||
* @return boolean
|
||||
*/
|
||||
private fun showDescriptionEditNotification(
|
||||
context: Context,
|
||||
media: Media,
|
||||
result: Boolean
|
||||
): Boolean {
|
||||
val message: String
|
||||
var title= context.getString(
|
||||
R.string.description_edit_helper_show_edit_title
|
||||
)
|
||||
|
||||
if (result) {
|
||||
title += ": " + context.getString(
|
||||
R.string.coordinates_edit_helper_show_edit_title_success
|
||||
)
|
||||
message = context.getString(R.string.description_edit_helper_show_edit_message)
|
||||
} else {
|
||||
title += ": " + context.getString(R.string.description_edit_helper_show_edit_title)
|
||||
message = context.getString(R.string.description_edit_helper_edit_message_else)
|
||||
}
|
||||
|
||||
val urlForFile = "${BuildConfig.COMMONS_URL}/wiki/${media.filename}"
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(urlForFile))
|
||||
notificationHelper.showNotification(
|
||||
context,
|
||||
title,
|
||||
message,
|
||||
NOTIFICATION_EDIT_DESCRIPTION,
|
||||
browserIntent
|
||||
)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
package fr.free.nrw.commons.di;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.android.ContributesAndroidInjector;
|
||||
import fr.free.nrw.commons.AboutActivity;
|
||||
import fr.free.nrw.commons.LocationPicker.LocationPickerActivity;
|
||||
import fr.free.nrw.commons.WelcomeActivity;
|
||||
import fr.free.nrw.commons.auth.LoginActivity;
|
||||
import fr.free.nrw.commons.auth.SignupActivity;
|
||||
import fr.free.nrw.commons.category.CategoryDetailsActivity;
|
||||
import fr.free.nrw.commons.contributions.MainActivity;
|
||||
import fr.free.nrw.commons.customselector.ui.selector.CustomSelectorActivity;
|
||||
import fr.free.nrw.commons.description.DescriptionEditActivity;
|
||||
import fr.free.nrw.commons.explore.depictions.WikidataItemDetailsActivity;
|
||||
import fr.free.nrw.commons.explore.SearchActivity;
|
||||
import fr.free.nrw.commons.media.ZoomableActivity;
|
||||
import fr.free.nrw.commons.nearby.WikidataFeedback;
|
||||
import fr.free.nrw.commons.notification.NotificationActivity;
|
||||
import fr.free.nrw.commons.profile.ProfileActivity;
|
||||
import fr.free.nrw.commons.review.ReviewActivity;
|
||||
import fr.free.nrw.commons.settings.SettingsActivity;
|
||||
import fr.free.nrw.commons.upload.UploadActivity;
|
||||
import fr.free.nrw.commons.upload.UploadProgressActivity;
|
||||
|
||||
/**
|
||||
* This Class handles the dependency injection (using dagger)
|
||||
* so, if a developer needs to add a new activity to the commons app
|
||||
* then that must be mentioned here to inject the dependencies
|
||||
*/
|
||||
@Module
|
||||
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||
public abstract class ActivityBuilderModule {
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract LoginActivity bindLoginActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract WelcomeActivity bindWelcomeActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract MainActivity bindContributionsActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract CustomSelectorActivity bindCustomSelectorActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract SettingsActivity bindSettingsActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract AboutActivity bindAboutActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract LocationPickerActivity bindLocationPickerActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract SignupActivity bindSignupActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract NotificationActivity bindNotificationActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract UploadActivity bindUploadActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract SearchActivity bindSearchActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract CategoryDetailsActivity bindCategoryDetailsActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract WikidataItemDetailsActivity bindDepictionDetailsActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract ProfileActivity bindAchievementsActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract ReviewActivity bindReviewActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract DescriptionEditActivity bindDescriptionEditActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract ZoomableActivity bindZoomableActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract UploadProgressActivity bindUploadProgressActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract WikidataFeedback bindWikiFeedback();
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
package fr.free.nrw.commons.di
|
||||
|
||||
import dagger.Module
|
||||
import dagger.android.ContributesAndroidInjector
|
||||
import fr.free.nrw.commons.AboutActivity
|
||||
import fr.free.nrw.commons.locationpicker.LocationPickerActivity
|
||||
import fr.free.nrw.commons.WelcomeActivity
|
||||
import fr.free.nrw.commons.auth.LoginActivity
|
||||
import fr.free.nrw.commons.auth.SignupActivity
|
||||
import fr.free.nrw.commons.category.CategoryDetailsActivity
|
||||
import fr.free.nrw.commons.contributions.MainActivity
|
||||
import fr.free.nrw.commons.customselector.ui.selector.CustomSelectorActivity
|
||||
import fr.free.nrw.commons.description.DescriptionEditActivity
|
||||
import fr.free.nrw.commons.explore.SearchActivity
|
||||
import fr.free.nrw.commons.explore.depictions.WikidataItemDetailsActivity
|
||||
import fr.free.nrw.commons.media.ZoomableActivity
|
||||
import fr.free.nrw.commons.nearby.WikidataFeedback
|
||||
import fr.free.nrw.commons.notification.NotificationActivity
|
||||
import fr.free.nrw.commons.profile.ProfileActivity
|
||||
import fr.free.nrw.commons.review.ReviewActivity
|
||||
import fr.free.nrw.commons.settings.SettingsActivity
|
||||
import fr.free.nrw.commons.upload.UploadActivity
|
||||
import fr.free.nrw.commons.upload.UploadProgressActivity
|
||||
|
||||
/**
|
||||
* This Class handles the dependency injection (using dagger)
|
||||
* so, if a developer needs to add a new activity to the commons app
|
||||
* then that must be mentioned here to inject the dependencies
|
||||
*/
|
||||
@Module
|
||||
@Suppress("unused")
|
||||
abstract class ActivityBuilderModule {
|
||||
@ContributesAndroidInjector
|
||||
abstract fun bindLoginActivity(): LoginActivity
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun bindWelcomeActivity(): WelcomeActivity
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun bindContributionsActivity(): MainActivity
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun bindCustomSelectorActivity(): CustomSelectorActivity
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun bindSettingsActivity(): SettingsActivity
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun bindAboutActivity(): AboutActivity
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun bindLocationPickerActivity(): LocationPickerActivity
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun bindSignupActivity(): SignupActivity
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun bindNotificationActivity(): NotificationActivity
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun bindUploadActivity(): UploadActivity
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun bindSearchActivity(): SearchActivity
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun bindCategoryDetailsActivity(): CategoryDetailsActivity
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun bindDepictionDetailsActivity(): WikidataItemDetailsActivity
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun bindAchievementsActivity(): ProfileActivity
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun bindReviewActivity(): ReviewActivity
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun bindDescriptionEditActivity(): DescriptionEditActivity
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun bindZoomableActivity(): ZoomableActivity
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun bindUploadProgressActivity(): UploadProgressActivity
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun bindWikiFeedback(): WikidataFeedback
|
||||
}
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
package fr.free.nrw.commons.di;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Service;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ContentProvider;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import dagger.android.HasAndroidInjector;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import dagger.android.AndroidInjector;
|
||||
import dagger.android.DispatchingAndroidInjector;
|
||||
import dagger.android.HasActivityInjector;
|
||||
import dagger.android.HasBroadcastReceiverInjector;
|
||||
import dagger.android.HasContentProviderInjector;
|
||||
import dagger.android.HasFragmentInjector;
|
||||
import dagger.android.HasServiceInjector;
|
||||
import dagger.android.support.HasSupportFragmentInjector;
|
||||
|
||||
/**
|
||||
* Provides injectors for all sorts of components
|
||||
* Ex: Activities, Fragments, Services, ContentProviders
|
||||
*/
|
||||
public class ApplicationlessInjection
|
||||
implements
|
||||
HasAndroidInjector,
|
||||
HasActivityInjector,
|
||||
HasFragmentInjector,
|
||||
HasSupportFragmentInjector,
|
||||
HasServiceInjector,
|
||||
HasBroadcastReceiverInjector,
|
||||
HasContentProviderInjector {
|
||||
|
||||
private static ApplicationlessInjection instance = null;
|
||||
|
||||
@Inject DispatchingAndroidInjector<Object> androidInjector;
|
||||
@Inject DispatchingAndroidInjector<Activity> activityInjector;
|
||||
@Inject DispatchingAndroidInjector<BroadcastReceiver> broadcastReceiverInjector;
|
||||
@Inject DispatchingAndroidInjector<android.app.Fragment> fragmentInjector;
|
||||
@Inject DispatchingAndroidInjector<Fragment> supportFragmentInjector;
|
||||
@Inject DispatchingAndroidInjector<Service> serviceInjector;
|
||||
@Inject DispatchingAndroidInjector<ContentProvider> contentProviderInjector;
|
||||
|
||||
private CommonsApplicationComponent commonsApplicationComponent;
|
||||
|
||||
public ApplicationlessInjection(Context applicationContext) {
|
||||
commonsApplicationComponent = DaggerCommonsApplicationComponent.builder()
|
||||
.appModule(new CommonsApplicationModule(applicationContext)).build();
|
||||
commonsApplicationComponent.inject(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AndroidInjector<Object> androidInjector() {
|
||||
return androidInjector;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DispatchingAndroidInjector<Activity> activityInjector() {
|
||||
return activityInjector;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DispatchingAndroidInjector<android.app.Fragment> fragmentInjector() {
|
||||
return fragmentInjector;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DispatchingAndroidInjector<Fragment> supportFragmentInjector() {
|
||||
return supportFragmentInjector;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DispatchingAndroidInjector<BroadcastReceiver> broadcastReceiverInjector() {
|
||||
return broadcastReceiverInjector;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DispatchingAndroidInjector<Service> serviceInjector() {
|
||||
return serviceInjector;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AndroidInjector<ContentProvider> contentProviderInjector() {
|
||||
return contentProviderInjector;
|
||||
}
|
||||
|
||||
public CommonsApplicationComponent getCommonsApplicationComponent() {
|
||||
return commonsApplicationComponent;
|
||||
}
|
||||
|
||||
public static ApplicationlessInjection getInstance(Context applicationContext) {
|
||||
if (instance == null) {
|
||||
synchronized (ApplicationlessInjection.class) {
|
||||
if (instance == null) {
|
||||
instance = new ApplicationlessInjection(applicationContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
package fr.free.nrw.commons.di
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Fragment
|
||||
import android.app.Service
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.ContentProvider
|
||||
import android.content.Context
|
||||
import dagger.android.AndroidInjector
|
||||
import dagger.android.DispatchingAndroidInjector
|
||||
import dagger.android.HasActivityInjector
|
||||
import dagger.android.HasAndroidInjector
|
||||
import dagger.android.HasBroadcastReceiverInjector
|
||||
import dagger.android.HasContentProviderInjector
|
||||
import dagger.android.HasFragmentInjector
|
||||
import dagger.android.HasServiceInjector
|
||||
import dagger.android.support.HasSupportFragmentInjector
|
||||
import javax.inject.Inject
|
||||
import androidx.fragment.app.Fragment as AndroidXFragmen
|
||||
|
||||
/**
|
||||
* Provides injectors for all sorts of components
|
||||
* Ex: Activities, Fragments, Services, ContentProviders
|
||||
*/
|
||||
class ApplicationlessInjection(applicationContext: Context) : HasAndroidInjector,
|
||||
HasActivityInjector, HasFragmentInjector, HasSupportFragmentInjector, HasServiceInjector,
|
||||
HasBroadcastReceiverInjector, HasContentProviderInjector {
|
||||
@Inject @JvmField
|
||||
var androidInjector: DispatchingAndroidInjector<Any>? = null
|
||||
|
||||
@Inject @JvmField
|
||||
var activityInjector: DispatchingAndroidInjector<Activity>? = null
|
||||
|
||||
@Inject @JvmField
|
||||
var broadcastReceiverInjector: DispatchingAndroidInjector<BroadcastReceiver>? = null
|
||||
|
||||
@Inject @JvmField
|
||||
var fragmentInjector: DispatchingAndroidInjector<Fragment>? = null
|
||||
|
||||
@Inject @JvmField
|
||||
var supportFragmentInjector: DispatchingAndroidInjector<AndroidXFragmen>? = null
|
||||
|
||||
@Inject @JvmField
|
||||
var serviceInjector: DispatchingAndroidInjector<Service>? = null
|
||||
|
||||
@Inject @JvmField
|
||||
var contentProviderInjector: DispatchingAndroidInjector<ContentProvider>? = null
|
||||
|
||||
val instance: ApplicationlessInjection get() = _instance!!
|
||||
|
||||
val commonsApplicationComponent: CommonsApplicationComponent =
|
||||
DaggerCommonsApplicationComponent
|
||||
.builder()
|
||||
.appModule(CommonsApplicationModule(applicationContext))
|
||||
.build()
|
||||
|
||||
init {
|
||||
commonsApplicationComponent.inject(this)
|
||||
}
|
||||
|
||||
override fun androidInjector(): AndroidInjector<Any>? =
|
||||
androidInjector
|
||||
|
||||
override fun activityInjector(): DispatchingAndroidInjector<Activity>? =
|
||||
activityInjector
|
||||
|
||||
override fun fragmentInjector(): DispatchingAndroidInjector<Fragment>? =
|
||||
fragmentInjector
|
||||
|
||||
override fun supportFragmentInjector(): DispatchingAndroidInjector<AndroidXFragmen>? =
|
||||
supportFragmentInjector
|
||||
|
||||
override fun broadcastReceiverInjector(): DispatchingAndroidInjector<BroadcastReceiver>? =
|
||||
broadcastReceiverInjector
|
||||
|
||||
override fun serviceInjector(): DispatchingAndroidInjector<Service>? =
|
||||
serviceInjector
|
||||
|
||||
override fun contentProviderInjector(): AndroidInjector<ContentProvider>? =
|
||||
contentProviderInjector
|
||||
|
||||
companion object {
|
||||
private var _instance: ApplicationlessInjection? = null
|
||||
|
||||
@JvmStatic
|
||||
fun getInstance(applicationContext: Context): ApplicationlessInjection {
|
||||
if (_instance == null) {
|
||||
synchronized(ApplicationlessInjection::class.java) {
|
||||
if (_instance == null) {
|
||||
_instance = ApplicationlessInjection(applicationContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _instance!!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
package fr.free.nrw.commons.di;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
import fr.free.nrw.commons.actions.PageEditClient;
|
||||
import fr.free.nrw.commons.explore.categories.CategoriesModule;
|
||||
import fr.free.nrw.commons.navtab.MoreBottomSheetFragment;
|
||||
import fr.free.nrw.commons.navtab.MoreBottomSheetLoggedOutFragment;
|
||||
import fr.free.nrw.commons.nearby.NearbyController;
|
||||
import fr.free.nrw.commons.upload.worker.UploadWorker;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import dagger.Component;
|
||||
import dagger.android.AndroidInjectionModule;
|
||||
import dagger.android.AndroidInjector;
|
||||
import dagger.android.support.AndroidSupportInjectionModule;
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.auth.LoginActivity;
|
||||
import fr.free.nrw.commons.contributions.ContributionsModule;
|
||||
import fr.free.nrw.commons.explore.depictions.DepictionModule;
|
||||
import fr.free.nrw.commons.explore.SearchModule;
|
||||
import fr.free.nrw.commons.review.ReviewController;
|
||||
import fr.free.nrw.commons.settings.SettingsFragment;
|
||||
import fr.free.nrw.commons.upload.FileProcessor;
|
||||
import fr.free.nrw.commons.upload.UploadModule;
|
||||
import fr.free.nrw.commons.widget.PicOfDayAppWidget;
|
||||
|
||||
|
||||
/**
|
||||
* Facilitates Injection from CommonsApplicationModule to all the
|
||||
* classes seeking a dependency to be injected
|
||||
*/
|
||||
@Singleton
|
||||
@Component(modules = {
|
||||
CommonsApplicationModule.class,
|
||||
NetworkingModule.class,
|
||||
AndroidInjectionModule.class,
|
||||
AndroidSupportInjectionModule.class,
|
||||
ActivityBuilderModule.class,
|
||||
FragmentBuilderModule.class,
|
||||
ServiceBuilderModule.class,
|
||||
ContentProviderBuilderModule.class,
|
||||
UploadModule.class,
|
||||
ContributionsModule.class,
|
||||
SearchModule.class,
|
||||
DepictionModule.class,
|
||||
CategoriesModule.class
|
||||
})
|
||||
public interface CommonsApplicationComponent extends AndroidInjector<ApplicationlessInjection> {
|
||||
void inject(CommonsApplication application);
|
||||
|
||||
void inject(UploadWorker worker);
|
||||
|
||||
void inject(LoginActivity activity);
|
||||
|
||||
void inject(SettingsFragment fragment);
|
||||
|
||||
void inject(MoreBottomSheetFragment fragment);
|
||||
|
||||
void inject(MoreBottomSheetLoggedOutFragment fragment);
|
||||
|
||||
void inject(ReviewController reviewController);
|
||||
|
||||
//void inject(NavTabLayout view);
|
||||
|
||||
@Override
|
||||
void inject(ApplicationlessInjection instance);
|
||||
|
||||
void inject(FileProcessor fileProcessor);
|
||||
|
||||
void inject(PicOfDayAppWidget picOfDayAppWidget);
|
||||
|
||||
@Singleton
|
||||
void inject(NearbyController nearbyController);
|
||||
|
||||
Gson gson();
|
||||
|
||||
@Component.Builder
|
||||
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||
interface Builder {
|
||||
|
||||
Builder appModule(CommonsApplicationModule applicationModule);
|
||||
|
||||
CommonsApplicationComponent build();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
package fr.free.nrw.commons.di
|
||||
|
||||
import com.google.gson.Gson
|
||||
import dagger.Component
|
||||
import dagger.android.AndroidInjectionModule
|
||||
import dagger.android.AndroidInjector
|
||||
import dagger.android.support.AndroidSupportInjectionModule
|
||||
import fr.free.nrw.commons.CommonsApplication
|
||||
import fr.free.nrw.commons.auth.LoginActivity
|
||||
import fr.free.nrw.commons.contributions.ContributionsModule
|
||||
import fr.free.nrw.commons.explore.SearchModule
|
||||
import fr.free.nrw.commons.explore.categories.CategoriesModule
|
||||
import fr.free.nrw.commons.explore.depictions.DepictionModule
|
||||
import fr.free.nrw.commons.navtab.MoreBottomSheetFragment
|
||||
import fr.free.nrw.commons.navtab.MoreBottomSheetLoggedOutFragment
|
||||
import fr.free.nrw.commons.nearby.NearbyController
|
||||
import fr.free.nrw.commons.review.ReviewController
|
||||
import fr.free.nrw.commons.settings.SettingsFragment
|
||||
import fr.free.nrw.commons.upload.FileProcessor
|
||||
import fr.free.nrw.commons.upload.UploadModule
|
||||
import fr.free.nrw.commons.upload.worker.UploadWorker
|
||||
import fr.free.nrw.commons.widget.PicOfDayAppWidget
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Facilitates Injection from CommonsApplicationModule to all the
|
||||
* classes seeking a dependency to be injected
|
||||
*/
|
||||
@Singleton
|
||||
@Component(
|
||||
modules = [
|
||||
CommonsApplicationModule::class,
|
||||
NetworkingModule::class,
|
||||
AndroidInjectionModule::class,
|
||||
AndroidSupportInjectionModule::class,
|
||||
ActivityBuilderModule::class,
|
||||
FragmentBuilderModule::class,
|
||||
ServiceBuilderModule::class,
|
||||
ContentProviderBuilderModule::class,
|
||||
UploadModule::class,
|
||||
ContributionsModule::class,
|
||||
SearchModule::class,
|
||||
DepictionModule::class,
|
||||
CategoriesModule::class
|
||||
]
|
||||
)
|
||||
interface CommonsApplicationComponent : AndroidInjector<ApplicationlessInjection> {
|
||||
fun inject(application: CommonsApplication)
|
||||
|
||||
fun inject(worker: UploadWorker)
|
||||
|
||||
fun inject(activity: LoginActivity)
|
||||
|
||||
fun inject(fragment: SettingsFragment)
|
||||
|
||||
fun inject(fragment: MoreBottomSheetFragment)
|
||||
|
||||
fun inject(fragment: MoreBottomSheetLoggedOutFragment)
|
||||
|
||||
fun inject(reviewController: ReviewController)
|
||||
|
||||
override fun inject(instance: ApplicationlessInjection)
|
||||
|
||||
fun inject(fileProcessor: FileProcessor)
|
||||
|
||||
fun inject(picOfDayAppWidget: PicOfDayAppWidget)
|
||||
|
||||
@Singleton
|
||||
fun inject(nearbyController: NearbyController)
|
||||
|
||||
fun gson(): Gson
|
||||
|
||||
@Component.Builder
|
||||
@Suppress("unused")
|
||||
interface Builder {
|
||||
fun appModule(applicationModule: CommonsApplicationModule): Builder
|
||||
|
||||
fun build(): CommonsApplicationComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -1,320 +0,0 @@
|
|||
package fr.free.nrw.commons.di;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import androidx.collection.LruCache;
|
||||
import androidx.room.Room;
|
||||
import androidx.room.migration.Migration;
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||
import com.google.gson.Gson;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import fr.free.nrw.commons.BuildConfig;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.auth.AccountUtil;
|
||||
import fr.free.nrw.commons.auth.SessionManager;
|
||||
import fr.free.nrw.commons.contributions.ContributionDao;
|
||||
import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao;
|
||||
import fr.free.nrw.commons.customselector.database.UploadedStatusDao;
|
||||
import fr.free.nrw.commons.customselector.ui.selector.ImageFileLoader;
|
||||
import fr.free.nrw.commons.data.DBOpenHelper;
|
||||
import fr.free.nrw.commons.db.AppDatabase;
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
import fr.free.nrw.commons.location.LocationServiceManager;
|
||||
import fr.free.nrw.commons.nearby.PlaceDao;
|
||||
import fr.free.nrw.commons.review.ReviewDao;
|
||||
import fr.free.nrw.commons.settings.Prefs;
|
||||
import fr.free.nrw.commons.upload.UploadController;
|
||||
import fr.free.nrw.commons.upload.depicts.DepictsDao;
|
||||
import fr.free.nrw.commons.utils.ConfigUtils;
|
||||
import fr.free.nrw.commons.wikidata.WikidataEditListener;
|
||||
import fr.free.nrw.commons.wikidata.WikidataEditListenerImpl;
|
||||
import io.reactivex.Scheduler;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
/**
|
||||
* The Dependency Provider class for Commons Android.
|
||||
*
|
||||
* Provides all sorts of ContentProviderClients used by the app
|
||||
* along with the Liscences, AccountUtility, UploadController, Logged User,
|
||||
* Location manager etc
|
||||
*/
|
||||
@Module
|
||||
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||
public class CommonsApplicationModule {
|
||||
private Context applicationContext;
|
||||
public static final String IO_THREAD="io_thread";
|
||||
public static final String MAIN_THREAD="main_thread";
|
||||
private AppDatabase appDatabase;
|
||||
|
||||
static final Migration MIGRATION_1_2 = new Migration(1, 2) {
|
||||
@Override
|
||||
public void migrate(SupportSQLiteDatabase database) {
|
||||
database.execSQL("ALTER TABLE contribution "
|
||||
+ " ADD COLUMN hasInvalidLocation INTEGER NOT NULL DEFAULT 0");
|
||||
}
|
||||
};
|
||||
|
||||
public CommonsApplicationModule(Context applicationContext) {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides ImageFileLoader used to fetch device images.
|
||||
* @param context
|
||||
* @return
|
||||
*/
|
||||
@Provides
|
||||
public ImageFileLoader providesImageFileLoader(Context context) {
|
||||
return new ImageFileLoader(context);
|
||||
}
|
||||
|
||||
@Provides
|
||||
public Context providesApplicationContext() {
|
||||
return this.applicationContext;
|
||||
}
|
||||
|
||||
@Provides
|
||||
public InputMethodManager provideInputMethodManager() {
|
||||
return (InputMethodManager) applicationContext.getSystemService(Activity.INPUT_METHOD_SERVICE);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named("licenses")
|
||||
public List<String> provideLicenses(Context context) {
|
||||
List<String> licenseItems = new ArrayList<>();
|
||||
licenseItems.add(context.getString(R.string.license_name_cc0));
|
||||
licenseItems.add(context.getString(R.string.license_name_cc_by));
|
||||
licenseItems.add(context.getString(R.string.license_name_cc_by_sa));
|
||||
licenseItems.add(context.getString(R.string.license_name_cc_by_four));
|
||||
licenseItems.add(context.getString(R.string.license_name_cc_by_sa_four));
|
||||
return licenseItems;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named("licenses_by_name")
|
||||
public Map<String, String> provideLicensesByName(Context context) {
|
||||
Map<String, String> byName = new HashMap<>();
|
||||
byName.put(context.getString(R.string.license_name_cc0), Prefs.Licenses.CC0);
|
||||
byName.put(context.getString(R.string.license_name_cc_by), Prefs.Licenses.CC_BY_3);
|
||||
byName.put(context.getString(R.string.license_name_cc_by_sa), Prefs.Licenses.CC_BY_SA_3);
|
||||
byName.put(context.getString(R.string.license_name_cc_by_four), Prefs.Licenses.CC_BY_4);
|
||||
byName.put(context.getString(R.string.license_name_cc_by_sa_four), Prefs.Licenses.CC_BY_SA_4);
|
||||
return byName;
|
||||
}
|
||||
|
||||
@Provides
|
||||
public AccountUtil providesAccountUtil(Context context) {
|
||||
return new AccountUtil();
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides an instance of CategoryContentProviderClient i.e. the categories
|
||||
* that are there in local storage
|
||||
*/
|
||||
@Provides
|
||||
@Named("category")
|
||||
public ContentProviderClient provideCategoryContentProviderClient(Context context) {
|
||||
return context.getContentResolver().acquireContentProviderClient(BuildConfig.CATEGORY_AUTHORITY);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is used to provide instance of RecentSearchContentProviderClient
|
||||
* which provides content of Recent Searches from database
|
||||
* @param context
|
||||
* @return returns RecentSearchContentProviderClient
|
||||
*/
|
||||
@Provides
|
||||
@Named("recentsearch")
|
||||
public ContentProviderClient provideRecentSearchContentProviderClient(Context context) {
|
||||
return context.getContentResolver().acquireContentProviderClient(BuildConfig.RECENT_SEARCH_AUTHORITY);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named("contribution")
|
||||
public ContentProviderClient provideContributionContentProviderClient(Context context) {
|
||||
return context.getContentResolver().acquireContentProviderClient(BuildConfig.CONTRIBUTION_AUTHORITY);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named("modification")
|
||||
public ContentProviderClient provideModificationContentProviderClient(Context context) {
|
||||
return context.getContentResolver().acquireContentProviderClient(BuildConfig.MODIFICATION_AUTHORITY);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named("bookmarks")
|
||||
public ContentProviderClient provideBookmarkContentProviderClient(Context context) {
|
||||
return context.getContentResolver().acquireContentProviderClient(BuildConfig.BOOKMARK_AUTHORITY);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named("bookmarksLocation")
|
||||
public ContentProviderClient provideBookmarkLocationContentProviderClient(Context context) {
|
||||
return context.getContentResolver().acquireContentProviderClient(BuildConfig.BOOKMARK_LOCATIONS_AUTHORITY);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named("bookmarksItem")
|
||||
public ContentProviderClient provideBookmarkItemContentProviderClient(Context context) {
|
||||
return context.getContentResolver().acquireContentProviderClient(BuildConfig.BOOKMARK_ITEMS_AUTHORITY);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is used to provide instance of RecentLanguagesContentProvider
|
||||
* which provides content of recent used languages from database
|
||||
* @param context Context
|
||||
* @return returns RecentLanguagesContentProvider
|
||||
*/
|
||||
@Provides
|
||||
@Named("recent_languages")
|
||||
public ContentProviderClient provideRecentLanguagesContentProviderClient(final Context context) {
|
||||
return context.getContentResolver()
|
||||
.acquireContentProviderClient(BuildConfig.RECENT_LANGUAGE_AUTHORITY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a Json store instance(JsonKvStore) which keeps
|
||||
* the provided Gson in it's instance
|
||||
* @param gson stored inside the store instance
|
||||
*/
|
||||
@Provides
|
||||
@Named("default_preferences")
|
||||
public JsonKvStore providesDefaultKvStore(Context context, Gson gson) {
|
||||
String storeName = context.getPackageName() + "_preferences";
|
||||
return new JsonKvStore(context, storeName, gson);
|
||||
}
|
||||
|
||||
@Provides
|
||||
public UploadController providesUploadController(SessionManager sessionManager,
|
||||
@Named("default_preferences") JsonKvStore kvStore,
|
||||
Context context, ContributionDao contributionDao) {
|
||||
return new UploadController(sessionManager, context, kvStore);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public LocationServiceManager provideLocationServiceManager(Context context) {
|
||||
return new LocationServiceManager(context);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public DBOpenHelper provideDBOpenHelper(Context context) {
|
||||
return new DBOpenHelper(context);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Named("thumbnail-cache")
|
||||
public LruCache<String, String> provideLruCache() {
|
||||
return new LruCache<>(1024);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public WikidataEditListener provideWikidataEditListener() {
|
||||
return new WikidataEditListenerImpl();
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides app flavour. Can be used to alter flows in the app
|
||||
* @return
|
||||
*/
|
||||
@Named("isBeta")
|
||||
@Provides
|
||||
@Singleton
|
||||
public boolean provideIsBetaVariant() {
|
||||
return ConfigUtils.isBetaFlavour();
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide JavaRx IO scheduler which manages IO operations
|
||||
* across various Threads
|
||||
*/
|
||||
@Named(IO_THREAD)
|
||||
@Provides
|
||||
public Scheduler providesIoThread(){
|
||||
return Schedulers.io();
|
||||
}
|
||||
|
||||
@Named(MAIN_THREAD)
|
||||
@Provides
|
||||
public Scheduler providesMainThread() {
|
||||
return AndroidSchedulers.mainThread();
|
||||
}
|
||||
|
||||
@Named("username")
|
||||
@Provides
|
||||
public String provideLoggedInUsername(SessionManager sessionManager) {
|
||||
return Objects.toString(sessionManager.getUserName(), "");
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public AppDatabase provideAppDataBase() {
|
||||
appDatabase = Room.databaseBuilder(applicationContext, AppDatabase.class, "commons_room.db")
|
||||
.addMigrations(MIGRATION_1_2)
|
||||
.fallbackToDestructiveMigration()
|
||||
.build();
|
||||
return appDatabase;
|
||||
}
|
||||
|
||||
@Provides
|
||||
public ContributionDao providesContributionsDao(AppDatabase appDatabase) {
|
||||
return appDatabase.contributionDao();
|
||||
}
|
||||
|
||||
@Provides
|
||||
public PlaceDao providesPlaceDao(AppDatabase appDatabase) {
|
||||
return appDatabase.PlaceDao();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the reference of DepictsDao class.
|
||||
*/
|
||||
@Provides
|
||||
public DepictsDao providesDepictDao(AppDatabase appDatabase) {
|
||||
return appDatabase.DepictsDao();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the reference of UploadedStatus class.
|
||||
*/
|
||||
@Provides
|
||||
public UploadedStatusDao providesUploadedStatusDao(AppDatabase appDatabase) {
|
||||
return appDatabase.UploadedStatusDao();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the reference of NotForUploadStatus class.
|
||||
*/
|
||||
@Provides
|
||||
public NotForUploadStatusDao providesNotForUploadStatusDao(AppDatabase appDatabase) {
|
||||
return appDatabase.NotForUploadStatusDao();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the reference of ReviewDao class
|
||||
*/
|
||||
@Provides
|
||||
public ReviewDao providesReviewDao(AppDatabase appDatabase){
|
||||
return appDatabase.ReviewDao();
|
||||
}
|
||||
|
||||
@Provides
|
||||
public ContentResolver providesContentResolver(Context context){
|
||||
return context.getContentResolver();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,245 @@
|
|||
package fr.free.nrw.commons.di
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ContentProviderClient
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.collection.LruCache
|
||||
import androidx.room.Room.databaseBuilder
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import com.google.gson.Gson
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import fr.free.nrw.commons.BuildConfig
|
||||
import fr.free.nrw.commons.R
|
||||
import fr.free.nrw.commons.auth.SessionManager
|
||||
import fr.free.nrw.commons.contributions.ContributionDao
|
||||
import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao
|
||||
import fr.free.nrw.commons.customselector.database.UploadedStatusDao
|
||||
import fr.free.nrw.commons.customselector.ui.selector.ImageFileLoader
|
||||
import fr.free.nrw.commons.data.DBOpenHelper
|
||||
import fr.free.nrw.commons.db.AppDatabase
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore
|
||||
import fr.free.nrw.commons.location.LocationServiceManager
|
||||
import fr.free.nrw.commons.nearby.PlaceDao
|
||||
import fr.free.nrw.commons.review.ReviewDao
|
||||
import fr.free.nrw.commons.settings.Prefs
|
||||
import fr.free.nrw.commons.upload.UploadController
|
||||
import fr.free.nrw.commons.upload.depicts.DepictsDao
|
||||
import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour
|
||||
import fr.free.nrw.commons.utils.TimeProvider
|
||||
import fr.free.nrw.commons.wikidata.WikidataEditListener
|
||||
import fr.free.nrw.commons.wikidata.WikidataEditListenerImpl
|
||||
import io.reactivex.Scheduler
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import java.util.Objects
|
||||
import javax.inject.Named
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* The Dependency Provider class for Commons Android.
|
||||
* Provides all sorts of ContentProviderClients used by the app
|
||||
* along with the Liscences, AccountUtility, UploadController, Logged User,
|
||||
* Location manager etc
|
||||
*/
|
||||
@Module
|
||||
@Suppress("unused")
|
||||
open class CommonsApplicationModule(private val applicationContext: Context) {
|
||||
@Provides
|
||||
fun providesImageFileLoader(context: Context): ImageFileLoader =
|
||||
ImageFileLoader(context)
|
||||
|
||||
@Provides
|
||||
fun providesApplicationContext(): Context =
|
||||
applicationContext
|
||||
|
||||
@Provides
|
||||
fun provideInputMethodManager(): InputMethodManager =
|
||||
applicationContext.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
|
||||
@Provides
|
||||
@Named("licenses")
|
||||
fun provideLicenses(context: Context): List<String> = listOf(
|
||||
context.getString(R.string.license_name_cc0),
|
||||
context.getString(R.string.license_name_cc_by),
|
||||
context.getString(R.string.license_name_cc_by_sa),
|
||||
context.getString(R.string.license_name_cc_by_four),
|
||||
context.getString(R.string.license_name_cc_by_sa_four)
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Named("licenses_by_name")
|
||||
fun provideLicensesByName(context: Context): Map<String, String> = mapOf(
|
||||
context.getString(R.string.license_name_cc0) to Prefs.Licenses.CC0,
|
||||
context.getString(R.string.license_name_cc_by) to Prefs.Licenses.CC_BY_3,
|
||||
context.getString(R.string.license_name_cc_by_sa) to Prefs.Licenses.CC_BY_SA_3,
|
||||
context.getString(R.string.license_name_cc_by_four) to Prefs.Licenses.CC_BY_4,
|
||||
context.getString(R.string.license_name_cc_by_sa_four) to Prefs.Licenses.CC_BY_SA_4
|
||||
)
|
||||
|
||||
/**
|
||||
* Provides an instance of CategoryContentProviderClient i.e. the categories
|
||||
* that are there in local storage
|
||||
*/
|
||||
@Provides
|
||||
@Named("category")
|
||||
open fun provideCategoryContentProviderClient(context: Context): ContentProviderClient? =
|
||||
context.contentResolver.acquireContentProviderClient(BuildConfig.CATEGORY_AUTHORITY)
|
||||
|
||||
@Provides
|
||||
@Named("recentsearch")
|
||||
fun provideRecentSearchContentProviderClient(context: Context): ContentProviderClient? =
|
||||
context.contentResolver.acquireContentProviderClient(BuildConfig.RECENT_SEARCH_AUTHORITY)
|
||||
|
||||
@Provides
|
||||
@Named("contribution")
|
||||
open fun provideContributionContentProviderClient(context: Context): ContentProviderClient? =
|
||||
context.contentResolver.acquireContentProviderClient(BuildConfig.CONTRIBUTION_AUTHORITY)
|
||||
|
||||
@Provides
|
||||
@Named("modification")
|
||||
open fun provideModificationContentProviderClient(context: Context): ContentProviderClient? =
|
||||
context.contentResolver.acquireContentProviderClient(BuildConfig.MODIFICATION_AUTHORITY)
|
||||
|
||||
@Provides
|
||||
@Named("bookmarks")
|
||||
fun provideBookmarkContentProviderClient(context: Context): ContentProviderClient? =
|
||||
context.contentResolver.acquireContentProviderClient(BuildConfig.BOOKMARK_AUTHORITY)
|
||||
|
||||
@Provides
|
||||
@Named("bookmarksLocation")
|
||||
fun provideBookmarkLocationContentProviderClient(context: Context): ContentProviderClient? =
|
||||
context.contentResolver.acquireContentProviderClient(BuildConfig.BOOKMARK_LOCATIONS_AUTHORITY)
|
||||
|
||||
@Provides
|
||||
@Named("bookmarksItem")
|
||||
fun provideBookmarkItemContentProviderClient(context: Context): ContentProviderClient? =
|
||||
context.contentResolver.acquireContentProviderClient(BuildConfig.BOOKMARK_ITEMS_AUTHORITY)
|
||||
|
||||
/**
|
||||
* This method is used to provide instance of RecentLanguagesContentProvider
|
||||
* which provides content of recent used languages from database
|
||||
* @param context Context
|
||||
* @return returns RecentLanguagesContentProvider
|
||||
*/
|
||||
@Provides
|
||||
@Named("recent_languages")
|
||||
fun provideRecentLanguagesContentProviderClient(context: Context): ContentProviderClient? =
|
||||
context.contentResolver.acquireContentProviderClient(BuildConfig.RECENT_LANGUAGE_AUTHORITY)
|
||||
|
||||
/**
|
||||
* Provides a Json store instance(JsonKvStore) which keeps
|
||||
* the provided Gson in it's instance
|
||||
* @param gson stored inside the store instance
|
||||
*/
|
||||
@Provides
|
||||
@Named("default_preferences")
|
||||
open fun providesDefaultKvStore(context: Context, gson: Gson): JsonKvStore =
|
||||
JsonKvStore(context, "${context.packageName}_preferences", gson)
|
||||
|
||||
@Provides
|
||||
fun providesUploadController(
|
||||
sessionManager: SessionManager,
|
||||
@Named("default_preferences") kvStore: JsonKvStore,
|
||||
context: Context
|
||||
): UploadController = UploadController(sessionManager, context, kvStore)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
open fun provideLocationServiceManager(context: Context): LocationServiceManager =
|
||||
LocationServiceManager(context)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
open fun provideDBOpenHelper(context: Context): DBOpenHelper =
|
||||
DBOpenHelper(context)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Named("thumbnail-cache")
|
||||
open fun provideLruCache(): LruCache<String, String> =
|
||||
LruCache(1024)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideWikidataEditListener(): WikidataEditListener =
|
||||
WikidataEditListenerImpl()
|
||||
|
||||
@Named("isBeta")
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideIsBetaVariant(): Boolean =
|
||||
isBetaFlavour
|
||||
|
||||
@Named(IO_THREAD)
|
||||
@Provides
|
||||
fun providesIoThread(): Scheduler =
|
||||
Schedulers.io()
|
||||
|
||||
@Named(MAIN_THREAD)
|
||||
@Provides
|
||||
fun providesMainThread(): Scheduler =
|
||||
AndroidSchedulers.mainThread()
|
||||
|
||||
@Named("username")
|
||||
@Provides
|
||||
fun provideLoggedInUsername(sessionManager: SessionManager): String =
|
||||
Objects.toString(sessionManager.userName, "")
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAppDataBase(): AppDatabase = databaseBuilder(
|
||||
applicationContext,
|
||||
AppDatabase::class.java,
|
||||
"commons_room.db"
|
||||
).addMigrations(MIGRATION_1_2).fallbackToDestructiveMigration().build()
|
||||
|
||||
@Provides
|
||||
fun providesContributionsDao(appDatabase: AppDatabase): ContributionDao =
|
||||
appDatabase.contributionDao()
|
||||
|
||||
@Provides
|
||||
fun providesPlaceDao(appDatabase: AppDatabase): PlaceDao =
|
||||
appDatabase.PlaceDao()
|
||||
|
||||
@Provides
|
||||
fun providesDepictDao(appDatabase: AppDatabase): DepictsDao =
|
||||
appDatabase.DepictsDao()
|
||||
|
||||
@Provides
|
||||
fun providesUploadedStatusDao(appDatabase: AppDatabase): UploadedStatusDao =
|
||||
appDatabase.UploadedStatusDao()
|
||||
|
||||
@Provides
|
||||
fun providesNotForUploadStatusDao(appDatabase: AppDatabase): NotForUploadStatusDao =
|
||||
appDatabase.NotForUploadStatusDao()
|
||||
|
||||
@Provides
|
||||
fun providesReviewDao(appDatabase: AppDatabase): ReviewDao =
|
||||
appDatabase.ReviewDao()
|
||||
|
||||
@Provides
|
||||
fun providesContentResolver(context: Context): ContentResolver =
|
||||
context.contentResolver
|
||||
|
||||
@Provides
|
||||
fun provideTimeProvider(): TimeProvider {
|
||||
return TimeProvider(System::currentTimeMillis)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val IO_THREAD: String = "io_thread"
|
||||
const val MAIN_THREAD: String = "main_thread"
|
||||
|
||||
val MIGRATION_1_2: Migration = object : Migration(1, 2) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL(
|
||||
"ALTER TABLE contribution " + " ADD COLUMN hasInvalidLocation INTEGER NOT NULL DEFAULT 0"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
package fr.free.nrw.commons.di;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import dagger.android.AndroidInjector;
|
||||
import dagger.android.DispatchingAndroidInjector;
|
||||
import dagger.android.support.HasSupportFragmentInjector;
|
||||
|
||||
public abstract class CommonsDaggerAppCompatActivity extends AppCompatActivity implements HasSupportFragmentInjector {
|
||||
|
||||
@Inject
|
||||
DispatchingAndroidInjector<Fragment> supportFragmentInjector;
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
inject();
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AndroidInjector<Fragment> supportFragmentInjector() {
|
||||
return supportFragmentInjector;
|
||||
}
|
||||
|
||||
/**
|
||||
* when this Activity is created it injects an instance of this class inside
|
||||
* activityInjector method of ApplicationlessInjection
|
||||
*/
|
||||
private void inject() {
|
||||
ApplicationlessInjection injection = ApplicationlessInjection.getInstance(getApplicationContext());
|
||||
|
||||
AndroidInjector<Activity> activityInjector = injection.activityInjector();
|
||||
|
||||
if (activityInjector == null) {
|
||||
throw new NullPointerException("ApplicationlessInjection.activityInjector() returned null");
|
||||
}
|
||||
|
||||
activityInjector.inject(this);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package fr.free.nrw.commons.di
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import dagger.android.AndroidInjector
|
||||
import dagger.android.DispatchingAndroidInjector
|
||||
import dagger.android.support.HasSupportFragmentInjector
|
||||
import fr.free.nrw.commons.di.ApplicationlessInjection.Companion.getInstance
|
||||
import javax.inject.Inject
|
||||
|
||||
abstract class CommonsDaggerAppCompatActivity : AppCompatActivity(), HasSupportFragmentInjector {
|
||||
@Inject @JvmField
|
||||
var supportFragmentInjector: DispatchingAndroidInjector<Fragment>? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
inject()
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun supportFragmentInjector(): AndroidInjector<Fragment> {
|
||||
return supportFragmentInjector!!
|
||||
}
|
||||
|
||||
/**
|
||||
* when this Activity is created it injects an instance of this class inside
|
||||
* activityInjector method of ApplicationlessInjection
|
||||
*/
|
||||
private fun inject() {
|
||||
val injection = getInstance(applicationContext)
|
||||
|
||||
val activityInjector = injection.activityInjector()
|
||||
?: throw NullPointerException("ApplicationlessInjection.activityInjector() returned null")
|
||||
|
||||
activityInjector.inject(this)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
package fr.free.nrw.commons.di;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import dagger.android.AndroidInjector;
|
||||
|
||||
/**
|
||||
* Receives broadcast then injects it's instance to the broadcastReceiverInjector method of
|
||||
* ApplicationlessInjection class
|
||||
*/
|
||||
public abstract class CommonsDaggerBroadcastReceiver extends BroadcastReceiver {
|
||||
|
||||
public CommonsDaggerBroadcastReceiver() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
inject(context);
|
||||
}
|
||||
|
||||
private void inject(Context context) {
|
||||
ApplicationlessInjection injection = ApplicationlessInjection.getInstance(context.getApplicationContext());
|
||||
|
||||
AndroidInjector<BroadcastReceiver> serviceInjector = injection.broadcastReceiverInjector();
|
||||
|
||||
if (serviceInjector == null) {
|
||||
throw new NullPointerException("ApplicationlessInjection.broadcastReceiverInjector() returned null");
|
||||
}
|
||||
serviceInjector.inject(this);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package fr.free.nrw.commons.di
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import fr.free.nrw.commons.di.ApplicationlessInjection.Companion.getInstance
|
||||
|
||||
/**
|
||||
* Receives broadcast then injects it's instance to the broadcastReceiverInjector method of
|
||||
* ApplicationlessInjection class
|
||||
*/
|
||||
abstract class CommonsDaggerBroadcastReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
inject(context)
|
||||
}
|
||||
|
||||
private fun inject(context: Context) {
|
||||
val injection = getInstance(context.applicationContext)
|
||||
|
||||
val serviceInjector = injection.broadcastReceiverInjector()
|
||||
?: throw NullPointerException("ApplicationlessInjection.broadcastReceiverInjector() returned null")
|
||||
|
||||
serviceInjector.inject(this)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue