Skip to content

Commit 81030d1

Browse files
5136: Fix retried uploads stuck in queued state (commons-app#5272)
* fix stuck uploads * automate retries for failed uploads once the user returns to the app * UploadWorker: modify PendingIntent flag and Android version code * MainActivity: remove automatic retry logic * Revert "MainActivity: remove automatic retry logic" * set work request as expedited * handle notification for foreground service on older versions of Android * set backoff criteria for work requests * enqueue failed uploads for a retry * revert "enqueue failed uploads for a retry" * limit the number of retries for a failed upload * add a popup that suggests users to switch to unrestricted battery usage mode * take users to the battery settings page on the first big upload * take users to battery optimisation settings page using the standard intent * add instructions to the battery optimisation settings popup * remove the first usage of fr.free.nrw.commons from the popup * comply with the wording in the OS settings * modify battery optimisation popup instructions, add comments and rename firstBigUploadSet * add filename to the retry log statement * update database version * make battery optimisation dialog appear only on Android 6 and above * use foreground service instead of setting work request as expedited * fix retried uploads stuck in queued state * use MIN_BACKOFF_MILLIS constant instead of using the number 10 and add comments * factorise the creation of the new OneTimeWorkRequest at one place * ensure work requests are in accordance with the unit tests * forbid retries for images which have got uploaded without caption * add a TODO for the suggestion related to retries * revert "forbid retries for images which have got uploaded without caption"
1 parent 4540f54 commit 81030d1

File tree

11 files changed

+200
-47
lines changed

11 files changed

+200
-47
lines changed

app/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ dependencies {
142142

143143
implementation "androidx.multidex:multidex:$MULTIDEX_VERSION"
144144

145-
def work_version = "2.8.0"
145+
def work_version = "2.8.1"
146146
// Kotlin + coroutines
147147
implementation "androidx.work:work-runtime-ktx:$work_version"
148148
implementation("androidx.work:work-runtime:$work_version")
@@ -168,7 +168,7 @@ project.gradle.taskGraph.whenReady {
168168
}
169169

170170
android {
171-
compileSdkVersion 31
171+
compileSdkVersion 33
172172

173173
defaultConfig {
174174
//applicationId 'fr.free.nrw.commons'

app/src/main/java/fr/free/nrw/commons/contributions/Contribution.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,11 @@ data class Contribution constructor(
4343
var hasInvalidLocation : Int = 0,
4444
var contentUri: Uri? = null,
4545
var countryCode : String? = null,
46-
var imageSHA1 : String? = null
46+
var imageSHA1 : String? = null,
47+
/**
48+
* Number of times a contribution has been retried after a failure
49+
*/
50+
var retries: Int = 0
4751
) : Parcelable {
4852

4953
fun completeWith(media: Media): Contribution {

app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ public class ContributionsFragment
9292
private static final String CONTRIBUTION_LIST_FRAGMENT_TAG = "ContributionListFragmentTag";
9393
private MediaDetailPagerFragment mediaDetailPagerFragment;
9494
static final String MEDIA_DETAIL_PAGER_FRAGMENT_TAG = "MediaDetailFragmentTag";
95+
private static final int MAX_RETRIES = 10;
9596

9697
@BindView(R.id.card_view_nearby) public NearbyNotificationCardView nearbyNotificationCardView;
9798
@BindView(R.id.campaigns_view) CampaignView campaignView;
@@ -593,6 +594,15 @@ public void notifyDataSetChanged() {
593594
}
594595
}
595596

597+
/**
598+
* Restarts the upload process for a contribution
599+
* @param contribution
600+
*/
601+
public void restartUpload(Contribution contribution) {
602+
contribution.setState(Contribution.STATE_QUEUED);
603+
contributionsPresenter.saveContribution(contribution);
604+
Timber.d("Restarting for %s", contribution.toString());
605+
}
596606
/**
597607
* Retry upload when it is failed
598608
*
@@ -601,10 +611,23 @@ public void notifyDataSetChanged() {
601611
@Override
602612
public void retryUpload(Contribution contribution) {
603613
if (NetworkUtils.isInternetConnectionEstablished(getContext())) {
604-
if (contribution.getState() == STATE_FAILED || contribution.getState() == STATE_PAUSED || contribution.getState()==Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE) {
605-
contribution.setState(Contribution.STATE_QUEUED);
606-
contributionsPresenter.saveContribution(contribution);
607-
Timber.d("Restarting for %s", contribution.toString());
614+
if (contribution.getState() == STATE_PAUSED || contribution.getState()==Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE) {
615+
restartUpload(contribution);
616+
} else if (contribution.getState() == STATE_FAILED) {
617+
int retries = contribution.getRetries();
618+
// TODO: Improve UX. Additional details: https://github.com/commons-app/apps-android-commons/pull/5257#discussion_r1304662562
619+
/* Limit the number of retries for a failed upload
620+
to handle cases like invalid filename as such uploads
621+
will never be successful */
622+
if(retries < MAX_RETRIES) {
623+
contribution.setRetries(retries + 1);
624+
Timber.d("Retried uploading %s %d times", contribution.getMedia().getFilename(), retries + 1);
625+
restartUpload(contribution);
626+
} else {
627+
// TODO: Show the exact reason for failure
628+
Toast.makeText(getContext(),
629+
R.string.retry_limit_reached, Toast.LENGTH_SHORT).show();
630+
}
608631
} else {
609632
Timber.d("Skipping re-upload for non-failed %s", contribution.toString());
610633
}

app/src/main/java/fr/free/nrw/commons/contributions/ContributionsPresenter.java

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,12 @@
11
package fr.free.nrw.commons.contributions;
22

33
import androidx.work.ExistingWorkPolicy;
4-
import androidx.work.OneTimeWorkRequest;
5-
import androidx.work.WorkManager;
64
import fr.free.nrw.commons.MediaDataExtractor;
75
import fr.free.nrw.commons.contributions.ContributionsContract.UserActionListener;
86
import fr.free.nrw.commons.di.CommonsApplicationModule;
9-
import fr.free.nrw.commons.upload.worker.UploadWorker;
7+
import fr.free.nrw.commons.upload.worker.WorkRequestHelper;
108
import io.reactivex.Scheduler;
119
import io.reactivex.disposables.CompositeDisposable;
12-
import io.reactivex.functions.Action;
13-
import io.reactivex.functions.Consumer;
14-
import java.util.Collections;
15-
import java.util.List;
1610
import javax.inject.Inject;
1711
import javax.inject.Named;
1812

@@ -76,11 +70,7 @@ public void saveContribution(Contribution contribution) {
7670
compositeDisposable.add(repository
7771
.save(contribution)
7872
.subscribeOn(ioThreadScheduler)
79-
.subscribe(() -> {
80-
WorkManager.getInstance(view.getContext().getApplicationContext())
81-
.enqueueUniqueWork(
82-
UploadWorker.class.getSimpleName(),
83-
ExistingWorkPolicy.KEEP, OneTimeWorkRequest.from(UploadWorker.class));
84-
}));
73+
.subscribe(() -> WorkRequestHelper.Companion.makeOneTimeWorkRequest(
74+
view.getContext().getApplicationContext(), ExistingWorkPolicy.KEEP)));
8575
}
8676
}

app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package fr.free.nrw.commons.contributions;
22

33
import android.Manifest.permission;
4+
import android.annotation.SuppressLint;
45
import android.app.Activity;
56
import android.content.Context;
67
import android.content.Intent;
@@ -18,8 +19,6 @@
1819
import androidx.fragment.app.Fragment;
1920
import androidx.fragment.app.FragmentManager;
2021
import androidx.work.ExistingWorkPolicy;
21-
import androidx.work.OneTimeWorkRequest;
22-
import androidx.work.WorkManager;
2322
import butterknife.BindView;
2423
import butterknife.ButterKnife;
2524
import fr.free.nrw.commons.CommonsApplication;
@@ -44,9 +43,11 @@
4443
import fr.free.nrw.commons.quiz.QuizChecker;
4544
import fr.free.nrw.commons.settings.SettingsFragment;
4645
import fr.free.nrw.commons.theme.BaseActivity;
47-
import fr.free.nrw.commons.upload.worker.UploadWorker;
46+
import fr.free.nrw.commons.upload.worker.WorkRequestHelper;
4847
import fr.free.nrw.commons.utils.PermissionUtils;
4948
import fr.free.nrw.commons.utils.ViewUtilWrapper;
49+
import io.reactivex.schedulers.Schedulers;
50+
import java.util.Collections;
5051
import javax.inject.Inject;
5152
import javax.inject.Named;
5253
import timber.log.Timber;
@@ -58,6 +59,8 @@ public class MainActivity extends BaseActivity
5859
SessionManager sessionManager;
5960
@Inject
6061
ContributionController controller;
62+
@Inject
63+
ContributionDao contributionDao;
6164
@BindView(R.id.toolbar)
6265
Toolbar toolbar;
6366
@BindView(R.id.pager)
@@ -138,6 +141,9 @@ public void onCreate(Bundle savedInstanceState) {
138141
setTitle(getString(R.string.navigation_item_explore));
139142
setUpLoggedOutPager();
140143
} else {
144+
if (applicationKvStore.getBoolean("firstrun", true)) {
145+
applicationKvStore.putBoolean("hasAlreadyLaunchedBigMultiupload", false);
146+
}
141147
if(savedInstanceState == null){
142148
//starting a fresh fragment.
143149
// Open Last opened screen if it is Contributions or Nearby, otherwise Contributions
@@ -360,6 +366,21 @@ public boolean onOptionsItemSelected(MenuItem item) {
360366
}
361367
}
362368

369+
/**
370+
* Retry all failed uploads as soon as the user returns to the app
371+
*/
372+
@SuppressLint("CheckResult")
373+
private void retryAllFailedUploads() {
374+
contributionDao.
375+
getContribution(Collections.singletonList(Contribution.STATE_FAILED))
376+
.subscribeOn(Schedulers.io())
377+
.subscribe(failedUploads -> {
378+
for (Contribution contribution: failedUploads) {
379+
contributionsFragment.retryUpload(contribution);
380+
}
381+
});
382+
}
383+
363384
public void toggleLimitedConnectionMode() {
364385
defaultKvStore.putBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED,
365386
!defaultKvStore
@@ -369,10 +390,8 @@ public void toggleLimitedConnectionMode() {
369390
viewUtilWrapper
370391
.showShortToast(getBaseContext(), getString(R.string.limited_connection_enabled));
371392
} else {
372-
WorkManager.getInstance(getApplicationContext()).enqueueUniqueWork(
373-
UploadWorker.class.getSimpleName(),
374-
ExistingWorkPolicy.APPEND_OR_REPLACE, OneTimeWorkRequest.from(UploadWorker.class));
375-
393+
WorkRequestHelper.Companion.makeOneTimeWorkRequest(getApplicationContext(),
394+
ExistingWorkPolicy.APPEND_OR_REPLACE);
376395
viewUtilWrapper
377396
.showShortToast(getBaseContext(), getString(R.string.limited_connection_disabled));
378397
}
@@ -406,6 +425,8 @@ protected void onResume() {
406425
defaultKvStore.putBoolean("inAppCameraFirstRun", true);
407426
WelcomeActivity.startYourself(this);
408427
}
428+
429+
retryAllFailedUploads();
409430
}
410431

411432
@Override

app/src/main/java/fr/free/nrw/commons/customselector/helper/OnSwipeTouchListener.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ open class OnSwipeTouchListener(context: Context?) : View.OnTouchListener {
1616
private val SWIPE_THRESHOLD_WIDTH = (getScreenResolution(context!!)).first / 3
1717
private val SWIPE_VELOCITY_THRESHOLD = 1000
1818

19-
override fun onTouch(view: View?, motionEvent: MotionEvent?): Boolean {
19+
override fun onTouch(view: View?, motionEvent: MotionEvent): Boolean {
2020
return gestureDetector.onTouchEvent(motionEvent)
2121
}
2222

@@ -32,7 +32,7 @@ open class OnSwipeTouchListener(context: Context?) : View.OnTouchListener {
3232

3333
inner class GestureListener : GestureDetector.SimpleOnGestureListener() {
3434

35-
override fun onDown(e: MotionEvent?): Boolean {
35+
override fun onDown(e: MotionEvent): Boolean {
3636
return true
3737
}
3838

app/src/main/java/fr/free/nrw/commons/db/AppDatabase.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import fr.free.nrw.commons.upload.depicts.DepictsDao
1515
* The database for accessing the respective DAOs
1616
*
1717
*/
18-
@Database(entities = [Contribution::class, Depicts::class, UploadedStatus::class, NotForUploadStatus::class, ReviewEntity::class], version = 15, exportSchema = false)
18+
@Database(entities = [Contribution::class, Depicts::class, UploadedStatus::class, NotForUploadStatus::class, ReviewEntity::class], version = 16, exportSchema = false)
1919
@TypeConverters(Converters::class)
2020
abstract class AppDatabase : RoomDatabase() {
2121
abstract fun contributionDao(): ContributionDao

app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package fr.free.nrw.commons.upload;
22

33
import static fr.free.nrw.commons.contributions.ContributionController.ACTION_INTERNAL_UPLOADS;
4-
import static fr.free.nrw.commons.upload.UploadPresenter.COUNTER_OF_CONSECUTIVE_UPLOADS_WITHOUT_COORDINATES;
54
import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT;
65

76
import android.Manifest;
@@ -10,14 +9,15 @@
109
import android.content.Intent;
1110
import android.location.Location;
1211
import android.location.LocationManager;
12+
import android.os.Build;
1313
import android.os.Bundle;
14+
import android.provider.Settings;
1415
import android.util.DisplayMetrics;
1516
import android.view.View;
1617
import android.widget.ImageButton;
1718
import android.widget.LinearLayout;
1819
import android.widget.RelativeLayout;
1920
import android.widget.TextView;
20-
import androidx.appcompat.app.AlertDialog;
2121
import androidx.cardview.widget.CardView;
2222
import androidx.fragment.app.Fragment;
2323
import androidx.fragment.app.FragmentManager;
@@ -27,8 +27,6 @@
2727
import androidx.viewpager.widget.PagerAdapter;
2828
import androidx.viewpager.widget.ViewPager;
2929
import androidx.work.ExistingWorkPolicy;
30-
import androidx.work.OneTimeWorkRequest;
31-
import androidx.work.WorkManager;
3230
import butterknife.BindView;
3331
import butterknife.ButterKnife;
3432
import butterknife.OnClick;
@@ -37,7 +35,6 @@
3735
import fr.free.nrw.commons.auth.LoginActivity;
3836
import fr.free.nrw.commons.auth.SessionManager;
3937
import fr.free.nrw.commons.contributions.ContributionController;
40-
import fr.free.nrw.commons.contributions.MainActivity;
4138
import fr.free.nrw.commons.filepicker.UploadableFile;
4239
import fr.free.nrw.commons.kvstore.JsonKvStore;
4340
import fr.free.nrw.commons.location.LatLng;
@@ -52,7 +49,7 @@
5249
import fr.free.nrw.commons.upload.license.MediaLicenseFragment;
5350
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment;
5451
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.UploadMediaDetailFragmentCallback;
55-
import fr.free.nrw.commons.upload.worker.UploadWorker;
52+
import fr.free.nrw.commons.upload.worker.WorkRequestHelper;
5653
import fr.free.nrw.commons.utils.DialogUtil;
5754
import fr.free.nrw.commons.utils.PermissionUtils;
5855
import fr.free.nrw.commons.utils.ViewUtil;
@@ -336,9 +333,8 @@ public void updateTopCardTitle() {
336333

337334
@Override
338335
public void makeUploadRequest() {
339-
WorkManager.getInstance(getApplicationContext()).enqueueUniqueWork(
340-
UploadWorker.class.getSimpleName(),
341-
ExistingWorkPolicy.APPEND_OR_REPLACE, OneTimeWorkRequest.from(UploadWorker.class));
336+
WorkRequestHelper.Companion.makeOneTimeWorkRequest(getApplicationContext(),
337+
ExistingWorkPolicy.APPEND_OR_REPLACE);
342338
}
343339

344340
@Override
@@ -383,6 +379,42 @@ private void receiveSharedItems() {
383379
.getQuantityString(R.plurals.upload_count_title, uploadableFiles.size(), uploadableFiles.size()));
384380

385381
fragments = new ArrayList<>();
382+
/* Suggest users to turn battery optimisation off when uploading more than a few files.
383+
That's because we have noticed that many-files uploads have
384+
a much higher probability of failing than uploads with less files.
385+
386+
Show the dialog for Android 6 and above as
387+
the ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS intent was added in API level 23
388+
*/
389+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
390+
if (uploadableFiles.size() > 3
391+
&& !defaultKvStore.getBoolean("hasAlreadyLaunchedBigMultiupload")) {
392+
DialogUtil.showAlertDialog(
393+
this,
394+
getString(R.string.unrestricted_battery_mode),
395+
getString(R.string.suggest_unrestricted_mode),
396+
getString(R.string.title_activity_settings),
397+
getString(R.string.cancel),
398+
() -> {
399+
/* Since opening the right settings page might be device dependent, using
400+
https://github.com/WaseemSabir/BatteryPermissionHelper
401+
directly appeared like a promising idea.
402+
However, this simply closed the popup and did not make
403+
the settings page appear on a Pixel as well as a Xiaomi device.
404+
405+
Used the standard intent instead of using this library as
406+
it shows a list of all the apps on the device and allows users to
407+
turn battery optimisation off.
408+
*/
409+
Intent batteryOptimisationSettingsIntent = new Intent(
410+
Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS);
411+
startActivity(batteryOptimisationSettingsIntent);
412+
},
413+
() -> {}
414+
);
415+
defaultKvStore.putBoolean("hasAlreadyLaunchedBigMultiupload", true);
416+
}
417+
}
386418
for (UploadableFile uploadableFile : uploadableFiles) {
387419
UploadMediaDetailFragment uploadMediaDetailFragment = new UploadMediaDetailFragment();
388420

0 commit comments

Comments
 (0)