mirror of
				https://github.com/commons-app/apps-android-commons.git
				synced 2025-10-31 14:53:59 +01:00 
			
		
		
		
	Merge branch 'main' into issue5847-XiangyuBao
This commit is contained in:
		
						commit
						d0b4ec74ac
					
				
					 141 changed files with 2257 additions and 1280 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> | ||||
|  | @ -226,7 +226,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'] | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -366,11 +366,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 +380,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!!.codes[0] | ||||
|         Intents.intended( | ||||
|             CoreMatchers.allOf( | ||||
|                 IntentMatchers.hasAction(Intent.ACTION_VIEW), | ||||
|  |  | |||
|  | @ -17,6 +17,8 @@ import org.junit.Before | |||
| import org.junit.Rule | ||||
| import org.junit.Test | ||||
| import org.junit.runner.RunWith | ||||
| import org.hamcrest.MatcherAssert.assertThat | ||||
| import org.hamcrest.CoreMatchers.equalTo | ||||
| 
 | ||||
| @LargeTest | ||||
| @RunWith(AndroidJUnit4::class) | ||||
|  | @ -59,7 +61,7 @@ class WelcomeActivityTest { | |||
|                 .perform(ViewActions.click()) | ||||
|             onView(withId(R.id.finishTutorialButton)) | ||||
|                 .perform(ViewActions.click()) | ||||
|             assert(activityRule.activity.isDestroyed) | ||||
|             assertThat(activityRule.activity.isDestroyed, equalTo(true)) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -69,10 +71,10 @@ class WelcomeActivityTest { | |||
|             .perform(ViewActions.click()) | ||||
|         onView(withId(R.id.welcomePager)) | ||||
|             .perform(ViewActions.swipeLeft()) | ||||
|         assert(true) | ||||
|         assertThat(true, equalTo(true)) | ||||
|         onView(withId(R.id.welcomePager)) | ||||
|             .perform(ViewActions.swipeRight()) | ||||
|         assert(true) | ||||
|         assertThat(true, equalTo(true)) | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|  | @ -84,13 +86,13 @@ class WelcomeActivityTest { | |||
|             .perform(ViewActions.swipeLeft()) | ||||
|             .perform(ViewActions.swipeLeft()) | ||||
|             .perform(ViewActions.swipeLeft()) | ||||
|         assert(true) | ||||
|         assertThat(true, equalTo(true)) | ||||
|         onView(withId(R.id.welcomePager)) | ||||
|             .perform(ViewActions.swipeRight()) | ||||
|             .perform(ViewActions.swipeRight()) | ||||
|             .perform(ViewActions.swipeRight()) | ||||
|             .perform(ViewActions.swipeRight()) | ||||
|         assert(true) | ||||
|         assertThat(true, equalTo(true)) | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|  | @ -101,10 +103,10 @@ class WelcomeActivityTest { | |||
|             if (viewPager.currentItem == 3) { | ||||
|                 onView(withId(R.id.welcomePager)) | ||||
|                     .perform(ViewActions.swipeLeft()) | ||||
|                 assert(true) | ||||
|                 assertThat(true, equalTo(true)) | ||||
|                 onView(withId(R.id.welcomePager)) | ||||
|                     .perform(ViewActions.swipeRight()) | ||||
|                 assert(false) | ||||
|                 assertThat(true, equalTo(true)) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | @ -119,7 +121,7 @@ class WelcomeActivityTest { | |||
|                     .perform(ViewActions.click()) | ||||
|                 onView(withId(R.id.finishTutorialButton)) | ||||
|                     .perform(ViewActions.click()) | ||||
|                 assert(activityRule.activity.isDestroyed) | ||||
|                 assertThat(activityRule.activity.isDestroyed, equalTo(true)) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  |  | |||
|  | @ -13,7 +13,9 @@ | |||
|     android:maxSdkVersion="29"/> | ||||
|   <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> | ||||
|   <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" /> | ||||
|   <uses-permission android:name="android.permission.GET_ACCOUNTS" /> | ||||
|   <!-- Permission needed up to Android 5.1, see https://github.com/commons-app/apps-android-commons/pull/5863 --> | ||||
|   <uses-permission android:name="android.permission.GET_ACCOUNTS" | ||||
|     android:maxSdkVersion="22"/> | ||||
|   <uses-permission android:name="android.permission.USE_CREDENTIALS" /> | ||||
|   <uses-permission android:name="android.permission.MANAGE_ACCOUNTS" /> | ||||
|   <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> | ||||
|  | @ -97,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" /> | ||||
|  | @ -120,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" /> | ||||
|  |  | |||
|  | @ -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) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | @ -53,6 +53,7 @@ import fr.free.nrw.commons.utils.SystemThemeUtils; | |||
| import io.reactivex.android.schedulers.AndroidSchedulers; | ||||
| import io.reactivex.schedulers.Schedulers; | ||||
| import java.util.List; | ||||
| import java.util.Locale; | ||||
| import javax.inject.Inject; | ||||
| import javax.inject.Named; | ||||
| import org.osmdroid.tileprovider.tilesource.TileSourceFactory; | ||||
|  | @ -301,7 +302,8 @@ public class LocationPickerActivity extends BaseActivity implements | |||
|         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()); | ||||
|         showInMapButton.setText(getResources().getString(R.string.show_in_map_app).toUpperCase( | ||||
|             Locale.ROOT)); | ||||
|         shadow = findViewById(R.id.location_picker_image_view_shadow); | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|                     } | ||||
|  |  | |||
|  | @ -50,8 +50,8 @@ 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; | ||||
| import static fr.free.nrw.commons.CommonsApplication.LOGIN_MESSAGE_INTENT_KEY; | ||||
| import static fr.free.nrw.commons.CommonsApplication.LOGIN_USERNAME_INTENT_KEY; | ||||
| 
 | ||||
| public class LoginActivity extends AccountAuthenticatorActivity { | ||||
| 
 | ||||
|  | @ -94,8 +94,8 @@ public class LoginActivity extends AccountAuthenticatorActivity { | |||
|         binding = ActivityLoginBinding.inflate(getLayoutInflater()); | ||||
|         setContentView(binding.getRoot()); | ||||
| 
 | ||||
|         String message = getIntent().getStringExtra(loginMessageIntentKey); | ||||
|         String username = getIntent().getStringExtra(loginUsernameIntentKey); | ||||
|         String message = getIntent().getStringExtra(LOGIN_MESSAGE_INTENT_KEY); | ||||
|         String username = getIntent().getStringExtra(LOGIN_USERNAME_INTENT_KEY); | ||||
| 
 | ||||
|         binding.loginUsername.addTextChangedListener(textWatcher); | ||||
|         binding.loginPassword.addTextChangedListener(textWatcher); | ||||
|  |  | |||
|  | @ -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)); | ||||
|  |  | |||
|  | @ -124,7 +124,7 @@ class CategoryClient | |||
|                 }.map { | ||||
|                     it | ||||
|                         .filter { page -> | ||||
|                             page.categoryInfo() == null || !page.categoryInfo().isHidden | ||||
|                             !page.categoryInfo().isHidden | ||||
|                         }.map { | ||||
|                             CategoryItem( | ||||
|                                 it.title().replace(CATEGORY_PREFIX, ""), | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| package fr.free.nrw.commons.category; | ||||
| 
 | ||||
| import android.annotation.SuppressLint; | ||||
| import android.content.ContentProviderClient; | ||||
| import android.content.ContentValues; | ||||
| import android.database.Cursor; | ||||
|  | @ -111,6 +112,7 @@ public class CategoryDao { | |||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @SuppressLint("Range") | ||||
|     Category fromCursor(Cursor cursor) { | ||||
|         // Hardcoding column positions! | ||||
|         return new Category( | ||||
|  |  | |||
|  | @ -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; | ||||
|  | @ -65,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; | ||||
|         } | ||||
| 
 | ||||
|  | @ -76,12 +78,12 @@ 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, | ||||
|  | @ -95,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) { | ||||
|  | @ -104,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); | ||||
|                 } | ||||
|             } | ||||
|         }; | ||||
|  | @ -136,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), | ||||
|  | @ -149,20 +153,21 @@ 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)), | ||||
|  | @ -182,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), | ||||
|  | @ -191,12 +197,12 @@ 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); | ||||
|  | @ -205,18 +211,18 @@ public class ContributionController { | |||
|     /** | ||||
|      * 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); | ||||
|  | @ -226,12 +232,10 @@ public class ContributionController { | |||
|     /** | ||||
|      * 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()); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -248,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) { | ||||
|  |  | |||
|  | @ -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); | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -438,13 +438,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 { | ||||
|  |  | |||
|  | @ -0,0 +1,249 @@ | |||
| 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) { | ||||
|         val itemCount = countItemsInFolder(context, folder) | ||||
|         val folderPath = folder.absolutePath | ||||
| 
 | ||||
|         //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 = deleteFolderMain(context, folder, trashFolderLauncher) | ||||
|             onDeletionComplete(success) | ||||
| 
 | ||||
|         } else { | ||||
|             AlertDialog.Builder(context) | ||||
|                 .setTitle(context.getString(R.string.custom_selector_confirm_deletion_title)) | ||||
|                 .setMessage(context.getString(R.string.custom_selector_confirm_deletion_message, folderPath, itemCount)) | ||||
|                 .setPositiveButton(context.getString(R.string.custom_selector_delete)) { _, _ -> | ||||
| 
 | ||||
|                     //proceed with deletion if user confirms | ||||
|                     val success = deleteFolderMain(context, folder, trashFolderLauncher) | ||||
|                     onDeletionComplete(success) | ||||
|                 } | ||||
|                 .setNegativeButton(context.getString(R.string.custom_selector_cancel)) { dialog, _ -> | ||||
|                     dialog.dismiss() | ||||
|                     onDeletionComplete(false) | ||||
|                 } | ||||
|                 .show() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Deletes the specified folder, handling different Android storage models based on the API | ||||
|      * | ||||
|      * @param context The context used to manage storage operations. | ||||
|      * @param folder The folder to delete. | ||||
|      * @param trashFolderLauncher An ActivityResultLauncher for handling the result of the trash request. | ||||
|      * @return `true` if the folder deletion was successful, `false` otherwise. | ||||
|      */ | ||||
|     private fun deleteFolderMain( | ||||
|         context: Context, | ||||
|         folder: File, | ||||
|         trashFolderLauncher: ActivityResultLauncher<IntentSenderRequest>): Boolean | ||||
|     { | ||||
|         return when { | ||||
|             //for API 30 and above, use MediaStore | ||||
|             Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> trashFolderContents(context, folder, trashFolderLauncher) | ||||
| 
 | ||||
|             //for API 29 ('requestLegacyExternalStorage' is set to true in Manifest) | ||||
|             // and below use file system | ||||
|             else -> deleteFolderLegacy(folder) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Moves all contents of a specified folder 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 contents 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 trashFolderContents( | ||||
|         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>() | ||||
| 
 | ||||
|         // Use URIs specific to media items | ||||
|         val mediaUris = listOf( | ||||
|             MediaStore.Images.Media.EXTERNAL_CONTENT_URI, | ||||
|             MediaStore.Video.Media.EXTERNAL_CONTENT_URI, | ||||
|             MediaStore.Audio.Media.EXTERNAL_CONTENT_URI | ||||
|         ) | ||||
| 
 | ||||
|         for (mediaUri in mediaUris) { | ||||
|             val selection = "${MediaStore.MediaColumns.DATA} LIKE ?" | ||||
|             val selectionArgs = arrayOf("$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 | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * Counts the number of items in a specified folder, including items in subfolders. | ||||
|      * | ||||
|      * @param context The context used to access the content resolver. | ||||
|      * @param folder The folder in which to count items. | ||||
|      * @return The total number of items in the folder. | ||||
|      */ | ||||
|     private fun countItemsInFolder(context: Context, folder: File): Int { | ||||
|         val contentResolver = context.contentResolver | ||||
|         val folderPath = folder.absolutePath | ||||
|         val uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI | ||||
|         val selection = "${MediaStore.Images.Media.DATA} LIKE ?" | ||||
|         val selectionArgs = arrayOf("$folderPath/%") | ||||
| 
 | ||||
|         return contentResolver.query( | ||||
|             uri, | ||||
|             arrayOf(MediaStore.Images.Media._ID), | ||||
|             selection, | ||||
|             selectionArgs, | ||||
|             null)?.use { cursor -> | ||||
|             cursor.count | ||||
|         } ?: 0 | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * 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 specified folder and all of its contents on devices running | ||||
|      * Android 10 (API level 29) and below. | ||||
|      * | ||||
|      * @param folder The `File` object representing the folder to be deleted. | ||||
|      * @return `true` if the folder and all contents were deleted successfully; `false` otherwise. | ||||
|      */ | ||||
|     private fun deleteFolderLegacy(folder: File): Boolean { | ||||
|         return folder.deleteRecursively() | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * 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() | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  |  | |||
|  | @ -279,11 +279,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 { | ||||
|  | @ -382,14 +388,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 | ||||
|  |  | |||
|  | @ -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( | ||||
|  | @ -149,6 +156,15 @@ class DescriptionEditActivity : | |||
| 
 | ||||
|     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 +237,7 @@ class DescriptionEditActivity : | |||
|     ) { | ||||
|         try { | ||||
|             descriptionEditHelper | ||||
|                 ?.addDescription( | ||||
|                 .addDescription( | ||||
|                     applicationContext, | ||||
|                     media, | ||||
|                     updatedWikiText, | ||||
|  | @ -234,7 +250,7 @@ class DescriptionEditActivity : | |||
|                     ) | ||||
|                 } | ||||
|         } catch (e: InvalidLoginTokenException) { | ||||
|             val username: String? = sessionManager?.userName | ||||
|             val username: String? = sessionManager.userName | ||||
|             val logoutListener = | ||||
|                 CommonsApplication.BaseLogoutListener( | ||||
|                     this, | ||||
|  | @ -242,7 +258,7 @@ class DescriptionEditActivity : | |||
|                     username, | ||||
|                 ) | ||||
| 
 | ||||
|             val commonsApplication = CommonsApplication.getInstance() | ||||
|             val commonsApplication = CommonsApplication.instance | ||||
|             if (commonsApplication != null) { | ||||
|                 commonsApplication.clearApplicationData(this, logoutListener) | ||||
|             } | ||||
|  | @ -252,7 +268,7 @@ class DescriptionEditActivity : | |||
|         for (mediaDetail in uploadMediaDetails) { | ||||
|             try { | ||||
|                 compositeDisposable.add( | ||||
|                     descriptionEditHelper!! | ||||
|                     descriptionEditHelper | ||||
|                         .addCaption( | ||||
|                             applicationContext, | ||||
|                             media, | ||||
|  | @ -275,7 +291,7 @@ class DescriptionEditActivity : | |||
|                         username, | ||||
|                     ) | ||||
| 
 | ||||
|                 val commonsApplication = CommonsApplication.getInstance() | ||||
|                 val commonsApplication = CommonsApplication.instance | ||||
|                 if (commonsApplication != null) { | ||||
|                     commonsApplication.clearApplicationData(this, logoutListener) | ||||
|                 } | ||||
|  | @ -292,22 +308,6 @@ class DescriptionEditActivity : | |||
|         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) | ||||
| 
 | ||||
|  |  | |||
|  | @ -65,7 +65,6 @@ class TransformImageImpl : TransformImage { | |||
|             } catch (e: LLJTranException) { | ||||
|                 Timber.tag("Error").d(e) | ||||
|                 return null | ||||
|                 false | ||||
|             } | ||||
| 
 | ||||
|         if (rotated) { | ||||
|  |  | |||
|  | @ -22,6 +22,7 @@ import fr.free.nrw.commons.theme.BaseActivity; | |||
| import fr.free.nrw.commons.utils.ActivityUtils; | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
| import java.util.Locale; | ||||
| import javax.inject.Inject; | ||||
| import javax.inject.Named; | ||||
| 
 | ||||
|  | @ -112,13 +113,13 @@ public class ExploreFragment extends CommonsDaggerSupportFragment { | |||
|         mobileRootFragment = new ExploreListRootFragment(mobileArguments); | ||||
|         mapRootFragment = new ExploreMapRootFragment(mapArguments); | ||||
|         fragmentList.add(featuredRootFragment); | ||||
|         titleList.add(getString(R.string.explore_tab_title_featured).toUpperCase()); | ||||
|         titleList.add(getString(R.string.explore_tab_title_featured).toUpperCase(Locale.ROOT)); | ||||
| 
 | ||||
|         fragmentList.add(mobileRootFragment); | ||||
|         titleList.add(getString(R.string.explore_tab_title_mobile).toUpperCase()); | ||||
|         titleList.add(getString(R.string.explore_tab_title_mobile).toUpperCase(Locale.ROOT)); | ||||
| 
 | ||||
|         fragmentList.add(mapRootFragment); | ||||
|         titleList.add(getString(R.string.explore_tab_title_map).toUpperCase()); | ||||
|         titleList.add(getString(R.string.explore_tab_title_map).toUpperCase(Locale.ROOT)); | ||||
| 
 | ||||
|         ((MainActivity)getActivity()).showTabs(); | ||||
|         ((BaseActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false); | ||||
|  |  | |||
|  | @ -28,6 +28,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers; | |||
| import java.util.ArrayList; | ||||
| import java.util.Date; | ||||
| import java.util.List; | ||||
| import java.util.Locale; | ||||
| import java.util.concurrent.TimeUnit; | ||||
| import javax.inject.Inject; | ||||
| import timber.log.Timber; | ||||
|  | @ -95,11 +96,11 @@ public class SearchActivity extends BaseActivity | |||
|         searchDepictionsFragment = new SearchDepictionsFragment(); | ||||
|         searchCategoryFragment= new SearchCategoryFragment(); | ||||
|         fragmentList.add(searchMediaFragment); | ||||
|         titleList.add(getResources().getString(R.string.search_tab_title_media).toUpperCase()); | ||||
|         titleList.add(getResources().getString(R.string.search_tab_title_media).toUpperCase(Locale.ROOT)); | ||||
|         fragmentList.add(searchCategoryFragment); | ||||
|         titleList.add(getResources().getString(R.string.search_tab_title_categories).toUpperCase()); | ||||
|         titleList.add(getResources().getString(R.string.search_tab_title_categories).toUpperCase(Locale.ROOT)); | ||||
|         fragmentList.add(searchDepictionsFragment); | ||||
|         titleList.add(getResources().getString(R.string.search_tab_title_depictions).toUpperCase()); | ||||
|         titleList.add(getResources().getString(R.string.search_tab_title_depictions).toUpperCase(Locale.ROOT)); | ||||
| 
 | ||||
|         viewPagerAdapter.setTabData(fragmentList, titleList); | ||||
|         viewPagerAdapter.notifyDataSetChanged(); | ||||
|  |  | |||
|  | @ -18,6 +18,6 @@ class CategoriesMediaFragment : PageableMediaFragment() { | |||
|         savedInstanceState: Bundle?, | ||||
|     ) { | ||||
|         super.onViewCreated(view, savedInstanceState) | ||||
|         onQueryUpdated("$CATEGORY_PREFIX${arguments!!.getString("categoryName")!!}") | ||||
|         onQueryUpdated("$CATEGORY_PREFIX${requireArguments().getString("categoryName")!!}") | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -21,6 +21,6 @@ class ParentCategoriesFragment : PageableCategoryFragment() { | |||
|         savedInstanceState: Bundle?, | ||||
|     ) { | ||||
|         super.onViewCreated(view, savedInstanceState) | ||||
|         onQueryUpdated("$CATEGORY_PREFIX${arguments!!.getString("categoryName")!!}") | ||||
|         onQueryUpdated("$CATEGORY_PREFIX${requireArguments().getString("categoryName")!!}") | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -20,6 +20,6 @@ class SubCategoriesFragment : PageableCategoryFragment() { | |||
|         savedInstanceState: Bundle?, | ||||
|     ) { | ||||
|         super.onViewCreated(view, savedInstanceState) | ||||
|         onQueryUpdated("$CATEGORY_PREFIX${arguments!!.getString("categoryName")!!}") | ||||
|         onQueryUpdated("$CATEGORY_PREFIX${requireArguments().getString("categoryName")!!}") | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -13,13 +13,13 @@ class ChildDepictionsFragment : PageableDepictionsFragment() { | |||
|     override val injectedPresenter | ||||
|         get() = presenter | ||||
| 
 | ||||
|     override fun getEmptyText(query: String) = getString(R.string.no_child_classes, arguments!!.getString("wikidataItemName")!!) | ||||
|     override fun getEmptyText(query: String) = getString(R.string.no_child_classes, requireArguments().getString("wikidataItemName")!!) | ||||
| 
 | ||||
|     override fun onViewCreated( | ||||
|         view: View, | ||||
|         savedInstanceState: Bundle?, | ||||
|     ) { | ||||
|         super.onViewCreated(view, savedInstanceState) | ||||
|         onQueryUpdated(arguments!!.getString("entityId")!!) | ||||
|         onQueryUpdated(requireArguments().getString("entityId")!!) | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -17,6 +17,6 @@ class DepictedImagesFragment : PageableMediaFragment() { | |||
|         savedInstanceState: Bundle?, | ||||
|     ) { | ||||
|         super.onViewCreated(view, savedInstanceState) | ||||
|         onQueryUpdated(arguments!!.getString("entityId")!!) | ||||
|         onQueryUpdated(requireArguments().getString("entityId")!!) | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -13,13 +13,13 @@ class ParentDepictionsFragment : PageableDepictionsFragment() { | |||
|     override val injectedPresenter | ||||
|         get() = presenter | ||||
| 
 | ||||
|     override fun getEmptyText(query: String) = getString(R.string.no_parent_classes, arguments!!.getString("wikidataItemName")!!) | ||||
|     override fun getEmptyText(query: String) = getString(R.string.no_parent_classes, requireArguments().getString("wikidataItemName")!!) | ||||
| 
 | ||||
|     override fun onViewCreated( | ||||
|         view: View, | ||||
|         savedInstanceState: Bundle?, | ||||
|     ) { | ||||
|         super.onViewCreated(view, savedInstanceState) | ||||
|         onQueryUpdated(arguments!!.getString("entityId")!!) | ||||
|         onQueryUpdated(requireArguments().getString("entityId")!!) | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| package fr.free.nrw.commons.explore.recentsearches; | ||||
| 
 | ||||
| import android.annotation.SuppressLint; | ||||
| import android.content.ContentProviderClient; | ||||
| import android.content.ContentValues; | ||||
| import android.database.Cursor; | ||||
|  | @ -178,6 +179,7 @@ public class RecentSearchesDao { | |||
|      * @return RecentSearch object | ||||
|      */ | ||||
|     @NonNull | ||||
|     @SuppressLint("Range") | ||||
|     RecentSearch fromCursor(Cursor cursor) { | ||||
|         // Hardcoding column positions! | ||||
|         return new RecentSearch( | ||||
|  |  | |||
|  | @ -15,6 +15,7 @@ import fr.free.nrw.commons.databinding.FragmentSearchHistoryBinding; | |||
| import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; | ||||
| import fr.free.nrw.commons.explore.SearchActivity; | ||||
| import java.util.List; | ||||
| import java.util.Locale; | ||||
| import javax.inject.Inject; | ||||
| 
 | ||||
| 
 | ||||
|  | @ -90,7 +91,7 @@ public class RecentSearchesFragment extends CommonsDaggerSupportFragment { | |||
|     private void showDeleteAlertDialog(@NonNull final Context context, final int position) { | ||||
|         new AlertDialog.Builder(context) | ||||
|             .setMessage(R.string.delete_search_dialog) | ||||
|             .setPositiveButton(getString(R.string.delete).toUpperCase(), | ||||
|             .setPositiveButton(getString(R.string.delete).toUpperCase(Locale.ROOT), | ||||
|                 ((dialog, which) -> setDeletePositiveButton(context, dialog, position))) | ||||
|             .setNegativeButton(android.R.string.cancel, null) | ||||
|             .create() | ||||
|  |  | |||
|  | @ -4,20 +4,11 @@ public interface Constants { | |||
|     String DEFAULT_FOLDER_NAME = "CommonsContributions"; | ||||
| 
 | ||||
|     /** | ||||
|      * Provides the request codes utilised by the FilePicker | ||||
|      * Provides the request codes for permission handling | ||||
|      */ | ||||
|     interface RequestCodes { | ||||
|         int LOCATION = 1; | ||||
|         int STORAGE = 2; | ||||
|         int FILE_PICKER_IMAGE_IDENTIFICATOR = 0b1101101100; //876 | ||||
|         int SOURCE_CHOOSER = 1 << 15; | ||||
| 
 | ||||
|         int PICK_PICTURE_FROM_CUSTOM_SELECTOR = FILE_PICKER_IMAGE_IDENTIFICATOR + (1 << 10); | ||||
|         int PICK_PICTURE_FROM_DOCUMENTS = FILE_PICKER_IMAGE_IDENTIFICATOR + (1 << 11); | ||||
|         int PICK_PICTURE_FROM_GALLERY = FILE_PICKER_IMAGE_IDENTIFICATOR + (1 << 12); | ||||
|         int TAKE_PICTURE = FILE_PICKER_IMAGE_IDENTIFICATOR + (1 << 13); | ||||
| 
 | ||||
|         int RECEIVE_DATA_FROM_FULL_SCREEN_MODE = 1 << 9; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -29,4 +20,4 @@ public interface Constants { | |||
|         String COPY_TAKEN_PHOTOS = "fr.free.nrw.commons.copy_taken_photos"; | ||||
|         String COPY_PICKED_IMAGES = "fr.free.nrw.commons.copy_picked_images"; | ||||
|     } | ||||
| } | ||||
| } | ||||
|  |  | |||
|  | @ -12,6 +12,8 @@ import android.content.pm.ResolveInfo; | |||
| import android.net.Uri; | ||||
| import android.provider.MediaStore; | ||||
| import android.text.TextUtils; | ||||
| import androidx.activity.result.ActivityResult; | ||||
| import androidx.activity.result.ActivityResultLauncher; | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.preference.PreferenceManager; | ||||
|  | @ -107,31 +109,25 @@ public class FilePicker implements Constants { | |||
|      * | ||||
|      * @param type Custom type of your choice, which will be returned with the images | ||||
|      */ | ||||
|     public static void openGallery(Activity activity, int type, boolean openDocumentIntentPreferred) { | ||||
|     public static void openGallery(Activity activity, ActivityResultLauncher<Intent> resultLauncher, int type, boolean openDocumentIntentPreferred) { | ||||
|         Intent intent = createGalleryIntent(activity, type, openDocumentIntentPreferred); | ||||
|         int requestCode = RequestCodes.PICK_PICTURE_FROM_GALLERY; | ||||
| 
 | ||||
|             if(openDocumentIntentPreferred){ | ||||
|                 requestCode = RequestCodes.PICK_PICTURE_FROM_DOCUMENTS; | ||||
|             } | ||||
| 
 | ||||
|         activity.startActivityForResult(intent, requestCode); | ||||
|         resultLauncher.launch(intent); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Opens Custom Selector | ||||
|      */ | ||||
|     public static void openCustomSelector(Activity activity, int type) { | ||||
|     public static void openCustomSelector(Activity activity, ActivityResultLauncher<Intent> resultLauncher, int type) { | ||||
|         Intent intent = createCustomSelectorIntent(activity, type); | ||||
|         activity.startActivityForResult(intent, RequestCodes.PICK_PICTURE_FROM_CUSTOM_SELECTOR); | ||||
|         resultLauncher.launch(intent); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Opens the camera app to pick image clicked by user  | ||||
|      */ | ||||
|     public static void openCameraForImage(Activity activity, int type) { | ||||
|     public static void openCameraForImage(Activity activity, ActivityResultLauncher<Intent> resultLauncher, int type) { | ||||
|         Intent intent = createCameraForImageIntent(activity, type); | ||||
|         activity.startActivityForResult(intent, RequestCodes.TAKE_PICTURE); | ||||
|         resultLauncher.launch(intent); | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|  | @ -154,43 +150,6 @@ public class FilePicker implements Constants { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Any activity can use this method to attach their callback to the file picker | ||||
|      */ | ||||
|     public static void handleActivityResult(int requestCode, int resultCode, Intent data, Activity activity, @NonNull FilePicker.Callbacks callbacks) { | ||||
|         boolean isHandledPickedFile = (requestCode & RequestCodes.FILE_PICKER_IMAGE_IDENTIFICATOR) > 0; | ||||
|         if (isHandledPickedFile) { | ||||
|             requestCode &= ~RequestCodes.SOURCE_CHOOSER; | ||||
|             if (requestCode == RequestCodes.PICK_PICTURE_FROM_GALLERY || | ||||
|                     requestCode == RequestCodes.TAKE_PICTURE || | ||||
|                     requestCode == RequestCodes.PICK_PICTURE_FROM_DOCUMENTS || | ||||
|                     requestCode == RequestCodes.PICK_PICTURE_FROM_CUSTOM_SELECTOR) { | ||||
|                 if (resultCode == Activity.RESULT_OK) { | ||||
|                     if (requestCode == RequestCodes.PICK_PICTURE_FROM_DOCUMENTS && !isPhoto(data)) { | ||||
|                         onPictureReturnedFromDocuments(data, activity, callbacks); | ||||
|                     } else if (requestCode == RequestCodes.PICK_PICTURE_FROM_GALLERY && !isPhoto(data)) { | ||||
|                         onPictureReturnedFromGallery(data, activity, callbacks); | ||||
|                     } else if (requestCode == RequestCodes.PICK_PICTURE_FROM_CUSTOM_SELECTOR) { | ||||
|                         onPictureReturnedFromCustomSelector(data, activity, callbacks); | ||||
|                     } else if (requestCode == RequestCodes.TAKE_PICTURE) { | ||||
|                         onPictureReturnedFromCamera(activity, callbacks); | ||||
|                     } | ||||
|                 } else { | ||||
|                     if (requestCode == RequestCodes.PICK_PICTURE_FROM_DOCUMENTS) { | ||||
|                         callbacks.onCanceled(FilePicker.ImageSource.DOCUMENTS, restoreType(activity)); | ||||
|                     } else if (requestCode == RequestCodes.PICK_PICTURE_FROM_GALLERY) { | ||||
|                         callbacks.onCanceled(FilePicker.ImageSource.GALLERY, restoreType(activity)); | ||||
|                     } else if (requestCode == RequestCodes.PICK_PICTURE_FROM_CUSTOM_SELECTOR){ | ||||
|                         callbacks.onCanceled(ImageSource.CUSTOM_SELECTOR, restoreType(activity)); | ||||
|                     } | ||||
|                     else { | ||||
|                         callbacks.onCanceled(FilePicker.ImageSource.CAMERA_IMAGE, restoreType(activity)); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static List<UploadableFile> handleExternalImagesPicked(Intent data, Activity activity) { | ||||
|         try { | ||||
|             return getFilesFromGalleryPictures(data, activity); | ||||
|  | @ -243,18 +202,22 @@ public class FilePicker implements Constants { | |||
|         return intent; | ||||
|     } | ||||
| 
 | ||||
|     private static void onPictureReturnedFromDocuments(Intent data, Activity activity, @NonNull FilePicker.Callbacks callbacks) { | ||||
|         try { | ||||
|             Uri photoPath = data.getData(); | ||||
|             UploadableFile photoFile = PickedFiles.pickedExistingPicture(activity, photoPath); | ||||
|             callbacks.onImagesPicked(singleFileList(photoFile), FilePicker.ImageSource.DOCUMENTS, restoreType(activity)); | ||||
|     public static void onPictureReturnedFromDocuments(ActivityResult result, Activity activity, @NonNull FilePicker.Callbacks callbacks) { | ||||
|         if(result.getResultCode() == Activity.RESULT_OK && !isPhoto(result.getData())){ | ||||
|             try { | ||||
|                 Uri photoPath = result.getData().getData(); | ||||
|                 UploadableFile photoFile = PickedFiles.pickedExistingPicture(activity, photoPath); | ||||
|                 callbacks.onImagesPicked(singleFileList(photoFile), FilePicker.ImageSource.DOCUMENTS, restoreType(activity)); | ||||
| 
 | ||||
|             if (configuration(activity).shouldCopyPickedImagesToPublicGalleryAppFolder()) { | ||||
|                 PickedFiles.copyFilesInSeparateThread(activity, singleFileList(photoFile)); | ||||
|                 if (configuration(activity).shouldCopyPickedImagesToPublicGalleryAppFolder()) { | ||||
|                     PickedFiles.copyFilesInSeparateThread(activity, singleFileList(photoFile)); | ||||
|                 } | ||||
|             } catch (Exception e) { | ||||
|                 e.printStackTrace(); | ||||
|                 callbacks.onImagePickerError(e, FilePicker.ImageSource.DOCUMENTS, restoreType(activity)); | ||||
|             } | ||||
|         } catch (Exception e) { | ||||
|             e.printStackTrace(); | ||||
|             callbacks.onImagePickerError(e, FilePicker.ImageSource.DOCUMENTS, restoreType(activity)); | ||||
|         } else { | ||||
|             callbacks.onCanceled(FilePicker.ImageSource.DOCUMENTS, restoreType(activity)); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -262,14 +225,18 @@ public class FilePicker implements Constants { | |||
|      * onPictureReturnedFromCustomSelector. | ||||
|      * Retrieve and forward the images to upload wizard through callback. | ||||
|      */ | ||||
|     private static void onPictureReturnedFromCustomSelector(Intent data, Activity activity, @NonNull FilePicker.Callbacks callbacks) { | ||||
|         try { | ||||
|             List<UploadableFile> files = getFilesFromCustomSelector(data, activity); | ||||
|             callbacks.onImagesPicked(files, ImageSource.CUSTOM_SELECTOR, restoreType(activity)); | ||||
|         } catch (Exception e) { | ||||
|             e.printStackTrace(); | ||||
|             callbacks.onImagePickerError(e, ImageSource.CUSTOM_SELECTOR, restoreType(activity)); | ||||
|         } | ||||
|     public static void onPictureReturnedFromCustomSelector(ActivityResult result, Activity activity, @NonNull FilePicker.Callbacks callbacks) { | ||||
|        if(result.getResultCode() == Activity.RESULT_OK){ | ||||
|            try { | ||||
|                List<UploadableFile> files = getFilesFromCustomSelector(result.getData(), activity); | ||||
|                callbacks.onImagesPicked(files, ImageSource.CUSTOM_SELECTOR, restoreType(activity)); | ||||
|            } catch (Exception e) { | ||||
|                e.printStackTrace(); | ||||
|                callbacks.onImagePickerError(e, ImageSource.CUSTOM_SELECTOR, restoreType(activity)); | ||||
|            } | ||||
|        } else { | ||||
|            callbacks.onCanceled(ImageSource.CUSTOM_SELECTOR, restoreType(activity)); | ||||
|        } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -292,13 +259,17 @@ public class FilePicker implements Constants { | |||
|         return files; | ||||
|     } | ||||
| 
 | ||||
|     private static void onPictureReturnedFromGallery(Intent data, Activity activity, @NonNull FilePicker.Callbacks callbacks) { | ||||
|         try { | ||||
|             List<UploadableFile> files = getFilesFromGalleryPictures(data, activity); | ||||
|             callbacks.onImagesPicked(files, FilePicker.ImageSource.GALLERY, restoreType(activity)); | ||||
|         } catch (Exception e) { | ||||
|             e.printStackTrace(); | ||||
|             callbacks.onImagePickerError(e, FilePicker.ImageSource.GALLERY, restoreType(activity)); | ||||
|     public static void onPictureReturnedFromGallery(ActivityResult result, Activity activity, @NonNull FilePicker.Callbacks callbacks) { | ||||
|         if(result.getResultCode() == Activity.RESULT_OK && !isPhoto(result.getData())){ | ||||
|             try { | ||||
|                 List<UploadableFile> files = getFilesFromGalleryPictures(result.getData(), activity); | ||||
|                 callbacks.onImagesPicked(files, FilePicker.ImageSource.GALLERY, restoreType(activity)); | ||||
|             } catch (Exception e) { | ||||
|                 e.printStackTrace(); | ||||
|                 callbacks.onImagePickerError(e, FilePicker.ImageSource.GALLERY, restoreType(activity)); | ||||
|             } | ||||
|         } else{ | ||||
|             callbacks.onCanceled(FilePicker.ImageSource.GALLERY, restoreType(activity)); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -324,69 +295,40 @@ public class FilePicker implements Constants { | |||
|         return files; | ||||
|     } | ||||
| 
 | ||||
|     private static void onPictureReturnedFromCamera(Activity activity, @NonNull FilePicker.Callbacks callbacks) { | ||||
|         try { | ||||
|             String lastImageUri = PreferenceManager.getDefaultSharedPreferences(activity).getString(KEY_PHOTO_URI, null); | ||||
|             if (!TextUtils.isEmpty(lastImageUri)) { | ||||
|                 revokeWritePermission(activity, Uri.parse(lastImageUri)); | ||||
|             } | ||||
| 
 | ||||
|             UploadableFile photoFile = FilePicker.takenCameraPicture(activity); | ||||
|             List<UploadableFile> files = new ArrayList<>(); | ||||
|             files.add(photoFile); | ||||
| 
 | ||||
|             if (photoFile == null) { | ||||
|                 Exception e = new IllegalStateException("Unable to get the picture returned from camera"); | ||||
|                 callbacks.onImagePickerError(e, FilePicker.ImageSource.CAMERA_IMAGE, restoreType(activity)); | ||||
|             } else { | ||||
|                 if (configuration(activity).shouldCopyTakenPhotosToPublicGalleryAppFolder()) { | ||||
|                     PickedFiles.copyFilesInSeparateThread(activity, singleFileList(photoFile)); | ||||
|     public static void onPictureReturnedFromCamera(ActivityResult activityResult, Activity activity, @NonNull FilePicker.Callbacks callbacks) { | ||||
|         if(activityResult.getResultCode() == Activity.RESULT_OK){ | ||||
|             try { | ||||
|                 String lastImageUri = PreferenceManager.getDefaultSharedPreferences(activity).getString(KEY_PHOTO_URI, null); | ||||
|                 if (!TextUtils.isEmpty(lastImageUri)) { | ||||
|                     revokeWritePermission(activity, Uri.parse(lastImageUri)); | ||||
|                 } | ||||
| 
 | ||||
|                 callbacks.onImagesPicked(files, FilePicker.ImageSource.CAMERA_IMAGE, restoreType(activity)); | ||||
|             } | ||||
|                 UploadableFile photoFile = FilePicker.takenCameraPicture(activity); | ||||
|                 List<UploadableFile> files = new ArrayList<>(); | ||||
|                 files.add(photoFile); | ||||
| 
 | ||||
|             PreferenceManager.getDefaultSharedPreferences(activity) | ||||
|                 if (photoFile == null) { | ||||
|                     Exception e = new IllegalStateException("Unable to get the picture returned from camera"); | ||||
|                     callbacks.onImagePickerError(e, FilePicker.ImageSource.CAMERA_IMAGE, restoreType(activity)); | ||||
|                 } else { | ||||
|                     if (configuration(activity).shouldCopyTakenPhotosToPublicGalleryAppFolder()) { | ||||
|                         PickedFiles.copyFilesInSeparateThread(activity, singleFileList(photoFile)); | ||||
|                     } | ||||
| 
 | ||||
|                     callbacks.onImagesPicked(files, FilePicker.ImageSource.CAMERA_IMAGE, restoreType(activity)); | ||||
|                 } | ||||
| 
 | ||||
|                 PreferenceManager.getDefaultSharedPreferences(activity) | ||||
|                     .edit() | ||||
|                     .remove(KEY_LAST_CAMERA_PHOTO) | ||||
|                     .remove(KEY_PHOTO_URI) | ||||
|                     .apply(); | ||||
|         } catch (Exception e) { | ||||
|             e.printStackTrace(); | ||||
|             callbacks.onImagePickerError(e, FilePicker.ImageSource.CAMERA_IMAGE, restoreType(activity)); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private static void onVideoReturnedFromCamera(Activity activity, @NonNull FilePicker.Callbacks callbacks) { | ||||
|         try { | ||||
|             String lastVideoUri = PreferenceManager.getDefaultSharedPreferences(activity).getString(KEY_VIDEO_URI, null); | ||||
|             if (!TextUtils.isEmpty(lastVideoUri)) { | ||||
|                 revokeWritePermission(activity, Uri.parse(lastVideoUri)); | ||||
|             } catch (Exception e) { | ||||
|                 e.printStackTrace(); | ||||
|                 callbacks.onImagePickerError(e, FilePicker.ImageSource.CAMERA_IMAGE, restoreType(activity)); | ||||
|             } | ||||
| 
 | ||||
|             UploadableFile photoFile = FilePicker.takenCameraVideo(activity); | ||||
|             List<UploadableFile> files = new ArrayList<>(); | ||||
|             files.add(photoFile); | ||||
| 
 | ||||
|             if (photoFile == null) { | ||||
|                 Exception e = new IllegalStateException("Unable to get the video returned from camera"); | ||||
|                 callbacks.onImagePickerError(e, FilePicker.ImageSource.CAMERA_VIDEO, restoreType(activity)); | ||||
|             } else { | ||||
|                 if (configuration(activity).shouldCopyTakenPhotosToPublicGalleryAppFolder()) { | ||||
|                     PickedFiles.copyFilesInSeparateThread(activity, singleFileList(photoFile)); | ||||
|                 } | ||||
| 
 | ||||
|                 callbacks.onImagesPicked(files, FilePicker.ImageSource.CAMERA_VIDEO, restoreType(activity)); | ||||
|             } | ||||
| 
 | ||||
|             PreferenceManager.getDefaultSharedPreferences(activity) | ||||
|                     .edit() | ||||
|                     .remove(KEY_LAST_CAMERA_VIDEO) | ||||
|                     .remove(KEY_VIDEO_URI) | ||||
|                     .apply(); | ||||
|         } catch (Exception e) { | ||||
|             e.printStackTrace(); | ||||
|             callbacks.onImagePickerError(e, FilePicker.ImageSource.CAMERA_VIDEO, restoreType(activity)); | ||||
|         } else { | ||||
|             callbacks.onCanceled(FilePicker.ImageSource.CAMERA_IMAGE, restoreType(activity)); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -406,4 +348,8 @@ public class FilePicker implements Constants { | |||
| 
 | ||||
|         void onCanceled(FilePicker.ImageSource source, int type); | ||||
|     } | ||||
| 
 | ||||
|     public interface HandleActivityResult{ | ||||
|         void onHandleActivityResult(FilePicker.Callbacks callbacks); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -1,13 +1,10 @@ | |||
| package fr.free.nrw.commons.media; | ||||
| 
 | ||||
| import static android.app.Activity.RESULT_CANCELED; | ||||
| import static android.app.Activity.RESULT_OK; | ||||
| import static android.view.View.GONE; | ||||
| import static android.view.View.VISIBLE; | ||||
| import static fr.free.nrw.commons.category.CategoryClientKt.CATEGORY_NEEDING_CATEGORIES; | ||||
| import static fr.free.nrw.commons.category.CategoryClientKt.CATEGORY_UNCATEGORISED; | ||||
| import static fr.free.nrw.commons.description.EditDescriptionConstants.LIST_OF_DESCRIPTION_AND_CAPTION; | ||||
| import static fr.free.nrw.commons.description.EditDescriptionConstants.UPDATED_WIKITEXT; | ||||
| import static fr.free.nrw.commons.description.EditDescriptionConstants.WIKITEXT; | ||||
| import static fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.LAST_LOCATION; | ||||
| import static fr.free.nrw.commons.utils.LangCodeUtils.getLocalizedResources; | ||||
|  | @ -112,8 +109,6 @@ import timber.log.Timber; | |||
| public class MediaDetailFragment extends CommonsDaggerSupportFragment implements | ||||
|     CategoryEditHelper.Callback { | ||||
| 
 | ||||
|     private static final int REQUEST_CODE = 1001; | ||||
|     private static final int REQUEST_CODE_EDIT_DESCRIPTION = 1002; | ||||
|     private static final String IMAGE_BACKGROUND_COLOR = "image_background_color"; | ||||
|     static final int DEFAULT_IMAGE_BACKGROUND_COLOR = 0; | ||||
|      | ||||
|  | @ -277,6 +272,12 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements | |||
| 
 | ||||
|         if (!sessionManager.isUserLoggedIn()) { | ||||
|             binding.categoryEditButton.setVisibility(GONE); | ||||
|             binding.descriptionEdit.setVisibility(GONE); | ||||
|             binding.depictionsEditButton.setVisibility(GONE); | ||||
|         } else { | ||||
|             binding.categoryEditButton.setVisibility(VISIBLE); | ||||
|             binding.descriptionEdit.setVisibility(VISIBLE); | ||||
|             binding.depictionsEditButton.setVisibility(VISIBLE); | ||||
|         } | ||||
| 
 | ||||
|         if(applicationKvStore.getBoolean("login_skipped")){ | ||||
|  | @ -405,7 +406,6 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements | |||
|             } | ||||
|         ); | ||||
|         binding.progressBarEdit.setVisibility(GONE); | ||||
|         binding.descriptionEdit.setVisibility(VISIBLE); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|  | @ -605,8 +605,8 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements | |||
|         // Check if the presented category is about need of category | ||||
|         if (categoriesPresent) { | ||||
|             for (String category : media.getCategories()) { | ||||
|                 if (category.toLowerCase().contains(CATEGORY_NEEDING_CATEGORIES) || | ||||
|                     category.toLowerCase().contains(CATEGORY_UNCATEGORISED)) { | ||||
|                 if (category.toLowerCase(Locale.ROOT).contains(CATEGORY_NEEDING_CATEGORIES) || | ||||
|                     category.toLowerCase(Locale.ROOT).contains(CATEGORY_UNCATEGORISED)) { | ||||
|                     categoriesPresent = false; | ||||
|                 } | ||||
|                 break; | ||||
|  | @ -683,7 +683,9 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements | |||
|             // Stick in a filler element. | ||||
|             allCategories.add(getString(R.string.detail_panel_cats_none)); | ||||
|         } | ||||
|         binding.categoryEditButton.setVisibility(VISIBLE); | ||||
|         if(sessionManager.isUserLoggedIn()) { | ||||
|             binding.categoryEditButton.setVisibility(VISIBLE); | ||||
|         } | ||||
|         rebuildCatList(allCategories); | ||||
|     } | ||||
| 
 | ||||
|  | @ -1065,81 +1067,6 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements | |||
|         return captionList; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the result from another activity and act accordingly. | ||||
|      * @param requestCode | ||||
|      * @param resultCode | ||||
|      * @param data | ||||
|      */ | ||||
|     @Override | ||||
|     public void onActivityResult(final int requestCode, final int resultCode, | ||||
|         @Nullable final Intent data) { | ||||
|         super.onActivityResult(requestCode, resultCode, data); | ||||
| 
 | ||||
|         if (requestCode == REQUEST_CODE_EDIT_DESCRIPTION && resultCode == RESULT_OK) { | ||||
|             final String updatedWikiText = data.getStringExtra(UPDATED_WIKITEXT); | ||||
| 
 | ||||
|             try { | ||||
|                 compositeDisposable.add(descriptionEditHelper.addDescription(getContext(), media, | ||||
|                         updatedWikiText) | ||||
|                     .subscribeOn(Schedulers.io()) | ||||
|                     .observeOn(AndroidSchedulers.mainThread()) | ||||
|                     .subscribe(s -> { | ||||
|                         Timber.d("Descriptions 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( | ||||
|                         getActivity(), | ||||
|                         requireActivity().getString(R.string.invalid_login_message), | ||||
|                         username | ||||
|                     ); | ||||
| 
 | ||||
|                     CommonsApplication.getInstance().clearApplicationData( | ||||
|                         requireActivity(), logoutListener); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             final ArrayList<UploadMediaDetail> uploadMediaDetails | ||||
|                 = data.getParcelableArrayListExtra(LIST_OF_DESCRIPTION_AND_CAPTION); | ||||
| 
 | ||||
|             LinkedHashMap<String, String> updatedCaptions = new LinkedHashMap<>(); | ||||
|             for (UploadMediaDetail mediaDetail: | ||||
|             uploadMediaDetails) { | ||||
|                 try { | ||||
|                     compositeDisposable.add(descriptionEditHelper.addCaption(getContext(), media, | ||||
|                             mediaDetail.getLanguageCode(), mediaDetail.getCaptionText()) | ||||
|                         .subscribeOn(Schedulers.io()) | ||||
|                         .observeOn(AndroidSchedulers.mainThread()) | ||||
|                         .subscribe(s -> { | ||||
|                                 updateCaptions(mediaDetail, updatedCaptions); | ||||
|                                 Timber.d("Caption is added."); | ||||
|                             })); | ||||
| 
 | ||||
|                 } catch (Exception e) { | ||||
|                     if (e.getLocalizedMessage().equals(CsrfTokenClient.ANONYMOUS_TOKEN_MESSAGE)) { | ||||
|                         final String username = sessionManager.getUserName(); | ||||
|                         final CommonsApplication.BaseLogoutListener logoutListener = new CommonsApplication.BaseLogoutListener( | ||||
|                             getActivity(), | ||||
|                             requireActivity().getString(R.string.invalid_login_message), | ||||
|                             username | ||||
|                         ); | ||||
| 
 | ||||
|                         CommonsApplication.getInstance().clearApplicationData( | ||||
|                             requireActivity(), logoutListener); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             binding.progressBarEdit.setVisibility(GONE); | ||||
|             binding.descriptionEdit.setVisibility(VISIBLE); | ||||
| 
 | ||||
|         } else if (requestCode == REQUEST_CODE_EDIT_DESCRIPTION && resultCode == RESULT_CANCELED) { | ||||
|             binding.progressBarEdit.setVisibility(GONE); | ||||
|             binding.descriptionEdit.setVisibility(VISIBLE); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Adds caption to the map and updates captions | ||||
|      * @param mediaDetail UploadMediaDetail | ||||
|  |  | |||
|  | @ -219,7 +219,7 @@ class ZoomableActivity : BaseActivity() { | |||
|                 onSwipe() | ||||
|             } | ||||
|         } | ||||
|         binding.zoomProgressBar?.let { | ||||
|         binding.zoomProgressBar.let { | ||||
|             it.visibility = if (result.status is CallbackStatus.FETCHING) View.VISIBLE else View.GONE | ||||
|         } | ||||
|     } | ||||
|  | @ -234,7 +234,7 @@ class ZoomableActivity : BaseActivity() { | |||
|             sharedPreferences.getBoolean(ImageHelper.SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY, true) | ||||
| 
 | ||||
|         if (!images.isNullOrEmpty()) { | ||||
|             binding.zoomable!!.setOnTouchListener( | ||||
|             binding.zoomable.setOnTouchListener( | ||||
|                 object : OnSwipeTouchListener(this) { | ||||
|                     // Swipe left to view next image in the folder. (if available) | ||||
|                     override fun onSwipeLeft() { | ||||
|  | @ -271,7 +271,7 @@ class ZoomableActivity : BaseActivity() { | |||
|      * Handles down swipe action | ||||
|      */ | ||||
|     private fun onDownSwiped() { | ||||
|         if (binding.zoomable?.zoomableController?.isIdentity == false) { | ||||
|         if (binding.zoomable.zoomableController?.isIdentity == false) { | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
|  | @ -341,7 +341,7 @@ class ZoomableActivity : BaseActivity() { | |||
|      * Handles up swipe action | ||||
|      */ | ||||
|     private fun onUpSwiped() { | ||||
|         if (binding.zoomable?.zoomableController?.isIdentity == false) { | ||||
|         if (binding.zoomable.zoomableController?.isIdentity == false) { | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
|  | @ -414,7 +414,7 @@ class ZoomableActivity : BaseActivity() { | |||
|      * Handles right swipe action | ||||
|      */ | ||||
|     private fun onRightSwiped(showAlreadyActionedImages: Boolean) { | ||||
|         if (binding.zoomable?.zoomableController?.isIdentity == false) { | ||||
|         if (binding.zoomable.zoomableController?.isIdentity == false) { | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
|  | @ -451,7 +451,7 @@ class ZoomableActivity : BaseActivity() { | |||
|      * Handles left swipe action | ||||
|      */ | ||||
|     private fun onLeftSwiped(showAlreadyActionedImages: Boolean) { | ||||
|         if (binding.zoomable?.zoomableController?.isIdentity == false) { | ||||
|         if (binding.zoomable.zoomableController?.isIdentity == false) { | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
|  | @ -646,7 +646,7 @@ class ZoomableActivity : BaseActivity() { | |||
|                     .setProgressBarImage(ProgressBarDrawable()) | ||||
|                     .setProgressBarImageScaleType(ScalingUtils.ScaleType.FIT_CENTER) | ||||
|                     .build() | ||||
|             with(binding.zoomable!!) { | ||||
|             with(binding.zoomable) { | ||||
|                 setHierarchy(hierarchy) | ||||
|                 setAllowTouchInterceptionWhileZoomed(true) | ||||
|                 setIsLongpressEnabled(false) | ||||
|  | @ -658,10 +658,10 @@ class ZoomableActivity : BaseActivity() { | |||
|                     .setUri(imageUri) | ||||
|                     .setControllerListener(loadingListener) | ||||
|                     .build() | ||||
|             binding.zoomable!!.controller = controller | ||||
|             binding.zoomable.controller = controller | ||||
| 
 | ||||
|             if (photoBackgroundColor != null) { | ||||
|                 binding.zoomable!!.setBackgroundColor(photoBackgroundColor!!) | ||||
|                 binding.zoomable.setBackgroundColor(photoBackgroundColor!!) | ||||
|             } | ||||
| 
 | ||||
|             if (!images.isNullOrEmpty()) { | ||||
|  |  | |||
|  | @ -285,7 +285,7 @@ public class OkHttpJsonApiClient { | |||
|         throws Exception { | ||||
| 
 | ||||
|         Timber.d("Fetching nearby items at radius %s", radius); | ||||
|         Timber.d("CUSTOM_SPARQL%s", String.valueOf(customQuery != null)); | ||||
|         Timber.d("CUSTOM_SPARQL: %s", String.valueOf(customQuery != null)); | ||||
|         final String wikidataQuery; | ||||
|         if (customQuery != null) { | ||||
|             wikidataQuery = customQuery; | ||||
|  | @ -344,7 +344,7 @@ public class OkHttpJsonApiClient { | |||
|         final boolean shouldQueryForMonuments, final String customQuery) | ||||
|         throws Exception { | ||||
| 
 | ||||
|         Timber.d("CUSTOM_SPARQL%s", String.valueOf(customQuery != null)); | ||||
|         Timber.d("CUSTOM_SPARQL: %s", String.valueOf(customQuery != null)); | ||||
| 
 | ||||
|         final String wikidataQuery; | ||||
|         if (customQuery != null) { | ||||
|  |  | |||
|  | @ -17,6 +17,7 @@ import androidx.recyclerview.widget.RecyclerView; | |||
| import java.util.ArrayList; | ||||
| 
 | ||||
| import fr.free.nrw.commons.R; | ||||
| import java.util.Locale; | ||||
| 
 | ||||
| public class NearbyFilterSearchRecyclerViewAdapter | ||||
|         extends RecyclerView.Adapter<NearbyFilterSearchRecyclerViewAdapter.RecyclerViewHolder> | ||||
|  | @ -121,11 +122,11 @@ public class NearbyFilterSearchRecyclerViewAdapter | |||
|                     results.count = labels.size(); | ||||
|                     results.values = labels; | ||||
|                 } else { | ||||
|                     constraint = constraint.toString().toLowerCase(); | ||||
|                     constraint = constraint.toString().toLowerCase(Locale.ROOT); | ||||
| 
 | ||||
|                     for (Label label : labels) { | ||||
|                         String data = label.toString(); | ||||
|                         if (data.toLowerCase().startsWith(constraint.toString())) { | ||||
|                         if (data.toLowerCase(Locale.ROOT).startsWith(constraint.toString())) { | ||||
|                             filteredArrayList.add(Label.fromText(label.getText())); | ||||
|                         } | ||||
|                     } | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| package fr.free.nrw.commons.nearby | ||||
| 
 | ||||
| import android.content.Intent | ||||
| import android.view.View | ||||
| import android.view.View.GONE | ||||
| import android.view.View.INVISIBLE | ||||
|  | @ -17,9 +18,9 @@ import fr.free.nrw.commons.databinding.ItemPlaceBinding | |||
| fun placeAdapterDelegate( | ||||
|     bookmarkLocationDao: BookmarkLocationsDao, | ||||
|     onItemClick: ((Place) -> Unit)? = null, | ||||
|     onCameraClicked: (Place, ActivityResultLauncher<Array<String>>) -> Unit, | ||||
|     onCameraClicked: (Place, ActivityResultLauncher<Array<String>>, ActivityResultLauncher<Intent>) -> Unit, | ||||
|     onCameraLongPressed: () -> Boolean, | ||||
|     onGalleryClicked: (Place) -> Unit, | ||||
|     onGalleryClicked: (Place, ActivityResultLauncher<Intent>) -> Unit, | ||||
|     onGalleryLongPressed: () -> Boolean, | ||||
|     onBookmarkClicked: (Place, Boolean) -> Unit, | ||||
|     onBookmarkLongPressed: () -> Boolean, | ||||
|  | @ -28,6 +29,8 @@ fun placeAdapterDelegate( | |||
|     onDirectionsClicked: (Place) -> Unit, | ||||
|     onDirectionsLongPressed: () -> Boolean, | ||||
|     inAppCameraLocationPermissionLauncher: ActivityResultLauncher<Array<String>>, | ||||
|     cameraPickLauncherForResult: ActivityResultLauncher<Intent>, | ||||
|     galleryPickLauncherForResult: ActivityResultLauncher<Intent> | ||||
| ) = adapterDelegateViewBinding<Place, Place, ItemPlaceBinding>({ layoutInflater, parent -> | ||||
|     ItemPlaceBinding.inflate(layoutInflater, parent, false) | ||||
| }) { | ||||
|  | @ -44,10 +47,10 @@ fun placeAdapterDelegate( | |||
|                 onItemClick?.invoke(item) | ||||
|             } | ||||
|         } | ||||
|         nearbyButtonLayout.cameraButton.setOnClickListener { onCameraClicked(item, inAppCameraLocationPermissionLauncher) } | ||||
|         nearbyButtonLayout.cameraButton.setOnClickListener { onCameraClicked(item, inAppCameraLocationPermissionLauncher, cameraPickLauncherForResult) } | ||||
|         nearbyButtonLayout.cameraButton.setOnLongClickListener { onCameraLongPressed() } | ||||
| 
 | ||||
|         nearbyButtonLayout.galleryButton.setOnClickListener { onGalleryClicked(item) } | ||||
|         nearbyButtonLayout.galleryButton.setOnClickListener { onGalleryClicked(item, galleryPickLauncherForResult) } | ||||
|         nearbyButtonLayout.galleryButton.setOnLongClickListener { onGalleryLongPressed() } | ||||
|         bookmarkButtonImage.setOnClickListener { | ||||
|             val isBookmarked = bookmarkLocationDao.updateBookmarkLocation(item) | ||||
|  |  | |||
|  | @ -4,7 +4,6 @@ import androidx.room.Dao; | |||
| import androidx.room.Insert; | ||||
| import androidx.room.OnConflictStrategy; | ||||
| import androidx.room.Query; | ||||
| import fr.free.nrw.commons.location.LatLng; | ||||
| import io.reactivex.Completable; | ||||
| 
 | ||||
| /** | ||||
|  | @ -38,8 +37,21 @@ public abstract class PlaceDao { | |||
|      */ | ||||
|     public Completable save(final Place place) { | ||||
|         return Completable | ||||
|             .fromAction(() -> { | ||||
|                 saveSynchronous(place); | ||||
|             }); | ||||
|             .fromAction(() -> saveSynchronous(place)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Deletes all Place objects from the database. | ||||
|      */ | ||||
|     @Query("DELETE FROM place") | ||||
|     public abstract void deleteAllSynchronous(); | ||||
| 
 | ||||
|     /** | ||||
|      * Deletes all Place objects from the database. | ||||
|      * | ||||
|      * @return A Completable that completes once the deletion operation is done. | ||||
|      */ | ||||
|     public Completable deleteAll() { | ||||
|         return Completable.fromAction(this::deleteAllSynchronous); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -1,6 +1,5 @@ | |||
| package fr.free.nrw.commons.nearby; | ||||
| 
 | ||||
| import fr.free.nrw.commons.location.LatLng; | ||||
| import io.reactivex.Completable; | ||||
| import javax.inject.Inject; | ||||
| 
 | ||||
|  | @ -36,4 +35,8 @@ public class PlacesLocalDataSource { | |||
|     public Completable savePlace(Place place) { | ||||
|         return placeDao.save(place); | ||||
|     } | ||||
| 
 | ||||
|     public Completable clearCache() { | ||||
|         return placeDao.deleteAll(); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -3,6 +3,7 @@ package fr.free.nrw.commons.nearby; | |||
| import fr.free.nrw.commons.contributions.Contribution; | ||||
| import fr.free.nrw.commons.location.LatLng; | ||||
| import io.reactivex.Completable; | ||||
| import io.reactivex.schedulers.Schedulers; | ||||
| import javax.inject.Inject; | ||||
| 
 | ||||
| /** | ||||
|  | @ -38,4 +39,13 @@ public class PlacesRepository { | |||
|         return localDataSource.fetchPlace(entityID); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Clears the Nearby cache on an IO thread. | ||||
|      * | ||||
|      * @return A Completable that completes once the cache has been successfully cleared. | ||||
|      */ | ||||
|     public Completable clearCache() { | ||||
|         return localDataSource.clearCache() | ||||
|             .subscribeOn(Schedulers.io()); // Ensure it runs on IO thread | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -87,7 +87,7 @@ class WikidataFeedback : BaseActivity() { | |||
|                                 lat, | ||||
|                                 lng, | ||||
|                             ) | ||||
|                         } as Callable<SingleSource<Boolean?>>, | ||||
|                         }, | ||||
|                     ).subscribeOn(Schedulers.io()) | ||||
|                     .observeOn(AndroidSchedulers.mainThread()) | ||||
|                     .subscribe({ aBoolean: Boolean? -> | ||||
|  |  | |||
|  | @ -28,14 +28,14 @@ class CommonPlaceClickActions | |||
|         private val activity: Activity, | ||||
|         private val contributionController: ContributionController, | ||||
|     ) { | ||||
|         fun onCameraClicked(): (Place, ActivityResultLauncher<Array<String>>) -> Unit = | ||||
|             { place, launcher -> | ||||
|         fun onCameraClicked(): (Place, ActivityResultLauncher<Array<String>>, ActivityResultLauncher<Intent>) -> Unit = | ||||
|             { place, launcher, resultLauncher -> | ||||
|                 if (applicationKvStore.getBoolean("login_skipped", false)) { | ||||
|                     showLoginDialog() | ||||
|                 } else { | ||||
|                     Timber.d("Camera button tapped. Image title: ${place.getName()}Image desc: ${place.longDescription}") | ||||
|                     storeSharedPrefs(place) | ||||
|                     contributionController.initiateCameraPick(activity, launcher) | ||||
|                     contributionController.initiateCameraPick(activity, launcher, resultLauncher) | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|  | @ -72,14 +72,14 @@ class CommonPlaceClickActions | |||
|                 true | ||||
|             } | ||||
| 
 | ||||
|         fun onGalleryClicked(): (Place) -> Unit = | ||||
|             { | ||||
|         fun onGalleryClicked(): (Place, ActivityResultLauncher<Intent>) -> Unit = | ||||
|             {place, galleryPickLauncherForResult -> | ||||
|                 if (applicationKvStore.getBoolean("login_skipped", false)) { | ||||
|                     showLoginDialog() | ||||
|                 } else { | ||||
|                     Timber.d("Gallery button tapped. Image title: ${it.getName()}Image desc: ${it.getLongDescription()}") | ||||
|                     storeSharedPrefs(it) | ||||
|                     contributionController.initiateGalleryPick(activity, false) | ||||
|                     Timber.d("Gallery button tapped. Image title: ${place.getName()}Image desc: ${place.getLongDescription()}") | ||||
|                     storeSharedPrefs(place) | ||||
|                     contributionController.initiateGalleryPick(activity, galleryPickLauncherForResult, false) | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|  |  | |||
|  | @ -43,15 +43,18 @@ import android.view.ViewGroup; | |||
| import android.view.ViewGroup.LayoutParams; | ||||
| import android.view.animation.Animation; | ||||
| import android.view.animation.AnimationUtils; | ||||
| import android.widget.Button; | ||||
| import android.widget.Toast; | ||||
| import androidx.activity.result.ActivityResultCallback; | ||||
| import androidx.activity.result.ActivityResultLauncher; | ||||
| import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions; | ||||
| import androidx.activity.result.contract.ActivityResultContracts.RequestPermission; | ||||
| import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult; | ||||
| import androidx.annotation.DrawableRes; | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.appcompat.app.AlertDialog.Builder; | ||||
| import androidx.constraintlayout.widget.ConstraintLayout; | ||||
| import androidx.core.content.ContextCompat; | ||||
| import androidx.core.content.FileProvider; | ||||
| import androidx.recyclerview.widget.DividerItemDecoration; | ||||
|  | @ -105,6 +108,7 @@ import fr.free.nrw.commons.utils.NetworkUtils; | |||
| import fr.free.nrw.commons.utils.SystemThemeUtils; | ||||
| import fr.free.nrw.commons.utils.ViewUtil; | ||||
| import fr.free.nrw.commons.wikidata.WikidataEditListener; | ||||
| import io.reactivex.Completable; | ||||
| import io.reactivex.Observable; | ||||
| import io.reactivex.android.schedulers.AndroidSchedulers; | ||||
| import io.reactivex.disposables.Disposable; | ||||
|  | @ -218,9 +222,36 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment | |||
|     private LatLng updatedLatLng; | ||||
|     private boolean searchable; | ||||
| 
 | ||||
|     private ConstraintLayout nearbyLegend; | ||||
| 
 | ||||
|     private GridLayoutManager gridLayoutManager; | ||||
|     private List<BottomSheetItem> dataList; | ||||
|     private BottomSheetAdapter bottomSheetAdapter; | ||||
| 
 | ||||
|     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>>() { | ||||
|  | @ -236,7 +267,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment | |||
|                 } else { | ||||
|                     if (shouldShowRequestPermissionRationale(permission.ACCESS_FINE_LOCATION)) { | ||||
|                         controller.handleShowRationaleFlowCameraLocation(getActivity(), | ||||
|                             inAppCameraLocationPermissionLauncher); | ||||
|                             inAppCameraLocationPermissionLauncher, cameraPickLauncherForResult); | ||||
|                     } else { | ||||
|                         controller.locationPermissionCallback.onLocationPermissionDenied( | ||||
|                             getActivity().getString( | ||||
|  | @ -302,6 +333,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment | |||
|         progressDialog.setCancelable(false); | ||||
|         progressDialog.setMessage("Saving in progress..."); | ||||
|         setHasOptionsMenu(true); | ||||
| 
 | ||||
|         // Inflate the layout for this fragment | ||||
|         return view; | ||||
| 
 | ||||
|  | @ -311,9 +343,21 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment | |||
|     public void onCreateOptionsMenu(@NonNull final Menu menu, | ||||
|         @NonNull final MenuInflater inflater) { | ||||
|         inflater.inflate(R.menu.nearby_fragment_menu, menu); | ||||
|         MenuItem refreshButton = menu.findItem(R.id.item_refresh); | ||||
|         MenuItem listMenu = menu.findItem(R.id.list_sheet); | ||||
|         MenuItem saveAsGPXButton = menu.findItem(R.id.list_item_gpx); | ||||
|         MenuItem saveAsKMLButton = menu.findItem(R.id.list_item_kml); | ||||
|         refreshButton.setOnMenuItemClickListener(new OnMenuItemClickListener() { | ||||
|             @Override | ||||
|             public boolean onMenuItemClick(MenuItem item) { | ||||
|                 try { | ||||
|                     emptyCache(); | ||||
|                 } catch (Exception e) { | ||||
|                     throw new RuntimeException(e); | ||||
|                 } | ||||
|                 return false; | ||||
|             } | ||||
|         }); | ||||
|         listMenu.setOnMenuItemClickListener(new OnMenuItemClickListener() { | ||||
|             @Override | ||||
|             public boolean onMenuItemClick(MenuItem item) { | ||||
|  | @ -362,6 +406,16 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment | |||
|         } | ||||
|         locationPermissionsHelper = new LocationPermissionsHelper(getActivity(), locationManager, | ||||
|             this); | ||||
| 
 | ||||
|         // Set up the floating activity button to toggle the visibility of the legend | ||||
|         binding.fabLegend.setOnClickListener(v -> { | ||||
|             if (binding.nearbyLegendLayout.getRoot().getVisibility() == View.VISIBLE) { | ||||
|                 binding.nearbyLegendLayout.getRoot().setVisibility(View.GONE); | ||||
|             } else { | ||||
|                 binding.nearbyLegendLayout.getRoot().setVisibility(View.VISIBLE); | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         presenter.attachView(this); | ||||
|         isPermissionDenied = false; | ||||
|         recenterToUserLocation = false; | ||||
|  | @ -555,7 +609,9 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment | |||
|                 return Unit.INSTANCE; | ||||
|             }, | ||||
|             commonPlaceClickActions, | ||||
|             inAppCameraLocationPermissionLauncher | ||||
|             inAppCameraLocationPermissionLauncher, | ||||
|             galleryPickLauncherForResult, | ||||
|             cameraPickLauncherForResult | ||||
|         ); | ||||
|         binding.bottomSheetNearby.rvNearbyList.setAdapter(adapter); | ||||
|     } | ||||
|  | @ -1115,6 +1171,48 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      *  Reloads the Nearby map | ||||
|      *  Clears all location markers, refreshes them, reinserts them into the map. | ||||
|      * | ||||
|      */ | ||||
|     private void reloadMap() { | ||||
|         clearAllMarkers(); // Clear the list of markers | ||||
|         binding.map.getController().setZoom(ZOOM_LEVEL); // Reset the zoom level | ||||
|         binding.map.getController().setCenter(lastMapFocus); // Recenter the focus | ||||
|         if (locationPermissionsHelper.checkLocationPermission(getActivity())) { | ||||
|             locationPermissionGranted(); // Reload map with user's location | ||||
|         } else { | ||||
|             startMapWithoutPermission(); // Reload map without user's location | ||||
|         } | ||||
|         binding.map.invalidate(); // Invalidate the map | ||||
|         presenter.updateMapAndList(LOCATION_SIGNIFICANTLY_CHANGED); // Restart the map | ||||
|         Timber.d("Reloaded Map Successfully"); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * Clears the Nearby local cache and then calls for the map to be reloaded | ||||
|      * | ||||
|      */ | ||||
|     private void emptyCache() { | ||||
|         // reload the map once the cache is cleared | ||||
|         compositeDisposable.add( | ||||
|             placesRepository.clearCache() | ||||
|                 .subscribeOn(Schedulers.io()) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .andThen(Completable.fromAction(this::reloadMap)) | ||||
|                 .subscribe( | ||||
|                     () -> { | ||||
|                         Timber.d("Nearby Cache cleared successfully."); | ||||
|                     }, | ||||
|                     throwable -> { | ||||
|                         Timber.e(throwable, "Failed to clear the Nearby Cache"); | ||||
|                     } | ||||
|                 ) | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     private void savePlacesAsKML() { | ||||
|         final Observable<String> savePlacesObservable = Observable | ||||
|             .fromCallable(() -> nearbyController | ||||
|  | @ -2186,7 +2284,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment | |||
|             if (binding.fabCamera.isShown()) { | ||||
|                 Timber.d("Camera button tapped. Place: %s", selectedPlace.toString()); | ||||
|                 storeSharedPrefs(selectedPlace); | ||||
|                 controller.initiateCameraPick(getActivity(), inAppCameraLocationPermissionLauncher); | ||||
|                 controller.initiateCameraPick(getActivity(), inAppCameraLocationPermissionLauncher, cameraPickLauncherForResult); | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|  | @ -2195,6 +2293,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment | |||
|                 Timber.d("Gallery button tapped. Place: %s", selectedPlace.toString()); | ||||
|                 storeSharedPrefs(selectedPlace); | ||||
|                 controller.initiateGalleryPick(getActivity(), | ||||
|                     galleryPickLauncherForResult, | ||||
|                     false); | ||||
|             } | ||||
|         }); | ||||
|  | @ -2203,7 +2302,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment | |||
|             if (binding.fabCustomGallery.isShown()) { | ||||
|                 Timber.d("Gallery button tapped. Place: %s", selectedPlace.toString()); | ||||
|                 storeSharedPrefs(selectedPlace); | ||||
|                 controller.initiateCustomGalleryPickWithPermission(getActivity()); | ||||
|                 controller.initiateCustomGalleryPickWithPermission(getActivity(), customSelectorLauncherForResult); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| package fr.free.nrw.commons.nearby.fragments | ||||
| 
 | ||||
| import android.content.Intent | ||||
| import androidx.activity.result.ActivityResultLauncher | ||||
| import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao | ||||
| import fr.free.nrw.commons.nearby.Place | ||||
|  | @ -12,6 +13,8 @@ class PlaceAdapter( | |||
|     onBookmarkClicked: (Place, Boolean) -> Unit, | ||||
|     commonPlaceClickActions: CommonPlaceClickActions, | ||||
|     inAppCameraLocationPermissionLauncher: ActivityResultLauncher<Array<String>>, | ||||
|     galleryPickLauncherForResult: ActivityResultLauncher<Intent>, | ||||
|     cameraPickLauncherForResult: ActivityResultLauncher<Intent> | ||||
| ) : BaseDelegateAdapter<Place>( | ||||
|         placeAdapterDelegate( | ||||
|             bookmarkLocationsDao, | ||||
|  | @ -27,6 +30,8 @@ class PlaceAdapter( | |||
|             commonPlaceClickActions.onDirectionsClicked(), | ||||
|             commonPlaceClickActions.onDirectionsLongPressed(), | ||||
|             inAppCameraLocationPermissionLauncher, | ||||
|             cameraPickLauncherForResult, | ||||
|             galleryPickLauncherForResult | ||||
|         ), | ||||
|         areItemsTheSame = { oldItem, newItem -> oldItem.wikiDataEntityId == newItem.wikiDataEntityId }, | ||||
|     ) | ||||
|  |  | |||
|  | @ -19,6 +19,7 @@ import fr.free.nrw.commons.databinding.ActivityNotificationBinding; | |||
| import fr.free.nrw.commons.auth.SessionManager; | ||||
| import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException; | ||||
| import fr.free.nrw.commons.notification.models.Notification; | ||||
| import fr.free.nrw.commons.notification.models.NotificationType; | ||||
| import fr.free.nrw.commons.theme.BaseActivity; | ||||
| import fr.free.nrw.commons.utils.NetworkUtils; | ||||
| import fr.free.nrw.commons.utils.ViewUtil; | ||||
|  | @ -148,7 +149,11 @@ public class NotificationActivity extends BaseActivity { | |||
|         } | ||||
|         adapter = new NotificatinAdapter(item -> { | ||||
|             Timber.d("Notification clicked %s", item.getLink()); | ||||
|             handleUrl(item.getLink()); | ||||
|             if (item.getNotificationType() == NotificationType.EMAIL){ | ||||
|                 ViewUtil.showLongSnackbar(binding.container,getString(R.string.check_your_email_inbox)); | ||||
|             } else { | ||||
|                 handleUrl(item.getLink()); | ||||
|             } | ||||
|             removeNotification(item); | ||||
|             return Unit.INSTANCE; | ||||
|         }); | ||||
|  |  | |||
|  | @ -51,13 +51,23 @@ class NotificationClient | |||
|                 } | ||||
|             } | ||||
| 
 | ||||
|         private fun WikimediaNotification.toCommonsNotification() = | ||||
|             Notification( | ||||
|                 notificationType = NotificationType.UNKNOWN, | ||||
|                 notificationText = contents?.compactHeader ?: "", | ||||
|                 date = DateUtil.getMonthOnlyDateString(timestamp), | ||||
|                 link = contents?.links?.primary?.url ?: "", | ||||
|                 iconUrl = "", | ||||
|                 notificationId = id().toString(), | ||||
|             ) | ||||
|         private fun WikimediaNotification.toCommonsNotification() : | ||||
|             Notification { | ||||
|             val notificationText = contents?.compactHeader ?: "" | ||||
|             val notificationType = | ||||
|                 if (notificationText.contains("Sent you an email", ignoreCase = true)) { | ||||
|                     NotificationType.EMAIL | ||||
|                 } else { | ||||
|                     NotificationType.UNKNOWN | ||||
|                 } | ||||
| 
 | ||||
|                 return Notification( | ||||
|                     notificationType = notificationType, | ||||
|                     notificationText = notificationText, | ||||
|                     date = DateUtil.getMonthOnlyDateString(timestamp), | ||||
|                     link = contents?.links?.primary?.url ?: "", | ||||
|                     iconUrl = "", | ||||
|                     notificationId = id().toString(), | ||||
|                 ) | ||||
|         } | ||||
|     } | ||||
|  |  | |||
|  | @ -4,6 +4,7 @@ public enum NotificationType { | |||
|     THANK_YOU_EDIT("thank-you-edit"), | ||||
|     EDIT_USER_TALK("edit-user-talk"), | ||||
|     MENTION("mention"), | ||||
|     EMAIL("email"), | ||||
|     WELCOME("welcome"), | ||||
|     UNKNOWN("unknown"); | ||||
|     private String type; | ||||
|  |  | |||
|  | @ -32,6 +32,7 @@ import java.io.FileOutputStream; | |||
| import java.io.IOException; | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
| import java.util.Locale; | ||||
| import javax.inject.Inject; | ||||
| 
 | ||||
| /** | ||||
|  | @ -139,14 +140,14 @@ public class ProfileActivity extends BaseActivity { | |||
|         leaderboardFragment.setArguments(leaderBoardBundle); | ||||
| 
 | ||||
|         fragmentList.add(leaderboardFragment); | ||||
|         titleList.add(getResources().getString(R.string.leaderboard_tab_title).toUpperCase()); | ||||
|         titleList.add(getResources().getString(R.string.leaderboard_tab_title).toUpperCase(Locale.ROOT)); | ||||
| 
 | ||||
|         contributionsFragment = new ContributionsFragment(); | ||||
|         Bundle contributionsListBundle = new Bundle(); | ||||
|         contributionsListBundle.putString(KEY_USERNAME, userName); | ||||
|         contributionsFragment.setArguments(contributionsListBundle); | ||||
|         fragmentList.add(contributionsFragment); | ||||
|         titleList.add(getString(R.string.contributions_fragment).toUpperCase()); | ||||
|         titleList.add(getString(R.string.contributions_fragment).toUpperCase(Locale.ROOT)); | ||||
| 
 | ||||
|         viewPagerAdapter.setTabData(fragmentList, titleList); | ||||
|         viewPagerAdapter.notifyDataSetChanged(); | ||||
|  |  | |||
|  | @ -27,6 +27,7 @@ import fr.free.nrw.commons.profile.ProfileActivity; | |||
| import io.reactivex.android.schedulers.AndroidSchedulers; | ||||
| import io.reactivex.disposables.CompositeDisposable; | ||||
| import io.reactivex.schedulers.Schedulers; | ||||
| import java.util.Locale; | ||||
| import java.util.Objects; | ||||
| import javax.inject.Inject; | ||||
| import org.apache.commons.lang3.StringUtils; | ||||
|  | @ -361,7 +362,7 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment { | |||
|             + levelInfo.getMaxUniqueImages()); | ||||
|         binding.imageFeatured.setText(String.valueOf(achievements.getFeaturedImages())); | ||||
|         binding.qualityImages.setText(String.valueOf(achievements.getQualityImages())); | ||||
|         String levelUpInfoString = getString(R.string.level).toUpperCase(); | ||||
|         String levelUpInfoString = getString(R.string.level).toUpperCase(Locale.ROOT); | ||||
|         levelUpInfoString += " " + levelInfo.getLevelNumber(); | ||||
|         binding.achievementLevel.setText(levelUpInfoString); | ||||
|         binding.achievementBadgeImage.setImageDrawable(VectorDrawableCompat.create(getResources(), R.drawable.badge, | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| package fr.free.nrw.commons.recentlanguages; | ||||
| 
 | ||||
| import android.annotation.SuppressLint; | ||||
| import android.content.ContentProviderClient; | ||||
| import android.content.ContentValues; | ||||
| import android.database.Cursor; | ||||
|  | @ -117,6 +118,7 @@ public class RecentLanguagesDao { | |||
|      * @return Language object | ||||
|      */ | ||||
|     @NonNull | ||||
|     @SuppressLint("Range") | ||||
|     Language fromCursor(final Cursor cursor) { | ||||
|         // Hardcoding column positions! | ||||
|         final String languageName = cursor.getString(cursor.getColumnIndex(Table.COLUMN_NAME)); | ||||
|  |  | |||
|  | @ -25,6 +25,7 @@ import fr.free.nrw.commons.utils.ViewUtil; | |||
| import io.reactivex.android.schedulers.AndroidSchedulers; | ||||
| import io.reactivex.disposables.CompositeDisposable; | ||||
| import io.reactivex.schedulers.Schedulers; | ||||
| import java.util.Locale; | ||||
| import javax.inject.Inject; | ||||
| 
 | ||||
| public class ReviewActivity extends BaseActivity { | ||||
|  | @ -241,7 +242,7 @@ public class ReviewActivity extends BaseActivity { | |||
| 
 | ||||
|     public void showSkipImageInfo(){ | ||||
|         DialogUtil.showAlertDialog(ReviewActivity.this, | ||||
|                 getString(R.string.skip_image).toUpperCase(), | ||||
|                 getString(R.string.skip_image).toUpperCase(Locale.ROOT), | ||||
|                 getString(R.string.skip_image_explanation), | ||||
|                 getString(android.R.string.ok), | ||||
|                 "", | ||||
|  |  | |||
|  | @ -12,6 +12,7 @@ import android.net.Uri; | |||
| import android.os.Bundle; | ||||
| import android.text.Editable; | ||||
| import android.text.TextWatcher; | ||||
| import android.util.Log; | ||||
| import android.view.View; | ||||
| import android.widget.AdapterView; | ||||
| import android.widget.AdapterView.OnItemClickListener; | ||||
|  | @ -21,6 +22,7 @@ import android.widget.TextView; | |||
| 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.preference.ListPreference; | ||||
| import androidx.preference.MultiSelectListPreference; | ||||
| import androidx.preference.Preference; | ||||
|  | @ -58,6 +60,7 @@ import java.util.Locale; | |||
| import java.util.Map; | ||||
| import javax.inject.Inject; | ||||
| import javax.inject.Named; | ||||
| import timber.log.Timber; | ||||
| 
 | ||||
| public class SettingsFragment extends PreferenceFragmentCompat { | ||||
| 
 | ||||
|  | @ -80,11 +83,21 @@ public class SettingsFragment extends PreferenceFragmentCompat { | |||
|     private ListPreference themeListPreference; | ||||
|     private Preference descriptionLanguageListPreference; | ||||
|     private Preference appUiLanguageListPreference; | ||||
|     private Preference showDeletionButtonPreference; | ||||
|     private String keyLanguageListPreference; | ||||
|     private TextView recentLanguagesTextView; | ||||
|     private View separator; | ||||
|     private ListView languageHistoryListView; | ||||
|     private static final String GET_CONTENT_PICKER_HELP_URL = "https://commons-app.github.io/docs.html#get-content"; | ||||
| 
 | ||||
|     private final ActivityResultLauncher<Intent> cameraPickLauncherForResult = | ||||
|         registerForActivityResult(new StartActivityForResult(), | ||||
|         result -> { | ||||
|             contributionController.handleActivityResultWithCallback(requireActivity(), callbacks -> { | ||||
|                 contributionController.onPictureReturnedFromCamera(result, requireActivity(), callbacks); | ||||
|             }); | ||||
|         }); | ||||
| 
 | ||||
|     private ActivityResultLauncher<String[]> inAppCameraLocationPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), new ActivityResultCallback<Map<String, Boolean>>() { | ||||
|         @Override | ||||
|         public void onActivityResult(Map<String, Boolean> result) { | ||||
|  | @ -93,7 +106,7 @@ public class SettingsFragment extends PreferenceFragmentCompat { | |||
|                 areAllGranted = areAllGranted && b; | ||||
|             } | ||||
|             if (!areAllGranted && shouldShowRequestPermissionRationale(permission.ACCESS_FINE_LOCATION)) { | ||||
|                 contributionController.handleShowRationaleFlowCameraLocation(getActivity(), inAppCameraLocationPermissionLauncher); | ||||
|                 contributionController.handleShowRationaleFlowCameraLocation(getActivity(), inAppCameraLocationPermissionLauncher, cameraPickLauncherForResult); | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
|  | @ -178,6 +191,18 @@ public class SettingsFragment extends PreferenceFragmentCompat { | |||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         // | ||||
|         showDeletionButtonPreference = findPreference("displayDeletionButton"); | ||||
|         if (showDeletionButtonPreference != null) { | ||||
|             showDeletionButtonPreference.setOnPreferenceChangeListener((preference, newValue) -> { | ||||
|                 boolean isEnabled = (boolean) newValue; | ||||
|                 // Save preference when user toggles the button | ||||
|                 defaultKvStore.putBoolean("displayDeletionButton", isEnabled); | ||||
|                 return true; | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         Preference betaTesterPreference = findPreference("becomeBetaTester"); | ||||
|         betaTesterPreference.setOnPreferenceClickListener(preference -> { | ||||
|             Utils.handleWebUrl(getActivity(), Uri.parse(getResources().getString(R.string.beta_opt_in_link))); | ||||
|  |  | |||
|  | @ -63,7 +63,7 @@ class FailedUploadsFragment : | |||
|         } | ||||
| 
 | ||||
|         if (StringUtils.isEmpty(userName)) { | ||||
|             userName = sessionManager!!.getUserName() | ||||
|             userName = sessionManager.getUserName() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -96,8 +96,8 @@ class FailedUploadsFragment : | |||
|     fun initRecyclerView() { | ||||
|         binding.failedUploadsRecyclerView.setLayoutManager(LinearLayoutManager(this.context)) | ||||
|         binding.failedUploadsRecyclerView.adapter = adapter | ||||
|         pendingUploadsPresenter!!.getFailedContributions() | ||||
|         pendingUploadsPresenter!!.failedContributionList.observe( | ||||
|         pendingUploadsPresenter.getFailedContributions() | ||||
|         pendingUploadsPresenter.failedContributionList.observe( | ||||
|             viewLifecycleOwner, | ||||
|         ) { list: PagedList<Contribution?> -> | ||||
|             adapter.submitList(list) | ||||
|  |  | |||
|  | @ -19,6 +19,7 @@ import java.math.BigInteger; | |||
| import java.security.MessageDigest; | ||||
| import java.security.NoSuchAlgorithmException; | ||||
| 
 | ||||
| import java.util.Locale; | ||||
| import timber.log.Timber; | ||||
| 
 | ||||
| public class FileUtils { | ||||
|  | @ -139,7 +140,7 @@ public class FileUtils { | |||
|             String fileExtension = MimeTypeMap.getFileExtensionFromUrl(uri | ||||
|                     .toString()); | ||||
|             mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension( | ||||
|                     fileExtension.toLowerCase()); | ||||
|                     fileExtension.toLowerCase(Locale.getDefault())); | ||||
|         } | ||||
|         return mimeType; | ||||
|     } | ||||
|  |  | |||
|  | @ -74,8 +74,8 @@ class PendingUploadsFragment : | |||
|     fun initRecyclerView() { | ||||
|         binding.pendingUploadsRecyclerView.setLayoutManager(LinearLayoutManager(this.context)) | ||||
|         binding.pendingUploadsRecyclerView.adapter = adapter | ||||
|         pendingUploadsPresenter!!.setup() | ||||
|         pendingUploadsPresenter!!.totalContributionList.observe( | ||||
|         pendingUploadsPresenter.setup() | ||||
|         pendingUploadsPresenter.totalContributionList.observe( | ||||
|             viewLifecycleOwner, | ||||
|         ) { list: PagedList<Contribution?> -> | ||||
|             contributionsSize = list.size | ||||
|  |  | |||
|  | @ -320,6 +320,14 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, | |||
|         finish(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * go to the uploadProgress activity to check the status of uploading | ||||
|      */ | ||||
|     @Override | ||||
|     public void goToUploadProgressActivity() { | ||||
|         startActivity(new Intent(this, UploadProgressActivity.class)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Show/Hide the progress dialog | ||||
|      */ | ||||
|  | @ -433,14 +441,6 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, | |||
|         super.onRequestPermissionsResult(requestCode, permissions, grantResults); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onActivityResult(int requestCode, int resultCode, Intent data) { | ||||
|         super.onActivityResult(requestCode, resultCode, data); | ||||
|         if (requestCode == CommonsApplication.OPEN_APPLICATION_DETAIL_SETTINGS) { | ||||
|             //TODO: Confirm if handling manual permission enabled is required | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Sets the flag indicating whether the upload is of a specific place. | ||||
|      * | ||||
|  |  | |||
|  | @ -18,6 +18,13 @@ public interface UploadContract { | |||
| 
 | ||||
|         void returnToMainActivity(); | ||||
| 
 | ||||
|         /** | ||||
|          * When submission successful, go to the loadProgressActivity to hint the user this | ||||
|          * submission is valid. And the user will see the upload progress in this activity; | ||||
|          * Fixes: <a href="https://github.com/commons-app/apps-android-commons/issues/5846">Issue</a> | ||||
|          */ | ||||
|         void goToUploadProgressActivity(); | ||||
| 
 | ||||
|         void askUserToLogIn(); | ||||
| 
 | ||||
|         /** | ||||
|  |  | |||
|  | @ -20,6 +20,7 @@ import android.widget.ImageView; | |||
| import android.widget.LinearLayout; | ||||
| import android.widget.ListView; | ||||
| import android.widget.TextView; | ||||
| import androidx.activity.result.ActivityResultLauncher; | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.constraintlayout.widget.ConstraintLayout; | ||||
|  | @ -57,27 +58,29 @@ public class UploadMediaDetailAdapter extends | |||
|     private int currentPosition; | ||||
|     private Fragment fragment; | ||||
|     private Activity activity; | ||||
|     private final ActivityResultLauncher<Intent> voiceInputResultLauncher; | ||||
|     private SelectedVoiceIcon selectedVoiceIcon; | ||||
|     private static final int REQUEST_CODE_FOR_VOICE_INPUT = 1213; | ||||
| 
 | ||||
|     private RowItemDescriptionBinding binding; | ||||
| 
 | ||||
|     public UploadMediaDetailAdapter(Fragment fragment, String savedLanguageValue, | ||||
|         RecentLanguagesDao recentLanguagesDao) { | ||||
|         RecentLanguagesDao recentLanguagesDao, ActivityResultLauncher<Intent> voiceInputResultLauncher) { | ||||
|         uploadMediaDetails = new ArrayList<>(); | ||||
|         selectedLanguages = new HashMap<>(); | ||||
|         this.savedLanguageValue = savedLanguageValue; | ||||
|         this.recentLanguagesDao = recentLanguagesDao; | ||||
|         this.fragment = fragment; | ||||
|         this.voiceInputResultLauncher = voiceInputResultLauncher; | ||||
|     } | ||||
| 
 | ||||
|     public UploadMediaDetailAdapter(Activity activity, final String savedLanguageValue, | ||||
|         List<UploadMediaDetail> uploadMediaDetails, RecentLanguagesDao recentLanguagesDao) { | ||||
|         List<UploadMediaDetail> uploadMediaDetails, RecentLanguagesDao recentLanguagesDao, ActivityResultLauncher<Intent> voiceInputResultLauncher) { | ||||
|         this.uploadMediaDetails = uploadMediaDetails; | ||||
|         selectedLanguages = new HashMap<>(); | ||||
|         this.savedLanguageValue = savedLanguageValue; | ||||
|         this.recentLanguagesDao = recentLanguagesDao; | ||||
|         this.activity = activity; | ||||
|         this.voiceInputResultLauncher = voiceInputResultLauncher; | ||||
|     } | ||||
| 
 | ||||
|     public void setCallback(Callback callback) { | ||||
|  | @ -150,11 +153,7 @@ public class UploadMediaDetailAdapter extends | |||
|         ); | ||||
| 
 | ||||
|         try { | ||||
|             if (activity == null) { | ||||
|                 fragment.startActivityForResult(intent, REQUEST_CODE_FOR_VOICE_INPUT); | ||||
|             } else { | ||||
|                 activity.startActivityForResult(intent, REQUEST_CODE_FOR_VOICE_INPUT); | ||||
|             } | ||||
|             voiceInputResultLauncher.launch(intent); | ||||
|         } catch (Exception e) { | ||||
|             Timber.e(e.getMessage()); | ||||
|         } | ||||
|  | @ -407,7 +406,7 @@ public class UploadMediaDetailAdapter extends | |||
|                             recentLanguagesDao | ||||
|                                 .addRecentLanguage(new Language(languageName, languageCode)); | ||||
| 
 | ||||
|                             selectedLanguages.remove(position); | ||||
|                             selectedLanguages.clear(); | ||||
|                             selectedLanguages.put(position, languageCode); | ||||
|                             ((LanguagesAdapter) adapterView | ||||
|                                 .getAdapter()).setSelectedLangCode(languageCode); | ||||
|  | @ -497,7 +496,7 @@ public class UploadMediaDetailAdapter extends | |||
|             } | ||||
|             recentLanguagesDao.addRecentLanguage(new Language(languageName, languageCode)); | ||||
| 
 | ||||
|             selectedLanguages.remove(position); | ||||
|             selectedLanguages.clear(); | ||||
|             selectedLanguages.put(position, languageCode); | ||||
|             ((RecentLanguagesAdapter) adapterView | ||||
|                 .getAdapter()).setSelectedLangCode(languageCode); | ||||
|  |  | |||
|  | @ -123,6 +123,9 @@ public class UploadPresenter implements UploadContract.UserActionListener { | |||
|                             view.returnToMainActivity(); | ||||
|                             compositeDisposable.clear(); | ||||
|                             Timber.e("failed to upload: " + e.getMessage()); | ||||
| 
 | ||||
|                             //is submission error, not need to go to the uploadActivity | ||||
|                             //not start the uploading progress | ||||
|                         } | ||||
| 
 | ||||
|                         @Override | ||||
|  | @ -131,6 +134,10 @@ public class UploadPresenter implements UploadContract.UserActionListener { | |||
|                             repository.cleanup(); | ||||
|                             view.returnToMainActivity(); | ||||
|                             compositeDisposable.clear(); | ||||
| 
 | ||||
|                             //after finish the uploadActivity, if successful, | ||||
|                             //directly go to the upload progress activity | ||||
|                             view.goToUploadProgressActivity(); | ||||
|                         } | ||||
|                     }); | ||||
|         } else { | ||||
|  |  | |||
|  | @ -10,15 +10,13 @@ abstract class BaseDelegateAdapter<T>( | |||
|     areContentsTheSame: (T, T) -> Boolean = { old, new -> old == new }, | ||||
| ) : AsyncListDifferDelegationAdapter<T>( | ||||
|         object : DiffUtil.ItemCallback<T>() { | ||||
|             override fun areItemsTheSame( | ||||
|                 oldItem: T, | ||||
|                 newItem: T, | ||||
|             ) = areItemsTheSame(oldItem, newItem) | ||||
|             override fun areItemsTheSame(oldItem: T & Any, newItem: T & Any): Boolean { | ||||
|                 return areItemsTheSame(oldItem, newItem) | ||||
|             } | ||||
| 
 | ||||
|             override fun areContentsTheSame( | ||||
|                 oldItem: T, | ||||
|                 newItem: T, | ||||
|             ) = areContentsTheSame(oldItem, newItem) | ||||
|             override fun areContentsTheSame(oldItem: T & Any, newItem: T & Any): Boolean { | ||||
|                 return areContentsTheSame(oldItem, newItem) | ||||
|             } | ||||
|         }, | ||||
|         *delegates, | ||||
|     ) { | ||||
|  |  | |||
|  | @ -372,7 +372,7 @@ public class UploadCategoriesFragment extends UploadBaseFragment implements Cate | |||
|                 return false; | ||||
|             }); | ||||
| 
 | ||||
|             Objects.requireNonNull(getView()).setFocusableInTouchMode(true); | ||||
|             requireView().setFocusableInTouchMode(true); | ||||
|             getView().requestFocus(); | ||||
|             getView().setOnKeyListener((v, keyCode, event) -> { | ||||
|                 if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { | ||||
|  | @ -387,7 +387,7 @@ public class UploadCategoriesFragment extends UploadBaseFragment implements Cate | |||
|             }); | ||||
| 
 | ||||
|             Objects.requireNonNull( | ||||
|                 ((AppCompatActivity) Objects.requireNonNull(getActivity())).getSupportActionBar()) | ||||
|                 ((AppCompatActivity) requireActivity()).getSupportActionBar()) | ||||
|                 .hide(); | ||||
| 
 | ||||
|             if (getParentFragment().getParentFragment().getParentFragment() | ||||
|  | @ -407,7 +407,7 @@ public class UploadCategoriesFragment extends UploadBaseFragment implements Cate | |||
|         super.onStop(); | ||||
|         if (media != null) { | ||||
|             Objects.requireNonNull( | ||||
|                 ((AppCompatActivity) Objects.requireNonNull(getActivity())).getSupportActionBar()) | ||||
|                 ((AppCompatActivity) requireActivity()).getSupportActionBar()) | ||||
|                 .show(); | ||||
|         } | ||||
|     } | ||||
|  |  | |||
|  | @ -36,7 +36,7 @@ abstract class DepictsDao { | |||
|     /** | ||||
|      * Gets all Depicts objects from the database, ordered by lastUsed in descending order. | ||||
|      * | ||||
|      * @return A list of Depicts objects. | ||||
|      * @return Deferred list of Depicts objects. | ||||
|      */ | ||||
|     fun depictsList(): Deferred<List<Depicts>> = | ||||
|         CoroutineScope(Dispatchers.IO).async { | ||||
|  | @ -48,7 +48,7 @@ abstract class DepictsDao { | |||
|      * | ||||
|      * @param depictedItem The Depicts object to insert. | ||||
|      */ | ||||
|     private fun insertDepict(depictedItem: Depicts) = | ||||
|     fun insertDepict(depictedItem: Depicts) = | ||||
|         CoroutineScope(Dispatchers.IO).launch { | ||||
|             insert(depictedItem) | ||||
|         } | ||||
|  | @ -59,7 +59,7 @@ abstract class DepictsDao { | |||
|      * @param n The number of depicts to delete. | ||||
|      * @return A list of Depicts objects to delete. | ||||
|      */ | ||||
|     private suspend fun depictsForDeletion(n: Int): Deferred<List<Depicts>> = | ||||
|     fun depictsForDeletion(n: Int): Deferred<List<Depicts>> = | ||||
|         CoroutineScope(Dispatchers.IO).async { | ||||
|             getDepictsForDeletion(n) | ||||
|         } | ||||
|  | @ -69,7 +69,7 @@ abstract class DepictsDao { | |||
|      * | ||||
|      * @param depicts The Depicts object to delete. | ||||
|      */ | ||||
|     private suspend fun deleteDepicts(depicts: Depicts) = | ||||
|     fun deleteDepicts(depicts: Depicts) = | ||||
|         CoroutineScope(Dispatchers.IO).launch { | ||||
|             delete(depicts) | ||||
|         } | ||||
|  |  | |||
|  | @ -398,7 +398,7 @@ public class DepictsFragment extends UploadBaseFragment implements DepictsContra | |||
|                 return false; | ||||
|             }); | ||||
| 
 | ||||
|             Objects.requireNonNull(getView()).setFocusableInTouchMode(true); | ||||
|             requireView().setFocusableInTouchMode(true); | ||||
|             getView().requestFocus(); | ||||
|             getView().setOnKeyListener((v, keyCode, event) -> { | ||||
|                 if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { | ||||
|  | @ -411,7 +411,7 @@ public class DepictsFragment extends UploadBaseFragment implements DepictsContra | |||
|             }); | ||||
| 
 | ||||
|             Objects.requireNonNull( | ||||
|                 ((AppCompatActivity) Objects.requireNonNull(getActivity())).getSupportActionBar()) | ||||
|                 ((AppCompatActivity) requireActivity()).getSupportActionBar()) | ||||
|                 .hide(); | ||||
| 
 | ||||
|             if (getParentFragment().getParentFragment().getParentFragment() | ||||
|  | @ -431,7 +431,7 @@ public class DepictsFragment extends UploadBaseFragment implements DepictsContra | |||
|         super.onStop(); | ||||
|         if (media != null) { | ||||
|             Objects.requireNonNull( | ||||
|                 ((AppCompatActivity) Objects.requireNonNull(getActivity())).getSupportActionBar()) | ||||
|                 ((AppCompatActivity) requireActivity()).getSupportActionBar()) | ||||
|                 .show(); | ||||
|         } | ||||
|     } | ||||
|  |  | |||
|  | @ -18,6 +18,9 @@ import android.view.ViewGroup; | |||
| import android.widget.CheckBox; | ||||
| import android.widget.ImageView; | ||||
| import android.widget.Toast; | ||||
| import androidx.activity.result.ActivityResult; | ||||
| import androidx.activity.result.ActivityResultLauncher; | ||||
| import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult; | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.exifinterface.media.ExifInterface; | ||||
|  | @ -58,9 +61,24 @@ import timber.log.Timber; | |||
| public class UploadMediaDetailFragment extends UploadBaseFragment implements | ||||
|     UploadMediaDetailsContract.View, UploadMediaDetailAdapter.EventListener { | ||||
| 
 | ||||
|     private static final int REQUEST_CODE = 1211; | ||||
|     private static final int REQUEST_CODE_FOR_EDIT_ACTIVITY = 1212; | ||||
|     private static final int REQUEST_CODE_FOR_VOICE_INPUT = 1213; | ||||
|     private UploadMediaDetailAdapter uploadMediaDetailAdapter; | ||||
| 
 | ||||
|     private final ActivityResultLauncher<Intent> startForResult = registerForActivityResult( | ||||
|         new StartActivityForResult(), result -> { | ||||
|                 onCameraPosition(result); | ||||
|         }); | ||||
| 
 | ||||
|     private final ActivityResultLauncher<Intent> startForEditActivityResult = registerForActivityResult( | ||||
|         new StartActivityForResult(), result -> { | ||||
|             onEditActivityResult(result); | ||||
|         } | ||||
|     ); | ||||
| 
 | ||||
|     private final ActivityResultLauncher<Intent> voiceInputResultLauncher = registerForActivityResult( | ||||
|         new StartActivityForResult(), result -> { | ||||
|             onVoiceInput(result); | ||||
|         } | ||||
|     ); | ||||
| 
 | ||||
|     public static Activity activity ; | ||||
| 
 | ||||
|  | @ -84,8 +102,6 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements | |||
|     private boolean hasUserRemovedLocation; | ||||
| 
 | ||||
| 
 | ||||
|     private UploadMediaDetailAdapter uploadMediaDetailAdapter; | ||||
| 
 | ||||
|     @Inject | ||||
|     UploadMediaDetailsContract.UserActionListener presenter; | ||||
| 
 | ||||
|  | @ -279,7 +295,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements | |||
|      */ | ||||
|     private void initRecyclerView() { | ||||
|         uploadMediaDetailAdapter = new UploadMediaDetailAdapter(this, | ||||
|             defaultKvStore.getString(Prefs.DESCRIPTION_LANGUAGE, ""), recentLanguagesDao); | ||||
|             defaultKvStore.getString(Prefs.DESCRIPTION_LANGUAGE, ""), recentLanguagesDao, voiceInputResultLauncher); | ||||
|         uploadMediaDetailAdapter.setCallback(this::showInfoAlert); | ||||
|         uploadMediaDetailAdapter.setEventListener(this); | ||||
|         binding.rvDescriptions.setLayoutManager(new LinearLayoutManager(getContext())); | ||||
|  | @ -593,14 +609,14 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements | |||
|      * This method is called to start the image editing activity for a specific UploadItem. | ||||
|      * It sets the UploadItem as the currently editable item, creates an intent to launch the | ||||
|      * EditActivity, and passes the image file path as an extra in the intent. The activity | ||||
|      * is started with a request code, allowing the result to be handled in onActivityResult. | ||||
|      * is started using resultLauncher that handles the result in respective callback. | ||||
|      */ | ||||
|     @Override | ||||
|     public void showEditActivity(UploadItem uploadItem) { | ||||
|         editableUploadItem = uploadItem; | ||||
|         Intent intent = new Intent(getContext(), EditActivity.class); | ||||
|         intent.putExtra("image", uploadableFile.getFilePath().toString()); | ||||
|         startActivityForResult(intent, REQUEST_CODE_FOR_EDIT_ACTIVITY); | ||||
|         startForEditActivityResult.launch(intent); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -615,6 +631,8 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements | |||
|         double defaultLongitude = -122.431297; | ||||
|         double defaultZoom = 16.0; | ||||
| 
 | ||||
|         final Intent locationPickerIntent; | ||||
| 
 | ||||
|         /* Retrieve image location from EXIF if present or | ||||
|            check if user has provided location while using the in-app camera. | ||||
|            Use location of last UploadItem if none of them is available */ | ||||
|  | @ -624,10 +642,11 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements | |||
|                 .getDecLatitude(); | ||||
|             defaultLongitude = uploadItem.getGpsCoords().getDecLongitude(); | ||||
|             defaultZoom = uploadItem.getGpsCoords().getZoomLevel(); | ||||
|             startActivityForResult(new LocationPicker.IntentBuilder() | ||||
| 
 | ||||
|             locationPickerIntent = new LocationPicker.IntentBuilder() | ||||
|                 .defaultLocation(new CameraPosition(defaultLatitude,defaultLongitude,defaultZoom)) | ||||
|                 .activityKey("UploadActivity") | ||||
|                 .build(getActivity()), REQUEST_CODE); | ||||
|                 .build(getActivity()); | ||||
|         } else { | ||||
|             if (defaultKvStore.getString(LAST_LOCATION) != null) { | ||||
|                 final String[] locationLatLng | ||||
|  | @ -638,27 +657,20 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements | |||
|             if (defaultKvStore.getString(LAST_ZOOM) != null) { | ||||
|                 defaultZoom = Double.parseDouble(defaultKvStore.getString(LAST_ZOOM)); | ||||
|             } | ||||
|             startActivityForResult(new LocationPicker.IntentBuilder() | ||||
| 
 | ||||
|             locationPickerIntent = new LocationPicker.IntentBuilder() | ||||
|                 .defaultLocation(new CameraPosition(defaultLatitude,defaultLongitude,defaultZoom)) | ||||
|                 .activityKey("NoLocationUploadActivity") | ||||
|                 .build(getActivity()), REQUEST_CODE); | ||||
|                 .build(getActivity()); | ||||
|         } | ||||
|         startForResult.launch(locationPickerIntent); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the coordinates and update the existing coordinates. | ||||
|      * @param requestCode code of request | ||||
|      * @param resultCode code of result | ||||
|      * @param data intent | ||||
|      */ | ||||
|     @Override | ||||
|     public void onActivityResult(final int requestCode, final int resultCode, | ||||
|         @Nullable final Intent data) { | ||||
|         super.onActivityResult(requestCode, resultCode, data); | ||||
|         if (requestCode == REQUEST_CODE && resultCode == RESULT_OK) { | ||||
|     private void onCameraPosition(ActivityResult result){ | ||||
|         if (result.getResultCode() == RESULT_OK) { | ||||
| 
 | ||||
|             assert data != null; | ||||
|             final CameraPosition cameraPosition = LocationPicker.getCameraPosition(data); | ||||
|             assert result.getData() != null; | ||||
|             final CameraPosition cameraPosition = LocationPicker.getCameraPosition(result.getData()); | ||||
| 
 | ||||
|             if (cameraPosition != null) { | ||||
| 
 | ||||
|  | @ -678,8 +690,21 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements | |||
|                 removeLocation(); | ||||
|             } | ||||
|         } | ||||
|         if (requestCode == REQUEST_CODE_FOR_EDIT_ACTIVITY && resultCode == RESULT_OK) { | ||||
|             String result = data.getStringExtra("editedImageFilePath"); | ||||
|     } | ||||
| 
 | ||||
|     private void onVoiceInput(ActivityResult result) { | ||||
|         if (result.getResultCode() == RESULT_OK && result.getData() != null) { | ||||
|             ArrayList<String> resultData = result.getData().getStringArrayListExtra( | ||||
|                 RecognizerIntent.EXTRA_RESULTS); | ||||
|             uploadMediaDetailAdapter.handleSpeechResult(resultData.get(0)); | ||||
|         }else { | ||||
|             Timber.e("Error %s", result.getResultCode()); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private void onEditActivityResult(ActivityResult result){ | ||||
|         if (result.getResultCode() == RESULT_OK) { | ||||
|             String path = result.getData().getStringExtra("editedImageFilePath"); | ||||
| 
 | ||||
|             if (Objects.equals(result, "Error")) { | ||||
|                 Timber.e("Error in rotating image"); | ||||
|  | @ -687,24 +712,15 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements | |||
|             } | ||||
|             try { | ||||
|                 if (binding != null){ | ||||
|                     binding.backgroundImage.setImageURI(Uri.fromFile(new File(result))); | ||||
|                     binding.backgroundImage.setImageURI(Uri.fromFile(new File(path))); | ||||
|                 } | ||||
|                 editableUploadItem.setContentUri(Uri.fromFile(new File(result))); | ||||
|                 editableUploadItem.setContentUri(Uri.fromFile(new File(path))); | ||||
|                 callback.changeThumbnail(indexOfFragment, | ||||
|                     result); | ||||
|                     path); | ||||
|             } catch (Exception e) { | ||||
|                 Timber.e(e); | ||||
|             } | ||||
|         } | ||||
|         else if (requestCode == REQUEST_CODE_FOR_VOICE_INPUT) { | ||||
|             if (resultCode == RESULT_OK && data != null) { | ||||
|                 ArrayList<String> result = data.getStringArrayListExtra( | ||||
|                     RecognizerIntent.EXTRA_RESULTS); | ||||
|                 uploadMediaDetailAdapter.handleSpeechResult(result.get(0)); | ||||
|             }else { | ||||
|                 Timber.e("Error %s", resultCode); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -809,7 +825,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements | |||
|     @Override | ||||
|     public void displayAddLocationDialog(final Runnable onSkipClicked) { | ||||
|         isMissingLocationDialog = true; | ||||
|         DialogUtil.showAlertDialog(Objects.requireNonNull(getActivity()), | ||||
|         DialogUtil.showAlertDialog(requireActivity(), | ||||
|             getString(R.string.no_location_found_title), | ||||
|             getString(R.string.no_location_found_message), | ||||
|             getString(R.string.add_location), | ||||
|  |  | |||
|  | @ -129,9 +129,9 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt | |||
|                         if (place.location != null) { | ||||
|                             final String countryCode = reverseGeoCode(place.location); | ||||
|                             if (countryCode != null && WLM_SUPPORTED_COUNTRIES | ||||
|                                 .contains(countryCode.toLowerCase())) { | ||||
|                                 .contains(countryCode.toLowerCase(Locale.ROOT))) { | ||||
|                                 uploadItem.setWLMUpload(true); | ||||
|                                 uploadItem.setCountryCode(countryCode.toLowerCase()); | ||||
|                                 uploadItem.setCountryCode(countryCode.toLowerCase(Locale.ROOT)); | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|  |  | |||
|  | @ -17,6 +17,7 @@ import androidx.work.Data | |||
| import androidx.work.ForegroundInfo | ||||
| import androidx.work.WorkerParameters | ||||
| import dagger.android.ContributesAndroidInjector | ||||
| import fr.free.nrw.commons.BuildConfig.HOME_URL | ||||
| import fr.free.nrw.commons.CommonsApplication | ||||
| import fr.free.nrw.commons.Media | ||||
| import fr.free.nrw.commons.R | ||||
|  | @ -30,6 +31,7 @@ import fr.free.nrw.commons.customselector.database.UploadedStatus | |||
| import fr.free.nrw.commons.customselector.database.UploadedStatusDao | ||||
| import fr.free.nrw.commons.di.ApplicationlessInjection | ||||
| import fr.free.nrw.commons.media.MediaClient | ||||
| import fr.free.nrw.commons.nearby.PlacesRepository | ||||
| import fr.free.nrw.commons.theme.BaseActivity | ||||
| import fr.free.nrw.commons.upload.FileUtilsWrapper | ||||
| import fr.free.nrw.commons.upload.StashUploadResult | ||||
|  | @ -38,12 +40,14 @@ import fr.free.nrw.commons.upload.UploadClient | |||
| import fr.free.nrw.commons.upload.UploadProgressActivity | ||||
| import fr.free.nrw.commons.upload.UploadResult | ||||
| import fr.free.nrw.commons.wikidata.WikidataEditService | ||||
| import io.reactivex.schedulers.Schedulers | ||||
| import kotlinx.coroutines.CoroutineScope | ||||
| import kotlinx.coroutines.Dispatchers | ||||
| import kotlinx.coroutines.MainScope | ||||
| import kotlinx.coroutines.launch | ||||
| import kotlinx.coroutines.withContext | ||||
| import timber.log.Timber | ||||
| import java.util.Date | ||||
| import java.util.Random | ||||
| import java.util.regex.Pattern | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
|  | @ -74,6 +78,9 @@ class UploadWorker( | |||
|     @Inject | ||||
|     lateinit var fileUtilsWrapper: FileUtilsWrapper | ||||
| 
 | ||||
|     @Inject | ||||
|     lateinit var placesRepository: PlacesRepository | ||||
| 
 | ||||
|     private val processingUploadsNotificationTag = BuildConfig.APPLICATION_ID + " : upload_tag" | ||||
| 
 | ||||
|     private val processingUploadsNotificationId = 101 | ||||
|  | @ -379,7 +386,7 @@ class UploadWorker( | |||
|                                 saveCompletedContribution(contribution, uploadResult) | ||||
|                             } else { | ||||
|                                 Timber.d( | ||||
|                                     "WikiDataEdit not required, making wikidata edit", | ||||
|                                     "WikiDataEdit required, making wikidata edit", | ||||
|                                 ) | ||||
|                                 makeWikiDataEdit(uploadResult, contribution) | ||||
|                             } | ||||
|  | @ -432,7 +439,7 @@ class UploadWorker( | |||
|                                 username, | ||||
|                             ) | ||||
|                         CommonsApplication | ||||
|                             .getInstance() | ||||
|                             .instance!! | ||||
|                             .clearApplicationData(appContext, logoutListener) | ||||
|                     } | ||||
|                 } | ||||
|  | @ -471,6 +478,16 @@ class UploadWorker( | |||
|                             contribution.media.captions, | ||||
|                         ) | ||||
|                     if (null != revisionID) { | ||||
|                         withContext(Dispatchers.IO) { | ||||
|                             val place = placesRepository.fetchPlace(wikiDataPlace.id); | ||||
|                             place.name = wikiDataPlace.name; | ||||
|                             place.pic = HOME_URL + uploadResult.createCanonicalFileName() | ||||
|                             placesRepository | ||||
|                                 .save(place) | ||||
|                                 .subscribeOn(Schedulers.io()) | ||||
|                                 .blockingAwait() | ||||
|                             Timber.d("Updated WikiItem place ${place.name} with image ${place.pic}") | ||||
|                         } | ||||
|                         showSuccessNotification(contribution) | ||||
|                     } | ||||
|                 } catch (exception: Exception) { | ||||
|  | @ -518,7 +535,7 @@ class UploadWorker( | |||
|         contribution.contentUri?.let { | ||||
|             val imageSha1 = contribution.imageSHA1.toString() | ||||
|             val modifiedSha1 = fileUtilsWrapper.getSHA1(fileUtilsWrapper.getFileInputStream(contribution.localUri?.path)) | ||||
|             MainScope().launch { | ||||
|             CoroutineScope(Dispatchers.IO).launch { | ||||
|                 uploadedStatusDao.insertUploaded( | ||||
|                     UploadedStatus( | ||||
|                         imageSha1, | ||||
|  | @ -532,33 +549,30 @@ class UploadWorker( | |||
|     } | ||||
| 
 | ||||
|     private fun findUniqueFileName(fileName: String): String { | ||||
|         var sequenceFileName: String? | ||||
|         var sequenceNumber = 1 | ||||
|         while (true) { | ||||
|         var sequenceFileName: String? = fileName | ||||
|         val random = Random() | ||||
| 
 | ||||
|         // Loops until sequenceFileName does not match any existing file names | ||||
|         while (mediaClient | ||||
|                 .checkPageExistsUsingTitle( | ||||
|                     String.format( | ||||
|                         "File:%s", | ||||
|                         sequenceFileName, | ||||
|                     ), | ||||
|                 ).blockingGet()) { | ||||
| 
 | ||||
|             // Generate a random 5-character alphanumeric string | ||||
|             val randomHash = (random.nextInt(90000) + 10000).toString() | ||||
| 
 | ||||
|             sequenceFileName = | ||||
|                 if (sequenceNumber == 1) { | ||||
|                     fileName | ||||
|                 if (fileName.indexOf('.') == -1) { | ||||
|                     "$fileName #$randomHash" | ||||
|                 } else { | ||||
|                     if (fileName.indexOf('.') == -1) { | ||||
|                         "$fileName $sequenceNumber" | ||||
|                     } else { | ||||
|                         val regex = | ||||
|                             Pattern.compile("^(.*)(\\..+?)$") | ||||
|                         val regexMatcher = regex.matcher(fileName) | ||||
|                         regexMatcher.replaceAll("$1 $sequenceNumber$2") | ||||
|                     } | ||||
|                     val regex = | ||||
|                         Pattern.compile("^(.*)(\\..+?)$") | ||||
|                     val regexMatcher = regex.matcher(fileName) | ||||
|                     regexMatcher.replaceAll("$1 #$randomHash") | ||||
|                 } | ||||
|             if (!mediaClient | ||||
|                     .checkPageExistsUsingTitle( | ||||
|                         String.format( | ||||
|                             "File:%s", | ||||
|                             sequenceFileName, | ||||
|                         ), | ||||
|                     ).blockingGet() | ||||
|             ) { | ||||
|                 break | ||||
|             } | ||||
|             sequenceNumber++ | ||||
|         } | ||||
|         return sequenceFileName!! | ||||
|     } | ||||
|  |  | |||
|  | @ -34,10 +34,15 @@ public class PermissionUtils { | |||
|             return new String[]{ Manifest.permission.READ_MEDIA_IMAGES, | ||||
|             Manifest. permission.ACCESS_MEDIA_LOCATION }; | ||||
|         } | ||||
|         if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { | ||||
|         if(Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) { | ||||
|             return new String[]{ Manifest.permission.READ_EXTERNAL_STORAGE, | ||||
|             Manifest.permission.ACCESS_MEDIA_LOCATION }; | ||||
|         } | ||||
|         if(Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) { | ||||
|             return new String[]{ Manifest.permission.READ_EXTERNAL_STORAGE, | ||||
|                 Manifest.permission.WRITE_EXTERNAL_STORAGE, | ||||
|                 Manifest.permission.ACCESS_MEDIA_LOCATION }; | ||||
|         } | ||||
|         return new String[]{ | ||||
|             Manifest.permission.READ_EXTERNAL_STORAGE, | ||||
|             Manifest.permission.WRITE_EXTERNAL_STORAGE }; | ||||
|  | @ -54,8 +59,7 @@ public class PermissionUtils { | |||
|         final Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); | ||||
|         final Uri uri = Uri.fromParts("package", activity.getPackageName(), null); | ||||
|         intent.setData(uri); | ||||
|         activity.startActivityForResult(intent, | ||||
|             CommonsApplication.OPEN_APPLICATION_DETAIL_SETTINGS); | ||||
|         activity.startActivity(intent); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  |  | |||
							
								
								
									
										18
									
								
								app/src/main/res/drawable/ic_refresh_24dp_nearby.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								app/src/main/res/drawable/ic_refresh_24dp_nearby.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,18 @@ | |||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|   android:width="@dimen/half_standard_height" | ||||
|   android:height="@dimen/half_standard_height" | ||||
|   android:viewportHeight="24.0" | ||||
|   android:viewportWidth="24.0"> | ||||
| 
 | ||||
|     <group | ||||
|       android:scaleX="1.0" | ||||
|     android:scaleY="1.0" | ||||
|     android:translateX="-0.0" | ||||
|     android:translateY="-0.0"> | ||||
| 
 | ||||
|     <path | ||||
|       android:fillColor="?attr/menu_item_tint" | ||||
|       android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/> | ||||
| 
 | ||||
| </group> | ||||
|   </vector> | ||||
|  | @ -11,7 +11,7 @@ | |||
|     android:id="@+id/partial_access_indicator" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="0dp" | ||||
|     app:layout_constraintTop_toBottomOf="@id/toolbar_layout"/> | ||||
|     app:layout_constraintTop_toBottomOf="@id/toolbar_layout" /> | ||||
| 
 | ||||
|   <androidx.fragment.app.FragmentContainerView | ||||
|     android:id="@+id/fragment_container" | ||||
|  |  | |||
|  | @ -36,11 +36,11 @@ | |||
|       android:layout_height="wrap_content" | ||||
|       android:layout_marginStart="16dp" | ||||
|       android:contentDescription="@string/exit_location_picker" | ||||
|       android:tint="@color/white" | ||||
|       app:layout_constraintBottom_toBottomOf="parent" | ||||
|       app:layout_constraintLeft_toLeftOf="parent" | ||||
|       app:layout_constraintTop_toTopOf="parent" | ||||
|       app:srcCompat="@drawable/ic_arrow_back_white" /> | ||||
|       app:srcCompat="@drawable/ic_arrow_back_white" | ||||
|       app:tint="@color/white" /> | ||||
| 
 | ||||
|   </androidx.constraintlayout.widget.ConstraintLayout> | ||||
| 
 | ||||
|  | @ -69,7 +69,7 @@ | |||
|       android:id="@+id/btn_edit_submit" | ||||
|       android:layout_width="wrap_content" | ||||
|       android:layout_height="wrap_content" | ||||
|       android:layout_alignParentRight="true" | ||||
|       android:layout_alignParentEnd="true" | ||||
|       android:text="@string/submit" | ||||
|       android:textColor="@android:color/white" /> | ||||
|   </RelativeLayout> | ||||
|  |  | |||
|  | @ -31,7 +31,7 @@ | |||
|                 android:layout_width="wrap_content" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:textSize="16sp" | ||||
|                 android:layout_marginRight="50dp" | ||||
|                 android:layout_marginEnd="50dp" | ||||
|                 android:maxLines="2" | ||||
|                 android:ellipsize="end" | ||||
|                 /> | ||||
|  | @ -58,6 +58,7 @@ | |||
|         android:layout_width="@dimen/dimen_0" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_weight="1" | ||||
|         android:focusable="true" | ||||
|         android:padding="@dimen/standard_gap" | ||||
|         android:clickable="true" | ||||
|         android:background="@drawable/button_background_selector" | ||||
|  | @ -69,8 +70,7 @@ | |||
|             android:layout_gravity="center_horizontal" | ||||
|             android:duplicateParentState="true" | ||||
|             app:srcCompat="@drawable/ic_directions_black_24dp" | ||||
|             android:tint="?attr/rowButtonColor" | ||||
|             /> | ||||
|             app:tint="?attr/rowButtonColor" /> | ||||
|         <TextView | ||||
|             android:layout_width="wrap_content" | ||||
|             android:layout_height="wrap_content" | ||||
|  | @ -89,6 +89,7 @@ | |||
|         android:layout_width="@dimen/dimen_0" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_weight="1" | ||||
|         android:focusable="true" | ||||
|         android:padding="@dimen/standard_gap" | ||||
|         android:clickable="true" | ||||
|         android:background="@drawable/button_background_selector" | ||||
|  | @ -118,6 +119,7 @@ | |||
|       android:layout_width="@dimen/dimen_0" | ||||
|       android:layout_height="wrap_content" | ||||
|       android:layout_weight="1" | ||||
|       android:focusable="true" | ||||
|       android:padding="@dimen/standard_gap" | ||||
|       android:clickable="true" | ||||
|       android:background="@drawable/button_background_selector" | ||||
|  | @ -153,8 +155,8 @@ | |||
|     android:id="@+id/description" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="wrap_content" | ||||
|     android:layout_marginLeft="@dimen/large_height" | ||||
|     android:layout_marginRight="@dimen/standard_gap" | ||||
|     android:layout_marginStart="@dimen/large_height" | ||||
|     android:layout_marginEnd="@dimen/standard_gap" | ||||
|     android:layout_marginBottom="@dimen/standard_gap" | ||||
|     android:textSize="16sp" /> | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,11 +1,13 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|   xmlns:app="http://schemas.android.com/apk/res-auto" | ||||
|   android:id="@+id/bookmarkButton" | ||||
|   android:layout_width="match_parent" | ||||
|   android:layout_height="wrap_content" | ||||
|   android:layout_columnWeight="1" | ||||
|   android:background="@drawable/button_background_selector" | ||||
|   android:clickable="true" | ||||
|   android:focusable="true" | ||||
|   android:orientation="vertical" | ||||
|   android:padding="@dimen/standard_gap"> | ||||
| 
 | ||||
|  | @ -14,7 +16,7 @@ | |||
|     android:layout_width="wrap_content" | ||||
|     android:layout_height="wrap_content" | ||||
|     android:layout_gravity="center_horizontal" | ||||
|     android:tint="?attr/rowButtonColor" /> | ||||
|     app:tint="?attr/rowButtonColor" /> | ||||
| 
 | ||||
|   <TextView | ||||
|     android:id="@+id/buttonText" | ||||
|  |  | |||
|  | @ -40,17 +40,36 @@ | |||
|       app:layout_constraintStart_toEndOf="@+id/back" | ||||
|       app:layout_constraintTop_toTopOf="parent" /> | ||||
| 
 | ||||
|     <!-- Warning Icon (image_limit_error) --> | ||||
|     <ImageButton | ||||
|       android:id="@+id/image_limit_error" | ||||
|       android:layout_width="48dp" | ||||
|       android:layout_height="0dp" | ||||
|       android:layout_width="wrap_content" | ||||
|       android:layout_height="match_parent" | ||||
|       android:background="#00FFFFFF" | ||||
|       android:padding="@dimen/standard_gap" | ||||
|       android:contentDescription="@string/custom_selector_limit_error_desc" | ||||
|       app:layout_constraintBottom_toBottomOf="parent" | ||||
|       app:layout_constraintEnd_toStartOf="@id/menu_overflow" | ||||
|       app:layout_constraintTop_toTopOf="parent" | ||||
|       app:layout_constraintVertical_bias="1.0" | ||||
|       app:layout_constraintStart_toEndOf="@id/title" | ||||
|       app:srcCompat="@drawable/ic_error_red_24dp" /> | ||||
| 
 | ||||
|     <!-- Overflow Menu Icon (menu_overflow) --> | ||||
|     <ImageButton | ||||
|       android:id="@+id/menu_overflow" | ||||
|       android:layout_width="wrap_content" | ||||
|       android:layout_height="match_parent" | ||||
|       android:background="#00FFFFFF" | ||||
|       android:padding="@dimen/standard_gap" | ||||
|       android:contentDescription="@string/menu_overflow_desc" | ||||
|       app:layout_constraintBottom_toBottomOf="parent" | ||||
|       app:layout_constraintEnd_toEndOf="parent" | ||||
|       app:layout_constraintTop_toTopOf="parent" | ||||
|       app:layout_constraintVertical_bias="1.0" | ||||
|       app:srcCompat="@drawable/ic_error_red_24dp" /> | ||||
|       app:layout_constraintStart_toEndOf="@id/image_limit_error" | ||||
|       app:srcCompat="@drawable/ic_overflow" /> | ||||
| 
 | ||||
| 
 | ||||
|   </androidx.constraintlayout.widget.ConstraintLayout> | ||||
| </merge> | ||||
|  | @ -28,7 +28,6 @@ | |||
|         <RelativeLayout | ||||
|           android:layout_width="match_parent" | ||||
|           android:layout_height="wrap_content" | ||||
|           android:layout_below="@+id/toolbar" | ||||
|           android:background="?attr/achievementBackground" | ||||
|           android:orientation="vertical"> | ||||
| 
 | ||||
|  | @ -36,7 +35,6 @@ | |||
|             style="?android:textAppearanceLarge" | ||||
|             android:layout_width="wrap_content" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:layout_marginLeft="@dimen/activity_margin_horizontal" | ||||
|             android:layout_marginStart="@dimen/activity_margin_horizontal" | ||||
|             android:layout_marginTop="@dimen/activity_margin_horizontal" | ||||
|             android:text="@string/level" | ||||
|  | @ -48,13 +46,11 @@ | |||
|             android:layout_width="wrap_content" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:layout_marginTop="@dimen/activity_margin_vertical" | ||||
|             android:layout_marginRight="@dimen/activity_margin_horizontal" | ||||
|             android:layout_marginEnd="@dimen/activity_margin_horizontal" | ||||
|             android:layout_alignParentRight="true" | ||||
|             android:layout_alignParentEnd="true" | ||||
|             app:srcCompat="@drawable/ic_info_outline_24dp" | ||||
|             android:tint="@color/black" | ||||
|             android:layout_marginVertical="@dimen/activity_margin_vertical" /> | ||||
|             android:layout_marginVertical="@dimen/activity_margin_vertical" | ||||
|             app:tint="@color/black" /> | ||||
| 
 | ||||
|           <androidx.constraintlayout.widget.ConstraintLayout | ||||
|             android:id="@+id/badge_layout" | ||||
|  | @ -108,7 +104,6 @@ | |||
|                 style="?android:textAppearanceMedium" | ||||
|                 android:layout_width="wrap_content" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:layout_marginLeft="@dimen/activity_margin_horizontal" | ||||
|                 android:layout_marginStart="@dimen/activity_margin_horizontal" | ||||
|                 android:id="@+id/images_upload_text_param" | ||||
|                 android:layout_marginTop="@dimen/achievements_activity_margin_vertical" | ||||
|  | @ -120,12 +115,10 @@ | |||
|                 android:layout_marginTop="@dimen/activity_margin_horizontal" | ||||
|                 android:layout_marginRight="@dimen/activity_margin_horizontal" | ||||
|                 android:layout_marginEnd="@dimen/activity_margin_horizontal" | ||||
|                 android:layout_toRightOf="@+id/images_upload_text_param" | ||||
|                 android:layout_toEndOf="@+id/images_upload_text_param" | ||||
|                 app:srcCompat="@drawable/ic_info_outline_24dp" | ||||
|                 android:tint="@color/primaryLightColor" | ||||
|                 android:layout_marginLeft="@dimen/activity_margin_horizontal" | ||||
|                 android:layout_marginStart="@dimen/activity_margin_horizontal"/> | ||||
|                 android:layout_marginStart="@dimen/activity_margin_horizontal" | ||||
|                 app:tint="@color/primaryLightColor" /> | ||||
| 
 | ||||
|             </LinearLayout> | ||||
| 
 | ||||
|  | @ -189,7 +182,6 @@ | |||
|                 style="?android:textAppearanceMedium" | ||||
|                 android:layout_width="wrap_content" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:layout_marginLeft="@dimen/activity_margin_horizontal" | ||||
|                 android:id="@+id/images_reverted_text" | ||||
|                 android:layout_marginStart="@dimen/activity_margin_horizontal" | ||||
|                 android:text="@string/image_reverts" /> | ||||
|  | @ -200,24 +192,19 @@ | |||
|                 android:layout_marginTop="@dimen/activity_margin_horizontal" | ||||
|                 android:layout_marginRight="@dimen/activity_margin_horizontal" | ||||
|                 android:layout_marginEnd="@dimen/activity_margin_horizontal" | ||||
|                 android:layout_toRightOf="@+id/images_reverted_text" | ||||
|                 android:layout_toEndOf="@+id/images_reverted_text" | ||||
|                 app:srcCompat="@drawable/ic_info_outline_24dp" | ||||
|                 android:tint="@color/primaryLightColor" | ||||
|                 android:layout_marginLeft="@dimen/activity_margin_horizontal" | ||||
|                 android:layout_marginStart="@dimen/activity_margin_horizontal"/> | ||||
|                 android:layout_marginStart="@dimen/activity_margin_horizontal" app:tint="@color/primaryLightColor" /> | ||||
| 
 | ||||
|             </LinearLayout> | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|             <TextView | ||||
|               android:layout_width="wrap_content" | ||||
|               android:layout_height="wrap_content" | ||||
|               android:text="@string/achievements_revert_limit_message" | ||||
|               android:textSize="@dimen/small_text" | ||||
|               android:id="@+id/images_revert_limit_text" | ||||
|               android:layout_marginLeft="@dimen/activity_margin_horizontal" | ||||
|               android:layout_marginStart="@dimen/activity_margin_horizontal" | ||||
|               android:layout_below="@id/images_reverted_info"/> | ||||
| 
 | ||||
|  | @ -278,7 +265,6 @@ | |||
|                 android:layout_width="wrap_content" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:id="@+id/images_used_by_wiki_text" | ||||
|                 android:layout_marginLeft="@dimen/activity_margin_horizontal" | ||||
|                 android:layout_marginStart="@dimen/activity_margin_horizontal" | ||||
|                 android:layout_marginTop="@dimen/achievements_activity_margin_vertical" | ||||
|                 android:text="@string/images_used_by_wiki" /> | ||||
|  | @ -289,12 +275,10 @@ | |||
|                 android:layout_marginTop="@dimen/activity_margin_horizontal" | ||||
|                 android:layout_marginRight="@dimen/activity_margin_horizontal" | ||||
|                 android:layout_marginEnd="@dimen/activity_margin_horizontal" | ||||
|                 android:layout_toRightOf="@+id/images_used_by_wiki_text" | ||||
|                 android:layout_toEndOf="@+id/images_used_by_wiki_text" | ||||
|                 app:srcCompat="@drawable/ic_info_outline_24dp" | ||||
|                 android:tint="@color/primaryLightColor" | ||||
|                 android:layout_marginLeft="@dimen/activity_margin_horizontal" | ||||
|                 android:layout_marginStart="@dimen/activity_margin_horizontal"/> | ||||
|                 android:layout_marginStart="@dimen/activity_margin_horizontal" | ||||
|                 app:tint="@color/primaryLightColor" /> | ||||
| 
 | ||||
|             </LinearLayout> | ||||
| 
 | ||||
|  | @ -353,7 +337,6 @@ | |||
|             android:layout_height="wrap_content" | ||||
|             android:text="@string/statistics" | ||||
|             style="?android:textAppearanceLarge" | ||||
|             android:layout_marginLeft="@dimen/activity_margin_horizontal" | ||||
|             android:layout_marginStart="@dimen/activity_margin_horizontal" | ||||
|             android:layout_marginTop="@dimen/activity_margin_vertical" | ||||
|             android:textAllCaps="true"/> | ||||
|  | @ -373,9 +356,7 @@ | |||
|               android:id="@+id/images_nearby_info" | ||||
|               android:layout_centerVertical="true" | ||||
|               android:layout_alignParentStart="true" | ||||
|               android:layout_alignParentLeft="true" | ||||
|               android:layout_toStartOf="@+id/wikidata_edits" | ||||
|               android:layout_toLeftOf="@+id/wikidata_edits" | ||||
|               android:orientation="horizontal" | ||||
|               android:gravity="center_vertical"> | ||||
| 
 | ||||
|  | @ -407,14 +388,13 @@ | |||
|                 android:layout_height="@dimen/medium_height" | ||||
|                 android:id="@+id/images_nearby_info_icon" | ||||
|                 android:layout_marginTop="@dimen/activity_margin_horizontal" | ||||
|                 android:layout_marginRight="@dimen/activity_margin_horizontal" | ||||
|                 android:layout_marginEnd="@dimen/activity_margin_horizontal" | ||||
|                 android:layout_gravity="top" | ||||
|                 app:layout_constraintLeft_toRightOf="@id/images_nearby_data" | ||||
|                 app:layout_constraintTop_toTopOf="parent" | ||||
|                 app:layout_constraintRight_toRightOf="parent" | ||||
|                 app:srcCompat="@drawable/ic_info_outline_24dp" | ||||
|                 android:tint="@color/primaryLightColor" /> | ||||
|                 app:tint="@color/primaryLightColor" /> | ||||
| 
 | ||||
|             </androidx.constraintlayout.widget.ConstraintLayout> | ||||
| 
 | ||||
|  | @ -423,16 +403,14 @@ | |||
|               android:layout_width="wrap_content" | ||||
|               android:layout_height="wrap_content" | ||||
|               style="?android:textAppearanceMedium" | ||||
|               android:layout_alignParentRight="true" | ||||
|               android:layout_alignParentEnd="true" | ||||
|               android:layout_marginLeft="@dimen/activity_margin_horizontal" | ||||
|               android:layout_marginEnd="@dimen/half_standard_height" | ||||
|               android:layout_marginTop="@dimen/activity_margin_horizontal" | ||||
|               android:layout_marginStart="@dimen/activity_margin_horizontal" | ||||
|               android:layout_centerVertical="true" | ||||
|               tools:text="2" | ||||
|               android:id="@+id/wikidata_edits" | ||||
|               android:layout_marginRight="@dimen/half_standard_height" /> | ||||
|               /> | ||||
| 
 | ||||
|           </RelativeLayout> | ||||
| 
 | ||||
|  | @ -451,9 +429,7 @@ | |||
|               android:id="@+id/images_featured_info" | ||||
|               android:layout_centerVertical="true" | ||||
|               android:layout_alignParentStart="true" | ||||
|               android:layout_alignParentLeft="true" | ||||
|               android:layout_toStartOf="@+id/image_featured" | ||||
|               android:layout_toLeftOf="@+id/image_featured" | ||||
|               android:orientation="horizontal" | ||||
|               android:gravity="center_vertical"> | ||||
| 
 | ||||
|  | @ -486,14 +462,13 @@ | |||
|                 android:layout_height="@dimen/medium_height" | ||||
|                 android:id="@+id/images_featured_info_icon" | ||||
|                 android:layout_marginTop="@dimen/activity_margin_horizontal" | ||||
|                 android:layout_marginRight="@dimen/activity_margin_horizontal" | ||||
|                 android:layout_marginEnd="@dimen/activity_margin_horizontal" | ||||
|                 app:layout_constraintLeft_toRightOf="@id/images_featured_data" | ||||
|                 app:layout_constraintTop_toTopOf="parent" | ||||
|                 app:layout_constraintRight_toRightOf="parent" | ||||
|                 android:layout_gravity="top" | ||||
|                 app:srcCompat="@drawable/ic_info_outline_24dp" | ||||
|                 android:tint="@color/primaryLightColor" /> | ||||
|                 app:tint="@color/primaryLightColor" /> | ||||
| 
 | ||||
|             </androidx.constraintlayout.widget.ConstraintLayout> | ||||
| 
 | ||||
|  | @ -501,16 +476,14 @@ | |||
|               android:layout_width="wrap_content" | ||||
|               android:layout_height="wrap_content" | ||||
|               style="?android:textAppearanceMedium" | ||||
|               android:layout_alignParentRight="true" | ||||
|               android:layout_alignParentEnd="true" | ||||
|               android:layout_marginTop="@dimen/activity_margin_horizontal" | ||||
|               android:layout_marginStart="@dimen/activity_margin_horizontal" | ||||
|               android:layout_centerVertical="true" | ||||
|               tools:text="2" | ||||
|               android:id="@+id/image_featured" | ||||
|               android:layout_marginLeft="@dimen/activity_margin_horizontal" | ||||
|               android:layout_marginEnd="@dimen/half_standard_height" | ||||
|               android:layout_marginRight="@dimen/half_standard_height" /> | ||||
|               /> | ||||
| 
 | ||||
|           </RelativeLayout> | ||||
| 
 | ||||
|  | @ -529,9 +502,7 @@ | |||
|               android:id="@+id/quality_images_info" | ||||
|               android:layout_centerVertical="true" | ||||
|               android:layout_alignParentStart="true" | ||||
|               android:layout_alignParentLeft="true" | ||||
|               android:layout_toStartOf="@+id/quality_images" | ||||
|               android:layout_toLeftOf="@+id/quality_images" | ||||
|               android:orientation="horizontal" | ||||
|               android:gravity="center_vertical"> | ||||
| 
 | ||||
|  | @ -564,14 +535,13 @@ | |||
|                 android:layout_height="@dimen/medium_height" | ||||
|                 android:id="@+id/quality_images_info_icon" | ||||
|                 android:layout_marginTop="@dimen/activity_margin_horizontal" | ||||
|                 android:layout_marginRight="@dimen/activity_margin_horizontal" | ||||
|                 android:layout_marginEnd="@dimen/activity_margin_horizontal" | ||||
|                 app:layout_constraintLeft_toRightOf="@id/quality_images_data" | ||||
|                 app:layout_constraintTop_toTopOf="parent" | ||||
|                 app:layout_constraintRight_toRightOf="parent" | ||||
|                 android:layout_gravity="top" | ||||
|                 app:srcCompat="@drawable/ic_info_outline_24dp" | ||||
|                 android:tint="@color/primaryLightColor" /> | ||||
|                 app:tint="@color/primaryLightColor" /> | ||||
| 
 | ||||
|             </androidx.constraintlayout.widget.ConstraintLayout> | ||||
| 
 | ||||
|  | @ -579,7 +549,6 @@ | |||
|               android:layout_width="wrap_content" | ||||
|               android:layout_height="wrap_content" | ||||
|               style="?android:textAppearanceMedium" | ||||
|               android:layout_alignParentRight="true" | ||||
|               android:layout_alignParentEnd="true" | ||||
|               android:layout_marginTop="@dimen/activity_margin_horizontal" | ||||
|               android:layout_marginStart="@dimen/activity_margin_horizontal" | ||||
|  | @ -587,9 +556,8 @@ | |||
|               tools:text="2" | ||||
|               android:text="0" | ||||
|               android:id="@+id/quality_images" | ||||
|               android:layout_marginLeft="@dimen/activity_margin_horizontal" | ||||
|               android:layout_marginEnd="@dimen/half_standard_height" | ||||
|               android:layout_marginRight="@dimen/half_standard_height" /> | ||||
|               /> | ||||
| 
 | ||||
|           </RelativeLayout> | ||||
| 
 | ||||
|  | @ -608,9 +576,7 @@ | |||
|               android:id="@+id/thanks_received_info" | ||||
|               android:layout_centerVertical="true" | ||||
|               android:layout_alignParentStart="true" | ||||
|               android:layout_alignParentLeft="true" | ||||
|               android:layout_toStartOf="@+id/thanks_received" | ||||
|               android:layout_toLeftOf="@+id/thanks_received" | ||||
|               android:orientation="horizontal" | ||||
|               android:gravity="center_vertical"> | ||||
| 
 | ||||
|  | @ -643,14 +609,13 @@ | |||
|                 android:layout_height="@dimen/medium_height" | ||||
|                 android:id="@+id/thanks_received_info_icon" | ||||
|                 android:layout_marginTop="@dimen/activity_margin_horizontal" | ||||
|                 android:layout_marginRight="@dimen/activity_margin_horizontal" | ||||
|                 android:layout_marginEnd="@dimen/activity_margin_horizontal" | ||||
|                 app:layout_constraintLeft_toRightOf="@id/thanks_received_data" | ||||
|                 app:layout_constraintTop_toTopOf="parent" | ||||
|                 app:layout_constraintRight_toRightOf="parent" | ||||
|                 android:layout_gravity="top" | ||||
|                 app:srcCompat="@drawable/ic_info_outline_24dp" | ||||
|                 android:tint="@color/primaryLightColor" /> | ||||
|                 app:tint="@color/primaryLightColor" /> | ||||
| 
 | ||||
|             </androidx.constraintlayout.widget.ConstraintLayout> | ||||
| 
 | ||||
|  | @ -658,16 +623,14 @@ | |||
|               android:layout_width="wrap_content" | ||||
|               android:layout_height="wrap_content" | ||||
|               style="?android:textAppearanceMedium" | ||||
|               android:layout_alignParentRight="true" | ||||
|               android:layout_alignParentEnd="true" | ||||
|               android:layout_marginTop="@dimen/activity_margin_horizontal" | ||||
|               android:layout_marginStart="@dimen/activity_margin_horizontal" | ||||
|               android:layout_marginLeft="@dimen/activity_margin_horizontal" | ||||
|               android:layout_centerVertical="true" | ||||
|               tools:text="2" | ||||
|               android:id="@+id/thanks_received" | ||||
|               android:layout_marginEnd="@dimen/half_standard_height" | ||||
|               android:layout_marginRight="@dimen/half_standard_height" /> | ||||
|               /> | ||||
| 
 | ||||
|           </RelativeLayout> | ||||
| 
 | ||||
|  |  | |||
|  | @ -124,6 +124,33 @@ | |||
|           app:srcCompat="@drawable/ic_my_location_black_24dp" | ||||
|           app:useCompatPadding="true" /> | ||||
| 
 | ||||
|         <com.google.android.material.floatingactionbutton.FloatingActionButton | ||||
|           android:id="@+id/fab_legend" | ||||
|           android:layout_width="wrap_content" | ||||
|           android:layout_height="wrap_content" | ||||
|           android:layout_below="@id/fab_recenter" | ||||
|           android:layout_alignParentEnd="true" | ||||
|           android:layout_alignParentRight="true" | ||||
|           android:clickable="true" | ||||
|           android:visibility="visible" | ||||
|           app:backgroundTint="@color/main_background_light" | ||||
|           app:elevation="@dimen/dimen_6" | ||||
|           app:fabSize="normal" | ||||
|           app:layout_anchorGravity="top|right|end" | ||||
|           app:srcCompat="@drawable/ic_info_outline_24dp" | ||||
|           app:useCompatPadding="true" /> | ||||
| 
 | ||||
|         <include | ||||
|           android:id="@+id/nearby_legend_layout" | ||||
|           layout="@layout/nearby_legend" | ||||
|           android:layout_width="wrap_content" | ||||
|           android:layout_height="wrap_content" | ||||
|           android:layout_below="@id/rl_container_wlm_month_message" | ||||
|           android:visibility="gone" | ||||
|           android:layout_marginTop="30dp" | ||||
|           android:layout_marginStart="5dp" | ||||
|           /> | ||||
| 
 | ||||
|       </RelativeLayout> | ||||
| 
 | ||||
|       <FrameLayout | ||||
|  |  | |||
|  | @ -11,8 +11,8 @@ | |||
|       android:layout_width="wrap_content" | ||||
|       android:layout_height="wrap_content" | ||||
|       android:layout_margin="@dimen/standard_gap" | ||||
|       android:tint="?attr/rowButtonColor" | ||||
|       app:srcCompat="@drawable/ic_round_star_border_24px" /> | ||||
|       app:srcCompat="@drawable/ic_round_star_border_24px" | ||||
|       app:tint="?attr/rowButtonColor" /> | ||||
| 
 | ||||
|     <com.facebook.drawee.view.SimpleDraweeView | ||||
|         android:id="@+id/icon" | ||||
|  | @ -30,7 +30,6 @@ | |||
|         android:layout_width="wrap_content" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_alignParentEnd="true" | ||||
|         android:layout_alignParentRight="true" | ||||
|         android:layout_marginLeft="@dimen/standard_gap" | ||||
|         android:layout_marginRight="@dimen/standard_gap" | ||||
|         android:layout_marginTop="@dimen/standard_gap" | ||||
|  | @ -43,8 +42,7 @@ | |||
|         android:layout_width="wrap_content" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_alignParentEnd="true" | ||||
|         android:layout_alignParentRight="true" | ||||
|         android:layout_marginRight="@dimen/standard_gap" | ||||
|         android:layout_marginEnd="@dimen/standard_gap" | ||||
|         android:layout_marginTop="@dimen/large_gap" | ||||
| 
 | ||||
|         /> | ||||
|  | @ -54,11 +52,8 @@ | |||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_alignTop="@id/distance" | ||||
|         android:layout_marginLeft="@dimen/standard_gap" | ||||
|         android:layout_marginStart="@dimen/standard_gap" | ||||
|         android:layout_toEndOf="@id/icon" | ||||
|         android:layout_toLeftOf="@id/distance" | ||||
|         android:layout_toRightOf="@id/icon" | ||||
|         android:layout_toStartOf="@id/distance" | ||||
|         android:ellipsize="end" | ||||
|         android:maxLines="2" | ||||
|  | @ -71,8 +66,6 @@ | |||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_alignEnd="@id/distance" | ||||
|         android:layout_alignLeft="@id/tvName" | ||||
|         android:layout_alignRight="@id/distance" | ||||
|         android:layout_alignStart="@id/tvName" | ||||
|         android:layout_below="@id/tvName" | ||||
|         android:layout_marginBottom="@dimen/standard_gap" | ||||
|  |  | |||
|  | @ -19,17 +19,14 @@ | |||
|       android:id="@+id/iv_campaign" | ||||
|       android:layout_width="@dimen/dimen_40" | ||||
|       android:layout_height="@dimen/dimen_40" | ||||
|       android:layout_marginLeft="@dimen/standard_gap" | ||||
|       android:layout_marginStart="@dimen/standard_gap" | ||||
|       android:scaleType="centerCrop" | ||||
|       app:srcCompat="@drawable/ic_campaign" | ||||
|       android:tint="?attr/card_item_color" | ||||
|       /> | ||||
|     app:tint="?attr/card_item_color" /> | ||||
|   <LinearLayout | ||||
|       android:layout_width="match_parent" | ||||
|       android:layout_height="wrap_content" | ||||
|       android:layout_centerInParent="true" | ||||
|       android:orientation="horizontal" | ||||
|     android:orientation="horizontal" | ||||
|       android:layout_gravity="center_vertical" | ||||
|       android:gravity="center_vertical" | ||||
|       android:weightSum="4"> | ||||
|  | @ -37,15 +34,13 @@ | |||
|   <androidx.constraintlayout.widget.ConstraintLayout | ||||
|       android:layout_width="match_parent" | ||||
|       android:layout_height="wrap_content" | ||||
|       android:layout_marginLeft="@dimen/standard_gap" | ||||
|       android:layout_marginRight="@dimen/tiny_margin" | ||||
|       android:layout_centerInParent="true" | ||||
|       > | ||||
|       android:layout_marginStart="@dimen/standard_gap" | ||||
|       android:layout_marginEnd="@dimen/tiny_margin"> | ||||
|     <TextView | ||||
|         android:id="@+id/tv_title" | ||||
|         android:layout_width="wrap_content" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_marginLeft="@dimen/standard_gap" | ||||
|         android:layout_marginStart="@dimen/standard_gap" | ||||
|         android:textColor="?attr/card_item_color" | ||||
|         android:textStyle="bold" | ||||
|         tools:text="Campaign Title" | ||||
|  | @ -55,7 +50,7 @@ | |||
|         android:id="@+id/tv_description" | ||||
|         android:layout_width="wrap_content" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_marginLeft="@dimen/standard_gap" | ||||
|         android:layout_marginStart="@dimen/standard_gap" | ||||
|         android:gravity="start" | ||||
|         android:paddingTop="@dimen/miniscule_margin" | ||||
|         android:textAlignment="textStart" | ||||
|  | @ -69,7 +64,7 @@ | |||
|         android:id="@+id/tv_dates" | ||||
|         android:layout_width="@dimen/dimen_0" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_marginLeft="@dimen/standard_gap" | ||||
|         android:layout_marginStart="@dimen/standard_gap" | ||||
|         android:layout_weight="1" | ||||
|         android:paddingTop="@dimen/miniscule_margin" | ||||
|         android:text="@string/ends_on" | ||||
|  |  | |||
|  | @ -113,9 +113,9 @@ | |||
|         android:background="@android:color/transparent" | ||||
|         android:padding="@dimen/activity_margin_horizontal" | ||||
|         android:src="@drawable/ic_wikipedia" | ||||
|         android:tint="?attr/contributionsListTextSecondary" | ||||
|         android:text="@string/menu_cancel_upload" | ||||
|         android:visibility="visible" /> | ||||
|         android:visibility="visible" | ||||
|         app:tint="?attr/contributionsListTextSecondary" /> | ||||
| 
 | ||||
|     </RelativeLayout> | ||||
| 
 | ||||
|  |  | |||
|  | @ -14,7 +14,6 @@ | |||
|       style="@style/Widget.AppCompat.Button.Borderless" | ||||
|       android:layout_width="wrap_content" | ||||
|       android:layout_height="wrap_content" | ||||
|       android:layout_centerInParent="true" | ||||
|       android:layout_marginLeft="@dimen/activity_margin_horizontal" | ||||
|       android:layout_marginTop="@dimen/activity_margin_horizontal" | ||||
|       android:layout_marginRight="@dimen/activity_margin_horizontal" | ||||
|  | @ -30,34 +29,28 @@ | |||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:id="@+id/content_layout" | ||||
|         android:layout_centerInParent="true" | ||||
|       android:orientation="horizontal" | ||||
|         > | ||||
| 
 | ||||
|         <ProgressBar | ||||
|             android:layout_width="wrap_content" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:id="@+id/progressBar" | ||||
|             android:layout_centerInParent="true" | ||||
|             /> | ||||
|             android:id="@+id/progressBar" /> | ||||
| 
 | ||||
|         <ImageView | ||||
|             android:id="@+id/nearby_icon" | ||||
|             android:layout_width="@dimen/dimen_40" | ||||
|             android:layout_height="@dimen/dimen_40" | ||||
|             android:layout_marginLeft="@dimen/standard_gap" | ||||
|             android:layout_marginStart="@dimen/standard_gap" | ||||
|             android:scaleType="centerCrop" | ||||
|             app:srcCompat="@drawable/ic_location_white_24dp" | ||||
|             android:tint="?attr/card_item_color" | ||||
|           /> | ||||
|           app:tint="?attr/card_item_color" /> | ||||
| 
 | ||||
| 
 | ||||
|         <LinearLayout | ||||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:layout_centerInParent="true" | ||||
|             android:orientation="horizontal" | ||||
|           android:orientation="horizontal" | ||||
|             android:layout_gravity="center_vertical" | ||||
|             android:gravity="center_vertical" | ||||
|             android:weightSum="4" | ||||
|  | @ -68,8 +61,7 @@ | |||
|                 android:layout_width="@dimen/dimen_0" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:layout_weight="3" | ||||
|                 android:layout_centerInParent="true" | ||||
|                 android:layout_marginLeft="@dimen/standard_gap" | ||||
|               android:layout_marginLeft="@dimen/standard_gap" | ||||
|                 android:layout_marginRight="@dimen/standard_gap" | ||||
|                 tools:text="test distance" | ||||
|                 android:textColor="?attr/card_item_color" | ||||
|  |  | |||
							
								
								
									
										74
									
								
								app/src/main/res/layout/nearby_legend.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								app/src/main/res/layout/nearby_legend.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,74 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|   xmlns:app="http://schemas.android.com/apk/res-auto" | ||||
|   xmlns:tools="http://schemas.android.com/tools" | ||||
|   android:layout_width="wrap_content" | ||||
|   android:layout_height="wrap_content" | ||||
|   android:background="#BFFFFFFF" | ||||
|   android:orientation="vertical"> | ||||
| 
 | ||||
|   <ImageView | ||||
|     android:id="@+id/imageRed" | ||||
|     android:layout_width="wrap_content" | ||||
|     android:layout_height="wrap_content" | ||||
|     android:layout_marginBottom="0dp" | ||||
|     app:layout_constraintBottom_toTopOf="@+id/imageGreen" | ||||
|     app:layout_constraintEnd_toEndOf="@+id/imageGreen" | ||||
|     app:layout_constraintStart_toStartOf="@+id/imageGreen" | ||||
|     app:srcCompat="@drawable/ic_custom_map_marker_red" /> | ||||
| 
 | ||||
|   <TextView | ||||
|     android:id="@+id/textRed" | ||||
|     android:layout_width="wrap_content" | ||||
|     android:layout_height="wrap_content" | ||||
|     android:layout_marginStart="0dp" | ||||
|     android:text="@string/red_pin" | ||||
|     android:textColor="#F74D4D" | ||||
|     android:textSize="12sp" | ||||
|     app:layout_constraintBottom_toBottomOf="@+id/imageRed" | ||||
|     app:layout_constraintStart_toEndOf="@+id/imageRed" | ||||
|     app:layout_constraintTop_toTopOf="@+id/imageRed" /> | ||||
| 
 | ||||
|   <ImageView | ||||
|     android:id="@+id/imageGreen" | ||||
|     android:layout_width="wrap_content" | ||||
|     android:layout_height="wrap_content" | ||||
|     android:layout_marginBottom="0dp" | ||||
|     app:layout_constraintBottom_toTopOf="@+id/imageGrey" | ||||
|     app:layout_constraintEnd_toEndOf="@+id/imageGrey" | ||||
|     app:layout_constraintStart_toStartOf="@+id/imageGrey" | ||||
|     app:srcCompat="@drawable/ic_custom_map_marker_green" /> | ||||
| 
 | ||||
|   <TextView | ||||
|     android:id="@+id/textGreen" | ||||
|     android:layout_width="wrap_content" | ||||
|     android:layout_height="wrap_content" | ||||
|     android:layout_marginStart="0dp" | ||||
|     android:text="@string/green_pin" | ||||
|     android:textColor="#1F7123" | ||||
|     android:textSize="12sp" | ||||
|     app:layout_constraintBottom_toBottomOf="@+id/imageGreen" | ||||
|     app:layout_constraintStart_toEndOf="@+id/imageGreen" | ||||
|     app:layout_constraintTop_toTopOf="@+id/imageGreen" /> | ||||
| 
 | ||||
|   <ImageView | ||||
|     android:id="@+id/imageGrey" | ||||
|     android:layout_width="wrap_content" | ||||
|     android:layout_height="wrap_content" | ||||
|     android:layout_marginStart="0dp" | ||||
|     app:layout_constraintBottom_toBottomOf="parent" | ||||
|     app:layout_constraintStart_toStartOf="parent" | ||||
|     app:srcCompat="@drawable/ic_custom_map_marker_grey" /> | ||||
| 
 | ||||
|   <TextView | ||||
|     android:id="@+id/textGrey" | ||||
|     android:layout_width="wrap_content" | ||||
|     android:layout_height="wrap_content" | ||||
|     android:text="@string/grey_pin" | ||||
|     android:textColor="#454547" | ||||
|     android:textSize="12sp" | ||||
|     app:layout_constraintBottom_toBottomOf="@+id/imageGrey" | ||||
|     app:layout_constraintStart_toEndOf="@+id/imageGrey" | ||||
|     app:layout_constraintTop_toTopOf="@+id/imageGrey" /> | ||||
| 
 | ||||
| </androidx.constraintlayout.widget.ConstraintLayout> | ||||
|  | @ -17,6 +17,7 @@ | |||
|       android:layout_weight="1" | ||||
|       android:background="@drawable/button_background_selector" | ||||
|       android:clickable="true" | ||||
|       android:focusable="true" | ||||
|       android:orientation="vertical" | ||||
|       android:padding="@dimen/standard_gap"> | ||||
| 
 | ||||
|  | @ -24,8 +25,8 @@ | |||
|           android:layout_width="wrap_content" | ||||
|           android:layout_height="wrap_content" | ||||
|           android:layout_gravity="center_horizontal" | ||||
|           android:tint="?attr/bookmarkButtonColor" | ||||
|           app:srcCompat="@drawable/ic_photo_camera_white_24dp" /> | ||||
|           app:srcCompat="@drawable/ic_photo_camera_white_24dp" | ||||
|           app:tint="?attr/bookmarkButtonColor" /> | ||||
| 
 | ||||
|         <TextView | ||||
|           android:id="@+id/cameraButtonText" | ||||
|  | @ -45,6 +46,7 @@ | |||
|       android:background="@drawable/button_background_selector" | ||||
|       android:clickable="true" | ||||
|       android:contentDescription="@string/nearby_row_image" | ||||
|       android:focusable="true" | ||||
|       android:orientation="vertical" | ||||
|       android:padding="@dimen/standard_gap"> | ||||
| 
 | ||||
|  | @ -53,8 +55,8 @@ | |||
|           android:layout_height="wrap_content" | ||||
|           android:layout_gravity="center_horizontal" | ||||
|           android:duplicateParentState="true" | ||||
|           android:tint="?attr/bookmarkButtonColor" | ||||
|           app:srcCompat="@drawable/ic_photo_white_24dp" /> | ||||
|           app:srcCompat="@drawable/ic_photo_white_24dp" | ||||
|           app:tint="?attr/bookmarkButtonColor" /> | ||||
| 
 | ||||
|         <TextView | ||||
|           android:id="@+id/galleryButtonText" | ||||
|  | @ -72,6 +74,7 @@ | |||
|         android:layout_width="@dimen/dimen_0" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_weight="1" | ||||
|         android:focusable="true" | ||||
|         android:padding="@dimen/standard_gap" | ||||
|         android:clickable="true" | ||||
|         android:orientation="vertical" | ||||
|  | @ -82,8 +85,8 @@ | |||
|             android:layout_height="wrap_content" | ||||
|             android:layout_gravity="center_horizontal" | ||||
|             app:srcCompat="@drawable/ic_directions_black_24dp" | ||||
|             android:tint="?attr/bookmarkButtonColor" | ||||
|             android:duplicateParentState="true"/> | ||||
|             android:duplicateParentState="true" | ||||
|             app:tint="?attr/bookmarkButtonColor" /> | ||||
|         <TextView | ||||
|             android:layout_width="wrap_content" | ||||
|             android:layout_height="wrap_content" | ||||
|  | @ -102,6 +105,7 @@ | |||
|       android:layout_height="wrap_content" | ||||
|       android:layout_weight="1" | ||||
|       android:clickable="true" | ||||
|       android:focusable="true" | ||||
|       android:orientation="vertical" | ||||
|       android:padding="@dimen/standard_gap"> | ||||
| 
 | ||||
|  | @ -110,8 +114,8 @@ | |||
|           android:layout_height="wrap_content" | ||||
|           android:layout_gravity="center_horizontal" | ||||
|           android:duplicateParentState="true" | ||||
|           android:tint="?attr/bookmarkButtonColor" | ||||
|           app:srcCompat="@drawable/ic_overflow" /> | ||||
|           app:srcCompat="@drawable/ic_overflow" | ||||
|           app:tint="?attr/bookmarkButtonColor" /> | ||||
| 
 | ||||
|         <TextView | ||||
|           android:id="@+id/iconOverflowText" | ||||
|  |  | |||
|  | @ -42,10 +42,10 @@ | |||
|     android:layout_height="wrap_content" | ||||
|     android:layout_marginStart="16dp" | ||||
|     android:contentDescription="@string/exit_location_picker" | ||||
|     android:tint="@color/white" | ||||
|     app:layout_constraintBottom_toBottomOf="parent" | ||||
|     app:layout_constraintLeft_toLeftOf="parent" | ||||
|     app:layout_constraintTop_toTopOf="parent" | ||||
|     app:srcCompat="@drawable/ic_arrow_back_white"/> | ||||
|     app:srcCompat="@drawable/ic_arrow_back_white" | ||||
|     app:tint="@color/white" /> | ||||
| 
 | ||||
| </androidx.constraintlayout.widget.ConstraintLayout> | ||||
							
								
								
									
										9
									
								
								app/src/main/res/menu/menu_custom_selector.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								app/src/main/res/menu/menu_custom_selector.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,9 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <menu xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|       xmlns:app="http://schemas.android.com/apk/res-auto"> | ||||
|   <item | ||||
|     android:id="@+id/action_delete_folder" | ||||
|     android:title="@string/custom_selector_delete_folder" | ||||
|     android:icon="@drawable/ic_delete_grey_700_24dp" | ||||
|     app:showAsAction="never" /> | ||||
| </menu> | ||||
|  | @ -1,17 +1,25 @@ | |||
| <menu xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|   xmlns:app="http://schemas.android.com/apk/res-auto"> | ||||
| 
 | ||||
|   <item android:id="@+id/item_refresh" | ||||
|     android:title="Refresh" | ||||
|     app:showAsAction="ifRoom" | ||||
|     android:icon="@drawable/ic_refresh_24dp_nearby" /> | ||||
| 
 | ||||
|   <item android:id="@+id/list_sheet" | ||||
|     android:title="@string/list_sheet" | ||||
|     app:showAsAction="ifRoom|withText" | ||||
|     android:icon="@drawable/ic_list_white_24dp" | ||||
|     /> | ||||
| 
 | ||||
|   <item android:id="@+id/list_item_gpx" | ||||
|     android:layout_width="wrap_content" | ||||
|     android:layout_height="wrap_content" | ||||
|     android:title="Save as GPX file" /> | ||||
| 
 | ||||
|   <item android:id="@+id/list_item_kml" | ||||
|     android:layout_width="wrap_content" | ||||
|     android:layout_height="wrap_content" | ||||
|     android:title="Save as KML file" /> | ||||
| 
 | ||||
| </menu> | ||||
|  |  | |||
|  | @ -171,6 +171,7 @@ | |||
|   <string name="categories_search_text_hint">تصنيفات البحث</string> | ||||
|   <string name="depicts_search_text_hint">(.البحث عن العناصر التي تصورها وسائطك (جبل ،تاج محل، إلخ</string> | ||||
|   <string name="menu_save_categories">حفظ</string> | ||||
|   <string name="menu_overflow_desc">القائمة الزائدة</string> | ||||
|   <string name="refresh_button">أنعش</string> | ||||
|   <string name="display_list_button">القائمة</string> | ||||
|   <string name="contributions_subtitle_zero">(لا يوجد تحميلات حتى الآن)</string> | ||||
|  | @ -527,6 +528,7 @@ | |||
|   <string name="no_notification">ليس لديك أي إشعارات غير مقروءة</string> | ||||
|   <string name="no_read_notification">ليس لديك أي إشعاراتٍ غير مقروءة</string> | ||||
|   <string name="share_logs_using">مشاركة السجلات باستخدام</string> | ||||
|   <string name="check_your_email_inbox">تحقق من صندوق بريدك الإلكتروني</string> | ||||
|   <string name="menu_option_read">عرض المقروءة</string> | ||||
|   <string name="menu_option_unread">عرض غير المقروءة</string> | ||||
|   <string name="error_occurred_in_picking_images">حدث خطأ أثناء التقاط الصور</string> | ||||
|  | @ -690,6 +692,7 @@ | |||
|   <string name="leaderboard_nearby">مجاور</string> | ||||
|   <string name="leaderboard_used">مستخدَم</string> | ||||
|   <string name="leaderboard_my_rank_button_text">ترتيبي</string> | ||||
|   <string name="map_attribution">&#169; <a href=\"https://www.openstreetmap.org/copyright\">خريطة الشارع المفتوحة</a></string> | ||||
|   <string name="limited_connection_enabled">وضع الاتصال المحدود مُمَكَّن!</string> | ||||
|   <string name="limited_connection_disabled">وضع الاتصال المحدود مُعطل. سيجري استئناف التحميلات المعلقة الآن.</string> | ||||
|   <string name="limited_connection_mode">وضع الاتصال المحدود</string> | ||||
|  | @ -739,6 +742,7 @@ | |||
|   <string name="custom_selector_dismiss_limit_warning_button_text">رفض</string> | ||||
|   <string name="custom_selector_button_limit_text">الحد الأقصى: %1$d</string> | ||||
|   <string name="custom_selector_limit_error_desc">خطأ: تجاوز حد التحميل</string> | ||||
|   <string name="place_state_wlm">دبليو إل إم</string> | ||||
|   <string name="wlm_upload_info">سيتم إدخال هذه الصورة في مسابقة Wiki Loves Monuments</string> | ||||
|   <string name="display_monuments">عرض الآثار</string> | ||||
|   <string name="wlm_month_message">إنه شهر Wiki Loves Monuments!</string> | ||||
|  | @ -788,19 +792,75 @@ | |||
|   <string name="image_selected">تم تحديد الصورة</string> | ||||
|   <string name="image_marked_as_not_for_upload">تم وضع علامة على الصورة على أنها ليست للتحميل</string> | ||||
|   <string name="menu_view_report">تقرير</string> | ||||
|   <string name="menu_view_set_white_background">تعيين الخلفية البيضاء</string> | ||||
|   <string name="menu_view_set_black_background">تعيين خلفية سوداء</string> | ||||
|   <string name="report_violation">تبليغ عن عنف</string> | ||||
|   <string name="report_user">أخطر عن هذا المستخدم</string> | ||||
|   <string name="report_content">الإبلاغ عن هذا المحتوى</string> | ||||
|   <string name="request_user_block">طلب منع هذا المستخدم</string> | ||||
|   <string name="welcome_to_full_screen_mode_text">مرحبًا بك في وضع التحديد بملء الشاشة</string> | ||||
|   <string name="full_screen_mode_zoom_info">استخدم إصبعين للتكبير والتصغير.</string> | ||||
|   <string name="full_screen_mode_features_info" fuzzy="true">مرر سريعًا وطويلًا لتنفيذ هذه الإجراءات:! N! - يسار / يمين: انتقل إلى السابق / التالي! N! - لأعلى: حدد! N! - أسفل: وضع علامة على أنه ليس للتحميل.</string> | ||||
|   <string name="full_screen_mode_features_info">مرر سريعًا وطويلًا لأداء هذه الإجراءات: \n- يسار/يمين: الانتقال إلى السابق/التالي \n- أعلى: تحديد\n- أسفل: وضع علامة على عدم التحميل.</string> | ||||
|   <string name="set_up_avatar_toast_string">لإعداد صورتك الرمزية في قائمة المتصدرين، اضغط على \"تعيين كصورة رمزية\" في قائمة النقاط الثلاث لأي صورة.</string> | ||||
|   <string name="similar_coordinate_description_auto_set">الإحداثيات ليست إحداثيات دقيقة، لكن الشخص الذي قام بتحميل هذه الصورة يعتقد أنها قريبة بما فيه الكفاية.</string> | ||||
|   <string name="storage_permissions_denied">رُفض إذن التخزين</string> | ||||
|   <string name="unable_to_share_upload_item">تعذر مشاركة هذا العنصر</string> | ||||
|   <string name="permissions_are_required_for_functionality">الإذن مطلوب لهذه الوظيفة</string> | ||||
|   <string name="learn_how_to_write_a_useful_description">تعلم كيفية كتابة وصف مفيد</string> | ||||
|   <string name="learn_how_to_write_a_useful_caption">تعلم كيفية كتابة تعليق مفيد</string> | ||||
|   <string name="see_your_achievements">شاهد إنجازاتك</string> | ||||
|   <string name="edit_image">تعديل الصورة</string> | ||||
|   <string name="edit_location">تعديل الموقع</string> | ||||
|   <string name="location_updated">تم تحديث الموقع!</string> | ||||
|   <string name="remove_location">إزالة الموقع</string> | ||||
|   <string name="remove_location_warning_title">إزالة تحذير الموقع</string> | ||||
|   <string name="remove_location_warning_desc">يجعل تحديد الموقع الصور أكثر فائدة وسهولة في العثور عليها. هل ترغب حقًا في إزالة تحديد الموقع من هذه الصورة؟</string> | ||||
|   <string name="location_removed">تمت إزالة الموقع!</string> | ||||
|   <string name="send_thanks_to_author">اشكر المؤلف</string> | ||||
|   <string name="error_sending_thanks">حدث خطأ أثناء إرسال الشكر للمؤلف.</string> | ||||
|   <string name="invalid_login_message">لقد انتهت صلاحية تسجيل الدخول الخاص بك. يرجى تسجيل الدخول مرة أخرى.</string> | ||||
|   <string name="no_application_available_to_open_gpx_files">لا يوجد تطبيق متاح لفتح ملفات GPX</string> | ||||
|   <string name="file_saved_successfully">تم حفظ الملف بنجاح</string> | ||||
|   <string name="do_you_want_to_open_gpx_file">هل تريد فتح ملف GPX؟</string> | ||||
|   <string name="do_you_want_to_open_kml_file">هل تريد فتح ملف KML؟</string> | ||||
|   <string name="failed_to_save_kml_file">فشل في حفظ ملف KML.</string> | ||||
|   <string name="failed_to_save_gpx_file">فشل في حفظ ملف GPX.</string> | ||||
|   <string name="saving_kml_file">حفظ ملف KML</string> | ||||
|   <string name="saving_gpx_file">حفظ ملف GPX</string> | ||||
|   <plurals name="custom_picker_images_selected_title_appendix"> | ||||
|     <item quantity="zero">لا صور تم اختيارها</item> | ||||
|     <item quantity="one">%d صورة تم اختيارها</item> | ||||
|     <item quantity="two">صورتان تم اختيارهما</item> | ||||
|     <item quantity="few">صور قليلة تم اختيارها</item> | ||||
|     <item quantity="many">صور كثيرة تم اختيارها</item> | ||||
|     <item quantity="other">%d صور تم اختيارها</item> | ||||
|   </plurals> | ||||
|   <string name="multiple_files_depiction">يرجى تذكر أن جميع الصور في التحميل المتعدد تحصل على نفس الفئات والأوصاف. إذا لم تتشارك الصور في الأوصاف والفئات، فيرجى إجراء عدة عمليات تحميل منفصلة.</string> | ||||
|   <string name="multiple_files_depiction_header">ملاحظة حول التحميلات المتعددة</string> | ||||
|   <string name="nearby_wikitalk">الإبلاغ عن مشكلة حول هذا العنصر إلى Wikidata</string> | ||||
|   <string name="please_enter_some_comments">الرجاء إدخال بعض التعليقات</string> | ||||
|   <string name="talk">نقاش</string> | ||||
|   <string name="write_something_about_the_item">اكتب شيئًا عن العنصر \'%1$s\'. سيكون مرئيًا للعامة.</string> | ||||
|   <string name="does_not_exist_anymore_no_picture_can_ever_be_taken_of_it">\'%1$s\' لم يعد موجودًا، ولا يمكن التقاط صورة له أبدًا.</string> | ||||
|   <string name="is_at_a_different_place_please_specify_the_correct_place_below_if_possible_tell_us_the_correct_latitude_longitude">\'%1$s\' موجود في مكان مختلف. يرجى تحديد المكان الصحيح أدناه، وإذا أمكن، اكتب خط العرض وخط الطول الصحيحين.</string> | ||||
|   <string name="other_problem_or_information_please_explain_below">مشكلة أو معلومات أخرى (يرجى التوضيح أدناه).</string> | ||||
|   <string name="feedback_destination_note">سيتم نشر تعليقاتك على صفحة الويكي التالية:  <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a></string> | ||||
|   <string name="are_you_sure_that_you_want_cancel_all_the_uploads">هل أنت متأكد أنك تريد إلغاء كافة التحميلات؟</string> | ||||
|   <string name="cancelling_all_the_uploads">إلغاء كافة التحميلات...</string> | ||||
|   <string name="uploads">المرفوعات</string> | ||||
|   <string name="pending">قيد الانتظار</string> | ||||
|   <string name="failed">فشل</string> | ||||
|   <string name="could_not_load_place_data">تعذر تحميل بيانات المكان</string> | ||||
|   <string name="custom_selector_delete_folder">حذف المجلد</string> | ||||
|   <string name="custom_selector_confirm_deletion_title">تأكيد الحذف</string> | ||||
|   <string name="custom_selector_confirm_deletion_message">هل أنت متأكد أنك تريد حذف المجلد %1$s الذي يحتوي على %2$d عنصرا؟</string> | ||||
|   <string name="custom_selector_delete">حذف</string> | ||||
|   <string name="custom_selector_cancel">إلغاء</string> | ||||
|   <string name="custom_selector_folder_deleted_success">تم حذف المجلد %1$s بنجاح</string> | ||||
|   <string name="custom_selector_folder_deleted_failure">فشل حذف المجلد %1$s</string> | ||||
|   <string name="custom_selector_error_trashing_folder_contents">خطأ في نقل محتويات المجلد إلى سلة المهملات: %1$s</string> | ||||
|   <string name="custom_selector_folder_not_found_error">فشل استرداد مسار المجلد لمعرف الدلو: %1$d</string> | ||||
|   <string name="red_pin">هذا المكان ليس له صورة بعد، اذهب والتقط واحدة!</string> | ||||
|   <string name="green_pin">هذا المكان لديه صورة بالفعل.</string> | ||||
|   <string name="grey_pin">الآن التحقق ما إذا كان هذا المكان لديه صورة.</string> | ||||
| </resources> | ||||
|  |  | |||
|  | @ -2,10 +2,11 @@ | |||
| <!-- Authors: | ||||
| * Dağlı95 | ||||
| * Khan27 | ||||
| * Nemoralis | ||||
| --> | ||||
| <resources> | ||||
|   <string name="crash_dialog_title">Nasazlıq</string> | ||||
|   <string name="crash_dialog_text">Uups. Nəsə düzgün çalışmır!</string> | ||||
|   <string name="crash_dialog_comment_prompt">Nə etdiyinizi dəqiqləşdirib, bizə bildirin və sonra e-poçtla bizə göndərin. Bu problemi həll etməyə bizə kömək edin.</string> | ||||
|   <string name="crash_dialog_ok_toast">Təşəkkür!</string> | ||||
|   <string name="crash_dialog_comment_prompt">Nə etdiyinizi bizə deyin, sonra e-poçt vasitəsilə bizimlə paylaşın. Bu, bizə bunu düzəltməyə kömək edəcək!</string> | ||||
|   <string name="crash_dialog_ok_toast">Təşəkkürlər!</string> | ||||
| </resources> | ||||
|  |  | |||
|  | @ -12,34 +12,60 @@ | |||
| * Şeyx Şamil | ||||
| --> | ||||
| <resources> | ||||
|   <string name="commons_facebook">Commons Facebook səhifəsi</string> | ||||
|   <string name="commons_github">Commons Github Mənbə Kodu</string> | ||||
|   <string name="commons_logo">Commons Loqotipi</string> | ||||
|   <string name="commons_website">Commons Veb-saytı</string> | ||||
|   <string name="exit_location_picker">Məkan seçicidən çıxın</string> | ||||
|   <string name="submit">Göndər</string> | ||||
|   <string name="add_another_description">Başqa təsvir əlavə et</string> | ||||
|   <string name="add_new_contribution">Yeni töhfə</string> | ||||
|   <string name="add_contribution_from_camera">Kamera ilə töhfə ver</string> | ||||
|   <string name="add_contribution_from_photos">Fotolar ilə töhfə ver</string> | ||||
|   <string name="add_contribution_from_contributions_gallery">Əvvəlki töhfələr qalereyasından töhfə əlavə et</string> | ||||
|   <string name="show_captions">Başlıqlar</string> | ||||
|   <string name="row_item_language_description">Dil təsviri</string> | ||||
|   <string name="row_item_caption">Başlıq</string> | ||||
|   <string name="show_captions_description">Təsvir</string> | ||||
|   <string name="nearby_row_image">Şəkil</string> | ||||
|   <string name="nearby_all">Hamısı</string> | ||||
|   <string name="nearby_filter_toggle">Aç/Bağla</string> | ||||
|   <string name="nearby_filter_search">Axtarış Görünüşü</string> | ||||
|   <string name="nearby_filter_state">Məkanın Vəziyyəti</string> | ||||
|   <string name="appwidget_img">Günün Şəkli</string> | ||||
|   <plurals name="uploads_pending_notification_indicator"> | ||||
|     <item quantity="one">%1$d fayl yüklənir</item> | ||||
|     <item quantity="other">%1$d fayllar yüklənir</item> | ||||
|   </plurals> | ||||
|   <plurals name="contributions_subtitle"> | ||||
|     <item quantity="one">(%1$d)</item> | ||||
|     <item quantity="other">(%1$d)</item> | ||||
|   </plurals> | ||||
|   <string name="starting_uploads">Yükləmələrə Başlanılır</string> | ||||
|   <string name="preference_category_general">Ümumi</string> | ||||
|   <string name="preference_category_privacy">Məxfilik</string> | ||||
|   <string name="app_name">Vikimedia Commons</string> | ||||
|   <string name="app_name">Vikianbar</string> | ||||
|   <string name="menu_settings">Tənzimləmələr</string> | ||||
|   <string name="username">Ləqəb</string> | ||||
|   <string name="username">İstifadəçi adı</string> | ||||
|   <string name="password">Parol</string> | ||||
|   <string name="login">Daxil ol</string> | ||||
|   <string name="signup">Qeydiyyatdan keç</string> | ||||
|   <string name="logging_in_title">Giriş edilir</string> | ||||
|   <string name="logging_in_message">Lütfən gözləyin…</string> | ||||
|   <string name="logging_in_message">Zəhmət olmasa, gözləyin…</string> | ||||
|   <string name="login_success" fuzzy="true">Daxil oldunuz!</string> | ||||
|   <string name="login_failed">Giriş baş tutmadı!</string> | ||||
|   <string name="upload_failed">Fayl tapılmadı. Xahiş edirik başqa bir fayl üzərində cəhd edin.</string> | ||||
|   <string name="authentication_failed" fuzzy="true">Doğrulama alınmadı, xahiş edirəm yenidən daxil olun</string> | ||||
|   <string name="uploading_started">Yükləmə başladı!</string> | ||||
|   <string name="upload_completed_notification_title">%1$s yükləndi!</string> | ||||
|   <string name="upload_completed_notification_text">Yüklədiyini izlə</string> | ||||
|   <string name="upload_completed_notification_text">Yükləmənizə baxmaq üçün toxunun</string> | ||||
|   <string name="upload_progress_notification_title_start" fuzzy="true">%1$s yüklənməsi başlanır</string> | ||||
|   <string name="upload_progress_notification_title_in_progress">%1$s yüklənir</string> | ||||
|   <string name="upload_progress_notification_title_finishing">%1$s yüklənməsi başa çatdı</string> | ||||
|   <string name="upload_failed_notification_title" fuzzy="true">%1$s faylının yüklənməsi alınmadı</string> | ||||
|   <string name="upload_failed_notification_subtitle">Baxmaq üçün toxunun</string> | ||||
|   <string name="title_activity_contributions">Yükləmələrim</string> | ||||
|   <string name="contribution_state_queued">Sırada</string> | ||||
|   <string name="upload_failed_notification_subtitle">Baxmaq üçün toxun</string> | ||||
|   <string name="title_activity_contributions">Son Yükləmələrim</string> | ||||
|   <string name="contribution_state_queued">Növbəyə alındı</string> | ||||
|   <string name="contribution_state_failed">Uğursuz</string> | ||||
|   <string name="contribution_state_in_progress">%1$d%% tamamlandı</string> | ||||
|   <string name="contribution_state_starting">Yüklənir</string> | ||||
|  | @ -52,15 +78,15 @@ | |||
|   <string name="share_description_hint">Açıqlama</string> | ||||
|   <string name="login_failed_network" fuzzy="true">Daxil olmaq olmur — şəbəkə xətası</string> | ||||
|   <string name="login_failed_throttled">Çox sayda uğursuz daxil olma. Xahiş edirik bir neçə dəqiqə sonra yenidən cəhd edin.</string> | ||||
|   <string name="login_failed_blocked">Bağışlayın, bu istifadəçi Commons-da bloklanmışdır.</string> | ||||
|   <string name="login_failed_2fa_needed">İki faktorlu giriş doğrulama kodunu verməlisiniz.</string> | ||||
|   <string name="login_failed_blocked">Bağışlayın, bu istifadəçi Vikianbardan bloklanıb</string> | ||||
|   <string name="login_failed_2fa_needed">Siz iki faktorlu autentifikasiya kodunuzu təqdim etməlisiniz.</string> | ||||
|   <string name="login_failed_generic" fuzzy="true">Daxil olma uğursuz oldu</string> | ||||
|   <string name="share_upload_button">Yüklə</string> | ||||
|   <string name="multiple_share_base_title">Bu dəsti adlandırın</string> | ||||
|   <string name="provider_modifications">Bildirişlər</string> | ||||
|   <string name="provider_modifications">Dəyişikliklər</string> | ||||
|   <string name="menu_upload_single">Yüklə</string> | ||||
|   <string name="categories_search_text_hint">Kateqoriyaları axtar</string> | ||||
|   <string name="menu_save_categories">Qeyd et</string> | ||||
|   <string name="categories_search_text_hint">Kateqoriyalarda axtar</string> | ||||
|   <string name="menu_save_categories">Yadda saxla</string> | ||||
|   <string name="refresh_button">Yenilə</string> | ||||
|   <string name="display_list_button">Siyahı</string> | ||||
|   <string name="contributions_subtitle_zero">(Hələ yükləmə yoxdur)</string> | ||||
|  |  | |||
|  | @ -307,4 +307,5 @@ | |||
|   <string name="please_wait">Моля, изчакайте...</string> | ||||
|   <string name="delete_helper_ask_spam_blurry">напълно размазано</string> | ||||
|   <string name="leaderboard_nearby">Наблизо</string> | ||||
|   <string name="read_help_link">Прочетете повече</string> | ||||
| </resources> | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ | |||
| * Chmee2 | ||||
| * Clon | ||||
| * Dvorapa | ||||
| * Fjuro | ||||
| * Frettie | ||||
| * Georg101 | ||||
| * Ilimanaq29 | ||||
|  | @ -28,31 +29,58 @@ | |||
|   <string name="commons_github">Zdrojový kód Commons na GitHubu</string> | ||||
|   <string name="commons_logo">Logo Wikimedia Commons</string> | ||||
|   <string name="commons_website">Stránka Commons</string> | ||||
|   <string name="exit_location_picker">Ukončit výběr polohy</string> | ||||
|   <string name="submit">Odeslat</string> | ||||
|   <string name="add_another_description">Přidat další popis</string> | ||||
|   <string name="add_new_contribution">Přidat nový příspěvek</string> | ||||
|   <string name="add_contribution_from_camera">Přidat příspěvek z fotoaparátu</string> | ||||
|   <string name="add_contribution_from_photos">Přidat příspěvek z fotek</string> | ||||
|   <string name="add_contribution_from_contributions_gallery">Přidat příspěvek z galerie předchozích příspěvků</string> | ||||
|   <string name="show_captions">Titulky</string> | ||||
|   <string name="row_item_language_description">Popis jazyka</string> | ||||
|   <string name="row_item_caption">Titulek</string> | ||||
|   <string name="show_captions_description">Popis</string> | ||||
|   <string name="nearby_row_image">Obrázek</string> | ||||
|   <string name="nearby_all">Vše</string> | ||||
|   <string name="nearby_filter_toggle">Přepnout nahoru</string> | ||||
|   <string name="nearby_filter_search">Zobrazení vyhledávání</string> | ||||
|   <string name="nearby_filter_state">Stát místa</string> | ||||
|   <string name="appwidget_img">Obrázek dne</string> | ||||
|   <plurals name="uploads_pending_notification_indicator" fuzzy="true"> | ||||
|     <item quantity="one">%1$d soubor se nahrává</item> | ||||
|     <item quantity="other">%1$d souborů se nahrává</item> | ||||
|   <plurals name="uploads_pending_notification_indicator"> | ||||
|     <item quantity="one">Nahrávání %1$d souboru</item> | ||||
|     <item quantity="few">Nahrávání %1$d souborů</item> | ||||
|     <item quantity="many">Nahrávání %1$d souborů</item> | ||||
|     <item quantity="other">Nahrávání %1$d souborů</item> | ||||
|   </plurals> | ||||
|   <plurals name="contributions_subtitle" fuzzy="true"> | ||||
|     <item quantity="zero">\@string/contributions_subtitle_zero</item> | ||||
|   <plurals name="contributions_subtitle"> | ||||
|     <item quantity="one">(%1$d)</item> | ||||
|     <item quantity="few">(%1$d)</item> | ||||
|     <item quantity="many">(%1$d)</item> | ||||
|     <item quantity="other">(%1$d)</item> | ||||
|   </plurals> | ||||
|   <plurals name="starting_multiple_uploads" fuzzy="true"> | ||||
|     <item quantity="one">Spouští se nahrávání %1$d souboru</item> | ||||
|     <item quantity="other">Spouští se nahrávání %1$d souborů</item> | ||||
|   <string name="starting_uploads">Spouštění nahrávání</string> | ||||
|   <plurals name="starting_multiple_uploads"> | ||||
|     <item quantity="one">Zpracovávání %d nahrání</item> | ||||
|     <item quantity="few">Zpracovávání %d nahrání</item> | ||||
|     <item quantity="many">Zpracovávání %d nahrání</item> | ||||
|     <item quantity="other">Zpracovávání %d nahrání</item> | ||||
|   </plurals> | ||||
|   <plurals name="multiple_uploads_title" fuzzy="true"> | ||||
|     <item quantity="one">%1$d nahrávání</item> | ||||
|     <item quantity="other">%1$d nahrávání</item> | ||||
|   <plurals name="multiple_uploads_title"> | ||||
|     <item quantity="one">%d nahrávání</item> | ||||
|     <item quantity="few">%d nahrávání</item> | ||||
|     <item quantity="many">%d nahrávání</item> | ||||
|     <item quantity="other">%d nahrávání</item> | ||||
|   </plurals> | ||||
|   <plurals name="share_license_summary" fuzzy="true"> | ||||
|   <plurals name="share_license_summary"> | ||||
|     <item quantity="one">Tento obrázek bude zveřejněn pod licencí %1$s</item> | ||||
|     <item quantity="few">Tyto obrázky budou zveřejněny pod licencí %1$s</item> | ||||
|     <item quantity="many">Tyto obrázky budou zveřejněny pod licencí %1$s</item> | ||||
|     <item quantity="other">Tyto obrázky budou zveřejněny pod licencí %1$s</item> | ||||
|   </plurals> | ||||
|   <plurals name="upload_count_title" fuzzy="true"> | ||||
|   <plurals name="upload_count_title"> | ||||
|     <item quantity="one">%1$d nahrání</item> | ||||
|     <item quantity="few">%1$d nahrání</item> | ||||
|     <item quantity="many">%1$d nahrání</item> | ||||
|     <item quantity="other">%1$d nahrání</item> | ||||
|   </plurals> | ||||
|   <plurals name="receiving_shared_content" fuzzy="true"> | ||||
|  | @ -67,6 +95,7 @@ | |||
|   <string name="app_name">Commons</string> | ||||
|   <string name="menu_settings">Nastavení</string> | ||||
|   <string name="intent_share_upload_label">Nahrát na Commons</string> | ||||
|   <string name="upload_in_progress">Probíhá nahrávání</string> | ||||
|   <string name="username">Uživatelské jméno</string> | ||||
|   <string name="password">Heslo</string> | ||||
|   <string name="login_credential">Přihlásit se do svého Commons beta účtu</string> | ||||
|  | @ -75,7 +104,9 @@ | |||
|   <string name="signup">Zaregistrovat se</string> | ||||
|   <string name="logging_in_title">Přihlášení</string> | ||||
|   <string name="logging_in_message">Čekejte prosím…</string> | ||||
|   <string name="login_success" fuzzy="true">Přihlášení uspělo!</string> | ||||
|   <string name="updating_caption_title">Nahrávání titulků a popisů</string> | ||||
|   <string name="updating_caption_message">Čekejte prosím…</string> | ||||
|   <string name="login_success">Úspěšně přihlášeni!</string> | ||||
|   <string name="login_failed" fuzzy="true">Přihlášení se nezdařilo!</string> | ||||
|   <string name="upload_failed">Soubor nebyl nalezen. Prosím, zkuste jiný soubor.</string> | ||||
|   <string name="authentication_failed" fuzzy="true">Ověření se nezdařilo, prosím přihlaste se znovu</string> | ||||
|  | @ -488,7 +519,10 @@ | |||
|   <string name="you_must_reset_your_passsword" fuzzy="true">Při přihlášení nastala chyba, musíte si resetovat vaše heslo!</string> | ||||
|   <string name="upload_nearby_place_found_title">Místo v okolí nalezeno</string> | ||||
|   <string name="upload_nearby_place_found_description_singular" fuzzy="true">Je toto fotka místa %1$s?</string> | ||||
|   <string name="title_app_shortcut_bookmark">Záložky</string> | ||||
|   <string name="title_app_shortcut_setting">Nastavení</string> | ||||
|   <string name="remove_bookmark">Odebráno ze záložek</string> | ||||
|   <string name="add_bookmark">Přidáno do záložek</string> | ||||
|   <string name="wallpaper_set_unsuccessfully">Něco se pokazilo. Tapetu se nepodařilo nastavit</string> | ||||
|   <string name="setting_wallpaper_dialog_title">Nastavit jako tapetu</string> | ||||
|   <string name="setting_wallpaper_dialog_message">Nastavování tapety. Prosím, čekejte…</string> | ||||
|  |  | |||
|  | @ -125,6 +125,7 @@ | |||
|   <string name="categories_search_text_hint">Søg kategorier</string> | ||||
|   <string name="depicts_search_text_hint">Søg efter genstande, som dine medier afbilder (bjerg, Taj Mahal osv.)</string> | ||||
|   <string name="menu_save_categories">Gem</string> | ||||
|   <string name="menu_overflow_desc">Overløbsmenu</string> | ||||
|   <string name="refresh_button">Opdater</string> | ||||
|   <string name="display_list_button">Liste</string> | ||||
|   <string name="contributions_subtitle_zero">(Ingen uploads endnu)</string> | ||||
|  | @ -481,6 +482,7 @@ | |||
|   <string name="no_notification">Du har ingen ulæste notifikationer</string> | ||||
|   <string name="no_read_notification">Du har ingen læste notifikationer</string> | ||||
|   <string name="share_logs_using">Del logs ved hjælp af</string> | ||||
|   <string name="check_your_email_inbox">Tjek din e-mail-indbakke</string> | ||||
|   <string name="menu_option_read">Vis læste</string> | ||||
|   <string name="menu_option_unread">Vis ulæste</string> | ||||
|   <string name="error_occurred_in_picking_images">Der opstod en fejl under udvælgelse af billeder</string> | ||||
|  | @ -788,4 +790,16 @@ | |||
|   <string name="pending">Afventer</string> | ||||
|   <string name="failed">Mislykkedes</string> | ||||
|   <string name="could_not_load_place_data">Kunne ikke indlæse steddata</string> | ||||
|   <string name="custom_selector_delete_folder">Slet mappe</string> | ||||
|   <string name="custom_selector_confirm_deletion_title">Bekræft sletning</string> | ||||
|   <string name="custom_selector_confirm_deletion_message">Er du sikker på, at du vil slette mappen %1$s, der indeholder %2$d elementer?</string> | ||||
|   <string name="custom_selector_delete">Slet</string> | ||||
|   <string name="custom_selector_cancel">Annuller</string> | ||||
|   <string name="custom_selector_folder_deleted_success">Mappen %1$s blev slettet</string> | ||||
|   <string name="custom_selector_folder_deleted_failure">Kunne ikke slette mappen %1$s</string> | ||||
|   <string name="custom_selector_error_trashing_folder_contents">Fejl ved sletning af mappeindhold: %1$s</string> | ||||
|   <string name="custom_selector_folder_not_found_error">Kunne ikke hente mappestien til bucket-id: %1$d</string> | ||||
|   <string name="red_pin">Dette sted har endnu ikke noget billede, så gå hen og tag et!</string> | ||||
|   <string name="green_pin">Dette sted har allerede et billede.</string> | ||||
|   <string name="grey_pin">Tjekker nu, om dette sted har et billede.</string> | ||||
| </resources> | ||||
|  |  | |||
|  | @ -22,6 +22,7 @@ | |||
| * JenyxGym | ||||
| * KATRINE1992 | ||||
| * Koreller | ||||
| * Mahabarata | ||||
| * McDutchie | ||||
| * Melissadeba95 | ||||
| * Metroitendo | ||||
|  | @ -160,6 +161,7 @@ | |||
|   <string name="categories_search_text_hint">Rechercher des catégories</string> | ||||
|   <string name="depicts_search_text_hint">Rechercher les éléments que votre média représente (montagne, Taj Mahal, etc.).</string> | ||||
|   <string name="menu_save_categories">Enregistrer</string> | ||||
|   <string name="menu_overflow_desc">Menu de débordement</string> | ||||
|   <string name="refresh_button">Actualiser</string> | ||||
|   <string name="display_list_button">Lister</string> | ||||
|   <string name="contributions_subtitle_zero">(Pas encore de téléversement)</string> | ||||
|  | @ -516,6 +518,7 @@ | |||
|   <string name="no_notification">Vous n’avez aucune notification non lue</string> | ||||
|   <string name="no_read_notification">Vous n’avez aucune notification lue</string> | ||||
|   <string name="share_logs_using">Partager les journaux en utilisant</string> | ||||
|   <string name="check_your_email_inbox">Vérifiez votre boîte de réception</string> | ||||
|   <string name="menu_option_read">Afficher les lus</string> | ||||
|   <string name="menu_option_unread">Afficher les non lus</string> | ||||
|   <string name="error_occurred_in_picking_images">Une erreur est survenue lors de la sélection des images</string> | ||||
|  | @ -825,4 +828,16 @@ | |||
|   <string name="pending">En attente</string> | ||||
|   <string name="failed">Échec</string> | ||||
|   <string name="could_not_load_place_data">Les données du lieu n\'ont pas pu être chargées</string> | ||||
|   <string name="custom_selector_delete_folder">Supprimer le dossier</string> | ||||
|   <string name="custom_selector_confirm_deletion_title">Confirmer la suppression</string> | ||||
|   <string name="custom_selector_confirm_deletion_message">Êtes-vous sûr de vouloir supprimer le dossier %1$s contenant %2$d éléments ?</string> | ||||
|   <string name="custom_selector_delete">Supprimer</string> | ||||
|   <string name="custom_selector_cancel">Annuler</string> | ||||
|   <string name="custom_selector_folder_deleted_success">Le dossier %1$s a été supprimé avec succès</string> | ||||
|   <string name="custom_selector_folder_deleted_failure">Impossible de supprimer le dossier %1$s</string> | ||||
|   <string name="custom_selector_error_trashing_folder_contents">Erreur lors de la suppression du contenu du dossier : %1$s</string> | ||||
|   <string name="custom_selector_folder_not_found_error">Échec de la récupération du chemin d\'accès au dossier pour le bucket ID : %1$d</string> | ||||
|   <string name="red_pin">Cet endroit n\'a pas encore de photo, allez en prendre une !</string> | ||||
|   <string name="green_pin">Cet endroit a déjà une photo.</string> | ||||
|   <string name="grey_pin">Je vérifie maintenant si cet endroit a une photo.</string> | ||||
| </resources> | ||||
|  |  | |||
|  | @ -354,4 +354,7 @@ | |||
|   <string name="uploads">अपलोड</string> | ||||
|   <string name="pending">लंबित</string> | ||||
|   <string name="failed">विफल हुआ</string> | ||||
|   <string name="custom_selector_delete_folder">फ़ोल्डर हटाएँ</string> | ||||
|   <string name="custom_selector_delete">हटाएँ</string> | ||||
|   <string name="custom_selector_cancel">रद्द करें</string> | ||||
| </resources> | ||||
|  |  | |||
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
	
	 Nicolas Raoul
						Nicolas Raoul