Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

5128: Fix stuck uploads #5257

Closed
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
4aa4ea0
fix stuck uploads
RitikaPahwa4444 Jul 6, 2023
96058ff
automate retries for failed uploads once the user returns to the app
RitikaPahwa4444 Jul 7, 2023
870f286
UploadWorker: modify PendingIntent flag and Android version code
RitikaPahwa4444 Jul 10, 2023
1ebb117
MainActivity: remove automatic retry logic
RitikaPahwa4444 Jul 13, 2023
2dd2be1
Revert "MainActivity: remove automatic retry logic"
RitikaPahwa4444 Jul 14, 2023
6b9e5ab
set work request as expedited
RitikaPahwa4444 Jul 16, 2023
2ec4667
merge branch 'master' into issue-5128_fix_stuck_uploads
RitikaPahwa4444 Jul 26, 2023
f1b2e03
handle notification for foreground service on older versions of Android
RitikaPahwa4444 Aug 2, 2023
0a64989
set backoff criteria for work requests
RitikaPahwa4444 Aug 9, 2023
c785d6f
enqueue failed uploads for a retry
RitikaPahwa4444 Aug 14, 2023
e1855b9
revert "enqueue failed uploads for a retry"
RitikaPahwa4444 Aug 15, 2023
6d013e6
limit the number of retries for a failed upload
RitikaPahwa4444 Aug 17, 2023
4cafd2a
add a popup that suggests users to switch to unrestricted battery usa…
RitikaPahwa4444 Aug 18, 2023
04b7255
take users to the battery settings page on the first big upload
RitikaPahwa4444 Aug 19, 2023
533e976
take users to battery optimisation settings page using the standard i…
RitikaPahwa4444 Aug 19, 2023
4fbe261
add instructions to the battery optimisation settings popup
RitikaPahwa4444 Aug 19, 2023
c35d936
remove the first usage of fr.free.nrw.commons from the popup
RitikaPahwa4444 Aug 19, 2023
d1bfb48
comply with the wording in the OS settings
RitikaPahwa4444 Aug 19, 2023
3ff04d3
modify battery optimisation popup instructions, add comments and rena…
RitikaPahwa4444 Aug 21, 2023
ab2f114
add filename to the retry log statement
RitikaPahwa4444 Aug 22, 2023
faf0cde
update database version
RitikaPahwa4444 Aug 23, 2023
f441eaa
make battery optimisation dialog appear only on Android 6 and above
RitikaPahwa4444 Aug 23, 2023
5ec4d56
use foreground service instead of setting work request as expedited
RitikaPahwa4444 Aug 24, 2023
034d8d6
forbid retries for images which have got uploaded without caption
RitikaPahwa4444 Aug 31, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -153,6 +153,7 @@ dependencies {
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'

implementation("io.github.coordinates2country:coordinates2country-android:1.3") { exclude group: 'com.google.android', module: 'android' }

}

task disableAnimations(type: Exec) {
Expand All @@ -168,7 +169,7 @@ project.gradle.taskGraph.whenReady {
}

android {
compileSdkVersion 31
compileSdkVersion 33

defaultConfig {
//applicationId 'fr.free.nrw.commons'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a schema change to the contribution table and requires a bump of the db version number [ref]. Kindly take care of the same.

) : Parcelable {

fun completeWith(media: Media): Contribution {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
nicolas-raoul marked this conversation as resolved.
Show resolved Hide resolved

@BindView(R.id.card_view_nearby) public NearbyNotificationCardView nearbyNotificationCardView;
@BindView(R.id.campaigns_view) CampaignView campaignView;
Expand Down Expand Up @@ -593,6 +594,15 @@ public void notifyDataSetChanged() {
}
}

/**
* 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
*
Expand All @@ -601,10 +611,22 @@ public void notifyDataSetChanged() {
@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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to test the change in my device. Blindly retrying all failed uploads does seem to be affecting the UX badly. The app seems to just retrying genuinely failed uploads again-and-again each time I open the app.

Is there really no other attribute that we have that could help with identifying if an upload failed for a genuine reason? (I'm presuming its because of beta cluster not supporting stashed uploads).

Also, I remember cancelling all uploads manually one time. The cancel didn't seem to have worked. Has anyone else observed this too?

int retries = contribution.getRetries();
/* 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 %d times", retries + 1);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can it be changed to "Retried upload %d times" so it's clear what we're trying to retry?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed the debug log line could say that, and also say what file, because often several files are being retried at the same time.

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());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
package fr.free.nrw.commons.contributions;

import androidx.work.BackoffPolicy;
import androidx.work.Constraints;
import androidx.work.ExistingWorkPolicy;
import androidx.work.NetworkType;
import androidx.work.OneTimeWorkRequest;
import androidx.work.OutOfQuotaPolicy;
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 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 java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.inject.Named;

Expand Down Expand Up @@ -77,10 +78,19 @@ public void saveContribution(Contribution contribution) {
.save(contribution)
.subscribeOn(ioThreadScheduler)
.subscribe(() -> {
Constraints constraints = new Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build();
OneTimeWorkRequest updatedUploadRequest = new OneTimeWorkRequest
.Builder(UploadWorker.class)
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.SECONDS)
nicolas-raoul marked this conversation as resolved.
Show resolved Hide resolved
.build();
WorkManager.getInstance(view.getContext().getApplicationContext())
.enqueueUniqueWork(
UploadWorker.class.getSimpleName(),
ExistingWorkPolicy.KEEP, OneTimeWorkRequest.from(UploadWorker.class));
ExistingWorkPolicy.KEEP, updatedUploadRequest);
}));
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -17,8 +18,12 @@
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.work.BackoffPolicy;
import androidx.work.Constraints;
import androidx.work.ExistingWorkPolicy;
import androidx.work.NetworkType;
import androidx.work.OneTimeWorkRequest;
import androidx.work.OutOfQuotaPolicy;
import androidx.work.WorkManager;
import butterknife.BindView;
import butterknife.ButterKnife;
Expand Down Expand Up @@ -47,6 +52,9 @@
import fr.free.nrw.commons.upload.worker.UploadWorker;
import fr.free.nrw.commons.utils.PermissionUtils;
import fr.free.nrw.commons.utils.ViewUtilWrapper;
import io.reactivex.schedulers.Schedulers;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.inject.Named;
import timber.log.Timber;
Expand All @@ -58,6 +66,8 @@ public class MainActivity extends BaseActivity
SessionManager sessionManager;
@Inject
ContributionController controller;
@Inject
ContributionDao contributionDao;
@BindView(R.id.toolbar)
Toolbar toolbar;
@BindView(R.id.pager)
Expand Down Expand Up @@ -138,6 +148,9 @@ public void onCreate(Bundle savedInstanceState) {
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
Expand Down Expand Up @@ -360,6 +373,21 @@ public boolean onOptionsItemSelected(MenuItem item) {
}
}

/**
* 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
Expand All @@ -369,9 +397,18 @@ public void toggleLimitedConnectionMode() {
viewUtilWrapper
.showShortToast(getBaseContext(), getString(R.string.limited_connection_enabled));
} else {
Constraints constraints = new Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build();
OneTimeWorkRequest restartUploadsRequest = new OneTimeWorkRequest
.Builder(UploadWorker.class)
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.SECONDS)
.build();
WorkManager.getInstance(getApplicationContext()).enqueueUniqueWork(
UploadWorker.class.getSimpleName(),
ExistingWorkPolicy.APPEND_OR_REPLACE, OneTimeWorkRequest.from(UploadWorker.class));
ExistingWorkPolicy.APPEND_OR_REPLACE, restartUploadsRequest);

viewUtilWrapper
.showShortToast(getBaseContext(), getString(R.string.limited_connection_disabled));
Expand Down Expand Up @@ -405,6 +442,8 @@ protected void onResume() {
(!applicationKvStore.getBoolean("login_skipped"))) {
WelcomeActivity.startYourself(this);
}

retryAllFailedUploads();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand All @@ -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
}

Expand Down
51 changes: 47 additions & 4 deletions app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
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;
import android.annotation.SuppressLint;
import android.app.ProgressDialog;
import android.content.Intent;
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;
Expand All @@ -24,8 +23,12 @@
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager;
import androidx.work.BackoffPolicy;
import androidx.work.Constraints;
import androidx.work.ExistingWorkPolicy;
import androidx.work.NetworkType;
import androidx.work.OneTimeWorkRequest;
import androidx.work.OutOfQuotaPolicy;
import androidx.work.WorkManager;
import butterknife.BindView;
import butterknife.ButterKnife;
Expand All @@ -35,7 +38,6 @@
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.mwapi.UserClient;
Expand All @@ -57,6 +59,7 @@
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.inject.Named;
import timber.log.Timber;
Expand Down Expand Up @@ -317,9 +320,18 @@ public void updateTopCardTitle() {

@Override
public void makeUploadRequest() {
Constraints constraints = new Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build();
OneTimeWorkRequest uploadRequest = new OneTimeWorkRequest
.Builder(UploadWorker.class)
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.SECONDS)
.build();
WorkManager.getInstance(getApplicationContext()).enqueueUniqueWork(
UploadWorker.class.getSimpleName(),
ExistingWorkPolicy.APPEND_OR_REPLACE, OneTimeWorkRequest.from(UploadWorker.class));
ExistingWorkPolicy.APPEND_OR_REPLACE, uploadRequest);
}

@Override
Expand Down Expand Up @@ -364,6 +376,37 @@ private void receiveSharedItems() {
.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.
*/
if (uploadableFiles.size() > 3
&& !defaultKvStore.getBoolean("hasAlreadyLaunchedBigMultiupload")) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we should show this dialog only for relevant Android versions. The intent itself is only available since API 23 (6+). Kindly check and make sure we only invoke this for the relevant Android versions.

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(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a comment about the library you tried, explaining how it did not make the setting dialog appear on Pixel nor Xiaomi. Someone may want to revisit that in the future.

Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS);
startActivity(batteryOptimisationSettingsIntent);
},
() -> {}
);
defaultKvStore.putBoolean("hasAlreadyLaunchedBigMultiupload", true);
}
for (UploadableFile uploadableFile : uploadableFiles) {
UploadMediaDetailFragment uploadMediaDetailFragment = new UploadMediaDetailFragment();
uploadMediaDetailFragment.setImageTobeUploaded(uploadableFile, place);
Expand Down
Loading