diff --git a/app/build.gradle b/app/build.gradle index f4ce47239..a7b9cf4c6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -142,7 +142,7 @@ dependencies { implementation "androidx.multidex:multidex:$MULTIDEX_VERSION" - def work_version = "2.8.0" + def work_version = "2.8.1" // Kotlin + coroutines implementation "androidx.work:work-runtime-ktx:$work_version" implementation("androidx.work:work-runtime:$work_version") @@ -168,7 +168,7 @@ project.gradle.taskGraph.whenReady { } android { - compileSdkVersion 31 + compileSdkVersion 33 defaultConfig { //applicationId 'fr.free.nrw.commons' diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.kt b/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.kt index c3d084f0a..b75271f05 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.kt +++ b/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.kt @@ -43,7 +43,11 @@ data class Contribution constructor( var hasInvalidLocation : Int = 0, var contentUri: Uri? = null, var countryCode : String? = null, - var imageSHA1 : String? = null + var imageSHA1 : String? = null, + /** + * Number of times a contribution has been retried after a failure + */ + var retries: Int = 0 ) : Parcelable { fun completeWith(media: Media): Contribution { diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java index 03e3e5611..249884b4b 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java @@ -92,6 +92,7 @@ public class ContributionsFragment private static final String CONTRIBUTION_LIST_FRAGMENT_TAG = "ContributionListFragmentTag"; private MediaDetailPagerFragment mediaDetailPagerFragment; static final String MEDIA_DETAIL_PAGER_FRAGMENT_TAG = "MediaDetailFragmentTag"; + private static final int MAX_RETRIES = 10; @BindView(R.id.card_view_nearby) public NearbyNotificationCardView nearbyNotificationCardView; @BindView(R.id.campaigns_view) CampaignView campaignView; @@ -593,6 +594,15 @@ public class ContributionsFragment } } + /** + * Restarts the upload process for a contribution + * @param contribution + */ + public void restartUpload(Contribution contribution) { + contribution.setState(Contribution.STATE_QUEUED); + contributionsPresenter.saveContribution(contribution); + Timber.d("Restarting for %s", contribution.toString()); + } /** * Retry upload when it is failed * @@ -601,10 +611,23 @@ public class ContributionsFragment @Override public void retryUpload(Contribution contribution) { if (NetworkUtils.isInternetConnectionEstablished(getContext())) { - if (contribution.getState() == STATE_FAILED || contribution.getState() == STATE_PAUSED || contribution.getState()==Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE) { - contribution.setState(Contribution.STATE_QUEUED); - contributionsPresenter.saveContribution(contribution); - Timber.d("Restarting for %s", contribution.toString()); + if (contribution.getState() == STATE_PAUSED || contribution.getState()==Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE) { + restartUpload(contribution); + } else if (contribution.getState() == STATE_FAILED) { + int retries = contribution.getRetries(); + // TODO: Improve UX. Additional details: https://github.com/commons-app/apps-android-commons/pull/5257#discussion_r1304662562 + /* Limit the number of retries for a failed upload + to handle cases like invalid filename as such uploads + will never be successful */ + if(retries < MAX_RETRIES) { + contribution.setRetries(retries + 1); + Timber.d("Retried uploading %s %d times", contribution.getMedia().getFilename(), retries + 1); + restartUpload(contribution); + } else { + // TODO: Show the exact reason for failure + Toast.makeText(getContext(), + R.string.retry_limit_reached, Toast.LENGTH_SHORT).show(); + } } else { Timber.d("Skipping re-upload for non-failed %s", contribution.toString()); } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsPresenter.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsPresenter.java index 002e8bc95..f676f193a 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsPresenter.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsPresenter.java @@ -1,18 +1,12 @@ package fr.free.nrw.commons.contributions; import androidx.work.ExistingWorkPolicy; -import androidx.work.OneTimeWorkRequest; -import androidx.work.WorkManager; import fr.free.nrw.commons.MediaDataExtractor; import fr.free.nrw.commons.contributions.ContributionsContract.UserActionListener; import fr.free.nrw.commons.di.CommonsApplicationModule; -import fr.free.nrw.commons.upload.worker.UploadWorker; +import fr.free.nrw.commons.upload.worker.WorkRequestHelper; import io.reactivex.Scheduler; import io.reactivex.disposables.CompositeDisposable; -import io.reactivex.functions.Action; -import io.reactivex.functions.Consumer; -import java.util.Collections; -import java.util.List; import javax.inject.Inject; import javax.inject.Named; @@ -76,11 +70,7 @@ public class ContributionsPresenter implements UserActionListener { compositeDisposable.add(repository .save(contribution) .subscribeOn(ioThreadScheduler) - .subscribe(() -> { - WorkManager.getInstance(view.getContext().getApplicationContext()) - .enqueueUniqueWork( - UploadWorker.class.getSimpleName(), - ExistingWorkPolicy.KEEP, OneTimeWorkRequest.from(UploadWorker.class)); - })); + .subscribe(() -> WorkRequestHelper.Companion.makeOneTimeWorkRequest( + view.getContext().getApplicationContext(), ExistingWorkPolicy.KEEP))); } } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java index 740952664..1861f5482 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java @@ -1,6 +1,7 @@ package fr.free.nrw.commons.contributions; import android.Manifest.permission; +import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; import android.content.Intent; @@ -18,8 +19,6 @@ import androidx.appcompat.widget.Toolbar; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.work.ExistingWorkPolicy; -import androidx.work.OneTimeWorkRequest; -import androidx.work.WorkManager; import butterknife.BindView; import butterknife.ButterKnife; import fr.free.nrw.commons.CommonsApplication; @@ -44,9 +43,11 @@ import fr.free.nrw.commons.notification.NotificationController; import fr.free.nrw.commons.quiz.QuizChecker; import fr.free.nrw.commons.settings.SettingsFragment; import fr.free.nrw.commons.theme.BaseActivity; -import fr.free.nrw.commons.upload.worker.UploadWorker; +import fr.free.nrw.commons.upload.worker.WorkRequestHelper; import fr.free.nrw.commons.utils.PermissionUtils; import fr.free.nrw.commons.utils.ViewUtilWrapper; +import io.reactivex.schedulers.Schedulers; +import java.util.Collections; import javax.inject.Inject; import javax.inject.Named; import timber.log.Timber; @@ -58,6 +59,8 @@ public class MainActivity extends BaseActivity SessionManager sessionManager; @Inject ContributionController controller; + @Inject + ContributionDao contributionDao; @BindView(R.id.toolbar) Toolbar toolbar; @BindView(R.id.pager) @@ -138,6 +141,9 @@ public class MainActivity extends BaseActivity setTitle(getString(R.string.navigation_item_explore)); setUpLoggedOutPager(); } else { + if (applicationKvStore.getBoolean("firstrun", true)) { + applicationKvStore.putBoolean("hasAlreadyLaunchedBigMultiupload", false); + } if(savedInstanceState == null){ //starting a fresh fragment. // Open Last opened screen if it is Contributions or Nearby, otherwise Contributions @@ -360,6 +366,21 @@ public class MainActivity extends BaseActivity } } + /** + * Retry all failed uploads as soon as the user returns to the app + */ + @SuppressLint("CheckResult") + private void retryAllFailedUploads() { + contributionDao. + getContribution(Collections.singletonList(Contribution.STATE_FAILED)) + .subscribeOn(Schedulers.io()) + .subscribe(failedUploads -> { + for (Contribution contribution: failedUploads) { + contributionsFragment.retryUpload(contribution); + } + }); + } + public void toggleLimitedConnectionMode() { defaultKvStore.putBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, !defaultKvStore @@ -369,10 +390,8 @@ public class MainActivity extends BaseActivity viewUtilWrapper .showShortToast(getBaseContext(), getString(R.string.limited_connection_enabled)); } else { - WorkManager.getInstance(getApplicationContext()).enqueueUniqueWork( - UploadWorker.class.getSimpleName(), - ExistingWorkPolicy.APPEND_OR_REPLACE, OneTimeWorkRequest.from(UploadWorker.class)); - + WorkRequestHelper.Companion.makeOneTimeWorkRequest(getApplicationContext(), + ExistingWorkPolicy.APPEND_OR_REPLACE); viewUtilWrapper .showShortToast(getBaseContext(), getString(R.string.limited_connection_disabled)); } @@ -406,6 +425,8 @@ public class MainActivity extends BaseActivity defaultKvStore.putBoolean("inAppCameraFirstRun", true); WelcomeActivity.startYourself(this); } + + retryAllFailedUploads(); } @Override diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/helper/OnSwipeTouchListener.kt b/app/src/main/java/fr/free/nrw/commons/customselector/helper/OnSwipeTouchListener.kt index 89cbb8fb4..f454a3af8 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/helper/OnSwipeTouchListener.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/helper/OnSwipeTouchListener.kt @@ -16,7 +16,7 @@ open class OnSwipeTouchListener(context: Context?) : View.OnTouchListener { private val SWIPE_THRESHOLD_WIDTH = (getScreenResolution(context!!)).first / 3 private val SWIPE_VELOCITY_THRESHOLD = 1000 - override fun onTouch(view: View?, motionEvent: MotionEvent?): Boolean { + override fun onTouch(view: View?, motionEvent: MotionEvent): Boolean { return gestureDetector.onTouchEvent(motionEvent) } @@ -32,7 +32,7 @@ open class OnSwipeTouchListener(context: Context?) : View.OnTouchListener { inner class GestureListener : GestureDetector.SimpleOnGestureListener() { - override fun onDown(e: MotionEvent?): Boolean { + override fun onDown(e: MotionEvent): Boolean { return true } diff --git a/app/src/main/java/fr/free/nrw/commons/db/AppDatabase.kt b/app/src/main/java/fr/free/nrw/commons/db/AppDatabase.kt index 49c95343a..6d63e58a1 100644 --- a/app/src/main/java/fr/free/nrw/commons/db/AppDatabase.kt +++ b/app/src/main/java/fr/free/nrw/commons/db/AppDatabase.kt @@ -15,7 +15,7 @@ import fr.free.nrw.commons.upload.depicts.DepictsDao * The database for accessing the respective DAOs * */ -@Database(entities = [Contribution::class, Depicts::class, UploadedStatus::class, NotForUploadStatus::class, ReviewEntity::class], version = 15, exportSchema = false) +@Database(entities = [Contribution::class, Depicts::class, UploadedStatus::class, NotForUploadStatus::class, ReviewEntity::class], version = 16, exportSchema = false) @TypeConverters(Converters::class) abstract class AppDatabase : RoomDatabase() { abstract fun contributionDao(): ContributionDao diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java index 9770e1ff4..91ab805f3 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java @@ -1,7 +1,6 @@ package fr.free.nrw.commons.upload; import static fr.free.nrw.commons.contributions.ContributionController.ACTION_INTERNAL_UPLOADS; -import static fr.free.nrw.commons.upload.UploadPresenter.COUNTER_OF_CONSECUTIVE_UPLOADS_WITHOUT_COORDINATES; import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT; import android.Manifest; @@ -10,14 +9,15 @@ import android.app.ProgressDialog; import android.content.Intent; import android.location.Location; import android.location.LocationManager; +import android.os.Build; import android.os.Bundle; +import android.provider.Settings; import android.util.DisplayMetrics; import android.view.View; import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.RelativeLayout; import android.widget.TextView; -import androidx.appcompat.app.AlertDialog; import androidx.cardview.widget.CardView; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; @@ -27,8 +27,6 @@ import androidx.recyclerview.widget.RecyclerView; import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.ViewPager; import androidx.work.ExistingWorkPolicy; -import androidx.work.OneTimeWorkRequest; -import androidx.work.WorkManager; import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnClick; @@ -37,7 +35,6 @@ import fr.free.nrw.commons.R; import fr.free.nrw.commons.auth.LoginActivity; import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.contributions.ContributionController; -import fr.free.nrw.commons.contributions.MainActivity; import fr.free.nrw.commons.filepicker.UploadableFile; import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.location.LatLng; @@ -52,7 +49,7 @@ import fr.free.nrw.commons.upload.depicts.DepictsFragment; import fr.free.nrw.commons.upload.license.MediaLicenseFragment; import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment; import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.UploadMediaDetailFragmentCallback; -import fr.free.nrw.commons.upload.worker.UploadWorker; +import fr.free.nrw.commons.upload.worker.WorkRequestHelper; import fr.free.nrw.commons.utils.DialogUtil; import fr.free.nrw.commons.utils.PermissionUtils; import fr.free.nrw.commons.utils.ViewUtil; @@ -336,9 +333,8 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, @Override public void makeUploadRequest() { - WorkManager.getInstance(getApplicationContext()).enqueueUniqueWork( - UploadWorker.class.getSimpleName(), - ExistingWorkPolicy.APPEND_OR_REPLACE, OneTimeWorkRequest.from(UploadWorker.class)); + WorkRequestHelper.Companion.makeOneTimeWorkRequest(getApplicationContext(), + ExistingWorkPolicy.APPEND_OR_REPLACE); } @Override @@ -383,6 +379,42 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, .getQuantityString(R.plurals.upload_count_title, uploadableFiles.size(), uploadableFiles.size())); fragments = new ArrayList<>(); + /* Suggest users to turn battery optimisation off when uploading more than a few files. + That's because we have noticed that many-files uploads have + a much higher probability of failing than uploads with less files. + + Show the dialog for Android 6 and above as + the ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS intent was added in API level 23 + */ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (uploadableFiles.size() > 3 + && !defaultKvStore.getBoolean("hasAlreadyLaunchedBigMultiupload")) { + DialogUtil.showAlertDialog( + this, + getString(R.string.unrestricted_battery_mode), + getString(R.string.suggest_unrestricted_mode), + getString(R.string.title_activity_settings), + getString(R.string.cancel), + () -> { + /* Since opening the right settings page might be device dependent, using + https://github.com/WaseemSabir/BatteryPermissionHelper + directly appeared like a promising idea. + However, this simply closed the popup and did not make + the settings page appear on a Pixel as well as a Xiaomi device. + + Used the standard intent instead of using this library as + it shows a list of all the apps on the device and allows users to + turn battery optimisation off. + */ + Intent batteryOptimisationSettingsIntent = new Intent( + Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS); + startActivity(batteryOptimisationSettingsIntent); + }, + () -> {} + ); + defaultKvStore.putBoolean("hasAlreadyLaunchedBigMultiupload", true); + } + } for (UploadableFile uploadableFile : uploadableFiles) { UploadMediaDetailFragment uploadMediaDetailFragment = new UploadMediaDetailFragment(); diff --git a/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt b/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt index 6a4497ea1..c26243762 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt @@ -1,17 +1,20 @@ package fr.free.nrw.commons.upload.worker import android.annotation.SuppressLint +import android.app.Notification import android.app.PendingIntent import android.app.TaskStackBuilder import android.content.Context import android.content.Intent import android.graphics.BitmapFactory +import android.os.Build import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.work.CoroutineWorker import androidx.work.Data import androidx.work.WorkerParameters import androidx.multidex.BuildConfig +import androidx.work.ForegroundInfo import dagger.android.ContributesAndroidInjector import fr.free.nrw.commons.CommonsApplication import fr.free.nrw.commons.Media @@ -40,6 +43,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber +import java.net.SocketTimeoutException import java.util.* import java.util.regex.Pattern import javax.inject.Inject @@ -161,6 +165,8 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : override suspend fun doWork(): Result { var countUpload = 0 + // Start a foreground service + setForeground(createForegroundInfo()) notificationManager = NotificationManagerCompat.from(appContext) val processingUploads = getNotificationBuilder( CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL @@ -170,15 +176,15 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : .blockingGet() //Showing initial notification for the number of uploads being processed - Timber.e("Queued Contributions: "+ queuedContributions.size) + Timber.e("Queued Contributions: " + queuedContributions.size) processingUploads.setContentTitle(appContext.getString(R.string.starting_uploads)) processingUploads.setContentText( appContext.resources.getQuantityString( - R.plurals.starting_multiple_uploads, - queuedContributions.size, - queuedContributions.size - ) + R.plurals.starting_multiple_uploads, + queuedContributions.size, + queuedContributions.size + ) ) notificationManager?.notify( PROCESSING_UPLOADS_NOTIFICATION_TAG, @@ -191,7 +197,7 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : so that the next one does not process these contribution again */ queuedContributions.forEach { - it.state=Contribution.STATE_IN_PROGRESS + it.state = Contribution.STATE_IN_PROGRESS contributionDao.saveSynchronous(it) } @@ -223,10 +229,37 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : PROCESSING_UPLOADS_NOTIFICATION_ID ) } - //TODO make this smart, think of handling retries in the future + // Trigger WorkManager to process any new contributions that may have been added to the queue + val updatedContributionQueue = withContext(Dispatchers.IO) { + contributionDao.getContribution(statesToProcess).blockingGet() + } + if (updatedContributionQueue.isNotEmpty()) { + return Result.retry() + } + return Result.success() } + /** + * Create new notification for foreground service + */ + private fun createForegroundInfo(): ForegroundInfo { + return ForegroundInfo( + 1, + createNotificationForForegroundService() + ) + } + + override suspend fun getForegroundInfo(): ForegroundInfo { + return createForegroundInfo() + } + private fun createNotificationForForegroundService(): Notification { + // TODO: Improve notification for foreground service + return getNotificationBuilder( + CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL)!! + .setContentTitle(appContext.getString(R.string.upload_in_progress)) + .build() + } /** * Returns true is the limited connection mode is enabled */ @@ -540,7 +573,12 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : val intent = Intent(appContext,toClass) return TaskStackBuilder.create(appContext).run { addNextIntentWithParentStack(intent) - getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + getPendingIntent(0, + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT) + } else { + getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT) + } }; } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/worker/WorkRequestHelper.kt b/app/src/main/java/fr/free/nrw/commons/upload/worker/WorkRequestHelper.kt new file mode 100644 index 000000000..9c0bbb6f4 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/worker/WorkRequestHelper.kt @@ -0,0 +1,41 @@ +package fr.free.nrw.commons.upload.worker + +import android.content.Context +import androidx.work.* +import androidx.work.WorkRequest.Companion.MIN_BACKOFF_MILLIS +import java.util.concurrent.TimeUnit + +/** + * Helper class for all the one time work requests + */ +class WorkRequestHelper { + + companion object { + fun makeOneTimeWorkRequest(context: Context, existingWorkPolicy: ExistingWorkPolicy) { + /* Set backoff criteria for the work request + The default backoff policy is EXPONENTIAL, but while testing we found that it + too long for the uploads to finish. So, set the backoff policy as LINEAR with the + minimum backoff delay value of 10 seconds. + + More details on when exactly it is retried: + https://developer.android.com/guide/background/persistent/getting-started/define-work#retries_backoff + */ + val constraints: Constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + val uploadRequest: OneTimeWorkRequest = + OneTimeWorkRequest.Builder(UploadWorker::class.java) + .setBackoffCriteria( + BackoffPolicy.LINEAR, + MIN_BACKOFF_MILLIS, + TimeUnit.MILLISECONDS + ) + .setConstraints(constraints) + .build() + WorkManager.getInstance(context).enqueueUniqueWork( + UploadWorker::class.java.simpleName, existingWorkPolicy, uploadRequest + ) + } + } + +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 42a1b70dc..9cef1c77d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -62,6 +62,7 @@ Settings Upload to Commons + Upload in progress Username Password Log in to your Commons Beta account @@ -75,6 +76,9 @@ Login success! Login failed! File not found. Please try another file. + Maximum retry limit reached! Please cancel the upload and try again + Turn battery optimization off? + Uploading more than 3 images works more reliably when the battery optimization is turned off. Please turn battery optimization off for the Commons app from the settings for a smooth upload experience. \n\nPossible steps to turn battery optimization off:\n\nStep 1: Tap on the \'Settings\' button below.\n\nStep 2: Switch from \'Not optimized\' to \'All apps\'.\n\nStep 3: Search for \"Commons\" or \"fr.free.nrw.commons\".\n\nStep 4: Tap it and select \'Don\'t optimize\'.\n\nStep 5: Press \'Done\'. Authentication failed, please login again Upload started! Upload queued (limited connection mode enabled)