diff --git a/app/build.gradle b/app/build.gradle index f4ce47239b..a7b9cf4c61 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 c3d084f0af..28e5892bd3 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,12 @@ 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, + var wikidataUpdateWasSuccessful: Boolean = false ) : 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 03e3e56116..8276f9c741 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 @@ -29,10 +29,13 @@ import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.auth.SessionManager; +import fr.free.nrw.commons.media.MediaClient; import fr.free.nrw.commons.notification.models.Notification; import fr.free.nrw.commons.notification.NotificationController; import fr.free.nrw.commons.profile.ProfileActivity; import fr.free.nrw.commons.theme.BaseActivity; +import fr.free.nrw.commons.upload.FileUtilsWrapper; +import java.io.FileNotFoundException; import java.util.Date; import java.util.List; import javax.inject.Inject; @@ -85,6 +88,10 @@ public class ContributionsFragment @Inject CampaignsPresenter presenter; @Inject LocationServiceManager locationManager; @Inject NotificationController notificationController; + @Inject + MediaClient mediaClient; + @Inject + FileUtilsWrapper fileUtilsWrapper; private CompositeDisposable compositeDisposable = new CompositeDisposable(); @@ -92,6 +99,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 +601,54 @@ public void notifyDataSetChanged() { } } + /** + * Restarts the upload process for a contribution + * @param contribution + */ + private void restartUpload(Contribution contribution) { + contribution.setState(Contribution.STATE_QUEUED); + contributionsPresenter.saveContribution(contribution); + Timber.d("Restarting for %s", contribution.toString()); + } + + /** + * Retry for a failed contribution only if it does not exist on the server + * @param contribution + * @param retries + */ + private void retryFailedUpload(Contribution contribution, int retries) { + String filePath = contribution.getLocalUri().getPath(); + String fileSHA; + try { + fileSHA = fileUtilsWrapper.getSHA1(fileUtilsWrapper.getFileInputStream(filePath)); + } catch (FileNotFoundException e) { + fileSHA = null; + } + boolean fileExists = mediaClient + .checkFileExistsUsingSha(fileSHA) + .subscribeOn(Schedulers.io()).blockingGet(); + if (fileExists && !contribution.getWikidataUpdateWasSuccessful()) { + Timber.d("%s exists on the server already but lacks caption", + contribution.getMedia().getFilename()); + // Show a popup to the user to edit the caption + DialogUtil.showAlertDialog( + getActivity(), + getString(R.string.wikidata_edit_failed_title), + getString(R.string.wikidata_edit_failed_explanation, contribution.getMedia().getFilename()), + getString(R.string.ok), + () -> {}, + true + ); + contribution.setState(Contribution.STATE_COMPLETED); + contributionsPresenter.saveContribution(contribution); + } else { + contribution.setRetries(retries + 1); + Timber.d("Retried uploading %s %d times", + contribution.getMedia().getFilename(), retries + 1); + restartUpload(contribution); + } + } + /** * Retry upload when it is failed * @@ -601,10 +657,20 @@ 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) { + 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) { + retryFailedUpload(contribution, retries); + } 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 002e8bc95b..e6fdaa11ec 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,7 +1,11 @@ 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; @@ -9,10 +13,7 @@ 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; @@ -77,10 +78,18 @@ 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) + .setConstraints(constraints) + .setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.SECONDS) + .build(); WorkManager.getInstance(view.getContext().getApplicationContext()) .enqueueUniqueWork( UploadWorker.class.getSimpleName(), - ExistingWorkPolicy.KEEP, OneTimeWorkRequest.from(UploadWorker.class)); + ExistingWorkPolicy.KEEP, updatedUploadRequest); })); } } 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 a96f1f37bd..18a5709309 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; @@ -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; @@ -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; @@ -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) @@ -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 @@ -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 @@ -369,9 +397,17 @@ 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) + .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)); @@ -405,6 +441,8 @@ protected void onResume() { (!applicationKvStore.getBoolean("login_skipped"))) { 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 89cbb8fb40..f454a3af8c 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 49c95343a5..6d63e58a1c 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 7fd4ecce7f..8279f8e74c 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,21 +1,21 @@ 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.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; @@ -24,8 +24,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; @@ -35,7 +39,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; @@ -57,6 +60,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; @@ -317,9 +321,17 @@ public void updateTopCardTitle() { @Override public void makeUploadRequest() { + Constraints constraints = new Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build(); + OneTimeWorkRequest uploadRequest = new OneTimeWorkRequest + .Builder(UploadWorker.class) + .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 @@ -364,6 +376,42 @@ 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. + + 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(); uploadMediaDetailFragment.setImageTobeUploaded(uploadableFile, place); 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 6a4497ea15..4761f48c5d 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 @@ -160,73 +164,99 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : } override suspend fun doWork(): Result { - var countUpload = 0 - notificationManager = NotificationManagerCompat.from(appContext) - val processingUploads = getNotificationBuilder( - CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL - )!! - withContext(Dispatchers.IO) { - val queuedContributions = contributionDao.getContribution(statesToProcess) - .blockingGet() - //Showing initial notification for the number of uploads being processed - - 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 - ) - ) - notificationManager?.notify( - PROCESSING_UPLOADS_NOTIFICATION_TAG, - PROCESSING_UPLOADS_NOTIFICATION_ID, - processingUploads.build() - ) + try { + var countUpload = 0 + // Start a foreground service + setForeground(createForegroundInfo()) + notificationManager = NotificationManagerCompat.from(appContext) + val processingUploads = getNotificationBuilder( + CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL + )!! + withContext(Dispatchers.IO) { + val queuedContributions = contributionDao.getContribution(statesToProcess) + .blockingGet() + //Showing initial notification for the number of uploads being processed - /** - * To avoid race condition when multiple of these workers are working, assign this state - so that the next one does not process these contribution again - */ - queuedContributions.forEach { - it.state=Contribution.STATE_IN_PROGRESS - contributionDao.saveSynchronous(it) - } + 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 + ) + ) + notificationManager?.notify( + PROCESSING_UPLOADS_NOTIFICATION_TAG, + PROCESSING_UPLOADS_NOTIFICATION_ID, + processingUploads.build() + ) - queuedContributions.asFlow().map { contribution -> /** - * If the limited connection mode is on, lets iterate through the queued - * contributions - * and set the state as STATE_QUEUED_LIMITED_CONNECTION_MODE , - * otherwise proceed with the upload + * To avoid race condition when multiple of these workers are working, assign this state + so that the next one does not process these contribution again */ - if (isLimitedConnectionModeEnabled()) { - if (contribution.state == Contribution.STATE_QUEUED) { - contribution.state = Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE + queuedContributions.forEach { + it.state = Contribution.STATE_IN_PROGRESS + contributionDao.saveSynchronous(it) + } + + queuedContributions.asFlow().map { contribution -> + /** + * If the limited connection mode is on, lets iterate through the queued + * contributions + * and set the state as STATE_QUEUED_LIMITED_CONNECTION_MODE , + * otherwise proceed with the upload + */ + if (isLimitedConnectionModeEnabled()) { + if (contribution.state == Contribution.STATE_QUEUED) { + contribution.state = Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE + contributionDao.saveSynchronous(contribution) + } + } else { + contribution.transferred = 0 + contribution.state = Contribution.STATE_IN_PROGRESS contributionDao.saveSynchronous(contribution) + setProgressAsync(Data.Builder().putInt("progress", countUpload).build()) + countUpload++ + uploadContribution(contribution = contribution) } - } else { - contribution.transferred = 0 - contribution.state = Contribution.STATE_IN_PROGRESS - contributionDao.saveSynchronous(contribution) - setProgressAsync(Data.Builder().putInt("progress", countUpload).build()) - countUpload++ - uploadContribution(contribution = contribution) - } - }.collect() + }.collect() - //Dismiss the global notification - notificationManager?.cancel( - PROCESSING_UPLOADS_NOTIFICATION_TAG, - PROCESSING_UPLOADS_NOTIFICATION_ID - ) + //Dismiss the global notification + notificationManager?.cancel( + PROCESSING_UPLOADS_NOTIFICATION_TAG, + PROCESSING_UPLOADS_NOTIFICATION_ID + ) + } + //TODO make this smart, think of handling retries in the future + return Result.success() + } catch (e: Exception) { + return Result.retry() } - //TODO make this smart, think of handling retries in the future - 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 */ @@ -379,6 +409,7 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : contribution.media.captions ) if (null != revisionID) { + contribution.wikidataUpdateWasSuccessful = true showSuccessNotification(contribution) } }catch (exception: Exception){ @@ -540,7 +571,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/res/values/strings.xml b/app/src/main/res/values/strings.xml index e402e0c1ca..647d20e81b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -62,6 +62,9 @@ Settings Upload to Commons + Upload in progress + Wikidata edit failed + Wikidata edit failed for the contribution %s. Please edit its caption and depiction. Username Password Log in to your Commons Beta account @@ -75,6 +78,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)