With pause and resume for uploads (#3858)

* With pause and resume for uploads

* Dispose current upload

* Make pause and resume work

* Check stash validity

* With java docs

* minor
This commit is contained in:
Vivek Maskara 2020-07-07 07:32:41 -07:00 committed by GitHub
parent e40e9690dd
commit 6f4c60b5ff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 483 additions and 155 deletions

View file

@ -0,0 +1,36 @@
package fr.free.nrw.commons.contributions
import android.os.Parcel
import android.os.Parcelable
import fr.free.nrw.commons.upload.UploadResult
import kotlinx.android.parcel.Parcelize
data class ChunkInfo(
val uploadResult: UploadResult,
val lastChunkIndex: Int
) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.readParcelable(UploadResult::class.java.classLoader),
parcel.readInt()
) {
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeParcelable(uploadResult, flags)
parcel.writeInt(lastChunkIndex)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<ChunkInfo> {
override fun createFromParcel(parcel: Parcel): ChunkInfo {
return ChunkInfo(parcel)
}
override fun newArray(size: Int): Array<ChunkInfo?> {
return arrayOfNulls(size)
}
}
}

View file

@ -25,6 +25,7 @@ data class Contribution constructor(
val decimalCoords: String? = null, val decimalCoords: String? = null,
var dateCreatedSource: String? = null, var dateCreatedSource: String? = null,
var wikidataPlace: WikidataPlace? = null, var wikidataPlace: WikidataPlace? = null,
var chunkInfo: ChunkInfo? = null,
/** /**
* @return array list of entityids for the depictions * @return array list of entityids for the depictions
*/ */
@ -36,7 +37,8 @@ data class Contribution constructor(
var mimeType: String? = null, var mimeType: String? = null,
val localUri: Uri? = null, val localUri: Uri? = null,
var dataLength: Long = 0, var dataLength: Long = 0,
var dateCreated: Date? = null var dateCreated: Date? = null,
var dateModified: Date? = null
) : Parcelable { ) : Parcelable {
fun completeWith(media: Media): Contribution { fun completeWith(media: Media): Contribution {
@ -68,6 +70,7 @@ data class Contribution constructor(
const val STATE_FAILED = 1 const val STATE_FAILED = 1
const val STATE_QUEUED = 2 const val STATE_QUEUED = 2
const val STATE_IN_PROGRESS = 3 const val STATE_IN_PROGRESS = 3
const val STATE_PAUSED = 4
/** /**
* Formatting captions to the Wikibase format for sending labels * Formatting captions to the Wikibase format for sending labels

View file

@ -10,6 +10,8 @@ import androidx.room.Transaction;
import androidx.room.Update; import androidx.room.Update;
import io.reactivex.Completable; import io.reactivex.Completable;
import io.reactivex.Single; import io.reactivex.Single;
import java.util.Calendar;
import java.util.Date;
import java.util.List; import java.util.List;
@Dao @Dao
@ -23,7 +25,10 @@ public abstract class ContributionDao {
public Completable save(final Contribution contribution) { public Completable save(final Contribution contribution) {
return Completable return Completable
.fromAction(() -> saveSynchronous(contribution)); .fromAction(() -> {
contribution.setDateModified(Calendar.getInstance().getTime());
saveSynchronous(contribution);
});
} }
@Transaction @Transaction
@ -67,6 +72,9 @@ public abstract class ContributionDao {
public Completable update(final Contribution contribution) { public Completable update(final Contribution contribution) {
return Completable return Completable
.fromAction(() -> updateSynchronous(contribution)); .fromAction(() -> {
contribution.setDateModified(Calendar.getInstance().getTime());
updateSynchronous(contribution);
});
} }
} }

View file

@ -43,6 +43,8 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
ImageButton retryButton; ImageButton retryButton;
@BindView(R.id.cancelButton) @BindView(R.id.cancelButton)
ImageButton cancelButton; ImageButton cancelButton;
@BindView(R.id.pauseResumeButton)
ImageButton pauseResumeButton;
private int position; private int position;
@ -93,7 +95,11 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
case Contribution.STATE_IN_PROGRESS: case Contribution.STATE_IN_PROGRESS:
stateView.setVisibility(View.GONE); stateView.setVisibility(View.GONE);
progressView.setVisibility(View.VISIBLE); progressView.setVisibility(View.VISIBLE);
imageOptions.setVisibility(View.GONE); addToWikipediaButton.setVisibility(View.GONE);
pauseResumeButton.setVisibility(View.VISIBLE);
cancelButton.setVisibility(View.GONE);
retryButton.setVisibility(View.GONE);
imageOptions.setVisibility(View.VISIBLE);
final long total = contribution.getDataLength(); final long total = contribution.getDataLength();
final long transferred = contribution.getTransferred(); final long transferred = contribution.getTransferred();
if (transferred == 0 || transferred >= total) { if (transferred == 0 || transferred >= total) {
@ -102,10 +108,23 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
progressView.setProgress((int) (((double) transferred / (double) total) * 100)); progressView.setProgress((int) (((double) transferred / (double) total) * 100));
} }
break; break;
case Contribution.STATE_PAUSED:
stateView.setVisibility(View.VISIBLE);
stateView.setText(R.string.paused);
setResume();
progressView.setVisibility(View.GONE);
cancelButton.setVisibility(View.GONE);
retryButton.setVisibility(View.GONE);
pauseResumeButton.setVisibility(View.VISIBLE);
imageOptions.setVisibility(View.VISIBLE);
break;
case Contribution.STATE_FAILED: case Contribution.STATE_FAILED:
stateView.setVisibility(View.VISIBLE); stateView.setVisibility(View.VISIBLE);
stateView.setText(R.string.contribution_state_failed); stateView.setText(R.string.contribution_state_failed);
progressView.setVisibility(View.GONE); progressView.setVisibility(View.GONE);
cancelButton.setVisibility(View.VISIBLE);
retryButton.setVisibility(View.VISIBLE);
pauseResumeButton.setVisibility(View.GONE);
imageOptions.setVisibility(View.VISIBLE); imageOptions.setVisibility(View.VISIBLE);
break; break;
} }
@ -187,4 +206,34 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
public void wikipediaButtonClicked() { public void wikipediaButtonClicked() {
callback.addImageToWikipedia(contribution); callback.addImageToWikipedia(contribution);
} }
/**
* Triggers a callback for pause/resume
*/
@OnClick(R.id.pauseResumeButton)
public void onPauseResumeButtonClicked() {
if (pauseResumeButton.getTag().toString().equals("pause")) {
callback.pauseUpload(contribution);
setResume();
} else {
callback.resumeUpload(contribution);
setPaused();
}
}
/**
* Update pause/resume button to show pause state
*/
private void setPaused() {
pauseResumeButton.setImageResource(R.drawable.pause_icon);
pauseResumeButton.setTag(R.string.pause);
}
/**
* Update pause/resume button to show resume state
*/
private void setResume() {
pauseResumeButton.setImageResource(R.drawable.play_icon);
pauseResumeButton.setTag(R.string.resume);
}
} }

View file

@ -1,6 +1,7 @@
package fr.free.nrw.commons.contributions; package fr.free.nrw.commons.contributions;
import static fr.free.nrw.commons.contributions.Contribution.STATE_FAILED; import static fr.free.nrw.commons.contributions.Contribution.STATE_FAILED;
import static fr.free.nrw.commons.contributions.Contribution.STATE_PAUSED;
import static fr.free.nrw.commons.contributions.MainActivity.CONTRIBUTIONS_TAB_POSITION; import static fr.free.nrw.commons.contributions.MainActivity.CONTRIBUTIONS_TAB_POSITION;
import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween; import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween;
@ -454,7 +455,7 @@ public class ContributionsFragment
@Override @Override
public void retryUpload(Contribution contribution) { public void retryUpload(Contribution contribution) {
if (NetworkUtils.isInternetConnectionEstablished(getContext())) { if (NetworkUtils.isInternetConnectionEstablished(getContext())) {
if (contribution.getState() == STATE_FAILED && null != uploadService) { if (contribution.getState() == STATE_FAILED || contribution.getState() == STATE_PAUSED && null != uploadService) {
uploadService.queue(contribution); uploadService.queue(contribution);
Timber.d("Restarting for %s", contribution.toString()); Timber.d("Restarting for %s", contribution.toString());
} else { } else {
@ -466,6 +467,15 @@ public class ContributionsFragment
} }
/**
* Pauses the upload
* @param contribution
*/
@Override
public void pauseUpload(Contribution contribution) {
uploadService.pauseUpload(contribution);
}
/** /**
* Replace whatever is in the current contributionsFragmentContainer view with * Replace whatever is in the current contributionsFragmentContainer view with
* mediaDetailPagerFragment, and preserve previous state in back stack. Called when user selects a * mediaDetailPagerFragment, and preserve previous state in back stack. Called when user selects a

View file

@ -78,5 +78,9 @@ public class ContributionsListAdapter extends
void openMediaDetail(int contribution); void openMediaDetail(int contribution);
void addImageToWikipedia(Contribution contribution); void addImageToWikipedia(Contribution contribution);
void pauseUpload(Contribution contribution);
void resumeUpload(Contribution contribution);
} }
} }

View file

@ -36,6 +36,7 @@ import java.util.Locale;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
import org.wikipedia.dataclient.WikiSite; import org.wikipedia.dataclient.WikiSite;
import timber.log.Timber;
/** /**
* Created by root on 01.06.2018. * Created by root on 01.06.2018.
@ -90,6 +91,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
private final int SPAN_COUNT_PORTRAIT = 1; private final int SPAN_COUNT_PORTRAIT = 1;
@Override
public View onCreateView( public View onCreateView(
final LayoutInflater inflater, @Nullable final ViewGroup container, final LayoutInflater inflater, @Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) { @Nullable final Bundle savedInstanceState) {
@ -192,6 +194,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
/** /**
* Shows welcome message if user has no contributions yet i.e. new user. * Shows welcome message if user has no contributions yet i.e. new user.
*/ */
@Override
public void showWelcomeTip(final boolean shouldShow) { public void showWelcomeTip(final boolean shouldShow) {
noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE); noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE);
} }
@ -201,10 +204,12 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
* *
* @param shouldShow True when contributions list should be hidden. * @param shouldShow True when contributions list should be hidden.
*/ */
@Override
public void showProgress(final boolean shouldShow) { public void showProgress(final boolean shouldShow) {
progressBar.setVisibility(shouldShow ? VISIBLE : GONE); progressBar.setVisibility(shouldShow ? VISIBLE : GONE);
} }
@Override
public void showNoContributionsUI(final boolean shouldShow) { public void showNoContributionsUI(final boolean shouldShow) {
noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE); noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE);
} }
@ -263,6 +268,24 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
}); });
} }
/**
* Pauses the current upload
* @param contribution
*/
@Override
public void pauseUpload(Contribution contribution) {
callback.pauseUpload(contribution);
}
/**
* Resumes the current upload
* @param contribution
*/
@Override
public void resumeUpload(Contribution contribution) {
callback.retryUpload(contribution);
}
/** /**
* Display confirmation dialog with instructions when the user tries to add image to wikipedia * Display confirmation dialog with instructions when the user tries to add image to wikipedia
* *
@ -311,6 +334,8 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
void retryUpload(Contribution contribution); void retryUpload(Contribution contribution);
void pauseUpload(Contribution contribution);
void showDetail(int position); void showDetail(int position);
} }
} }

View file

@ -3,6 +3,7 @@ package fr.free.nrw.commons.contributions;
import androidx.paging.DataSource.Factory; import androidx.paging.DataSource.Factory;
import io.reactivex.Completable; import io.reactivex.Completable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date;
import java.util.List; import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;

View file

@ -10,7 +10,7 @@ import fr.free.nrw.commons.contributions.ContributionDao
* The database for accessing the respective DAOs * The database for accessing the respective DAOs
* *
*/ */
@Database(entities = [Contribution::class], version = 4, exportSchema = false) @Database(entities = [Contribution::class], version = 5, exportSchema = false)
@TypeConverters(Converters::class) @TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
abstract fun contributionDao(): ContributionDao abstract fun contributionDao(): ContributionDao

View file

@ -5,6 +5,7 @@ import androidx.room.TypeConverter;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.contributions.ChunkInfo;
import fr.free.nrw.commons.di.ApplicationlessInjection; import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.upload.WikidataPlace; import fr.free.nrw.commons.upload.WikidataPlace;
@ -82,6 +83,16 @@ public class Converters {
return readObjectFromString(wikidataPlace, WikidataPlace.class); return readObjectFromString(wikidataPlace, WikidataPlace.class);
} }
@TypeConverter
public static String chunkInfoToString(ChunkInfo chunkInfo) {
return writeObjectToString(chunkInfo);
}
@TypeConverter
public static ChunkInfo stringToChunkInfo(String chunkInfo) {
return readObjectFromString(chunkInfo, ChunkInfo.class);
}
@TypeConverter @TypeConverter
public static String depictionListToString(List<DepictedItem> depictedItems) { public static String depictionListToString(List<DepictedItem> depictedItems) {
return writeObjectToString(depictedItems); return writeObjectToString(depictedItems);

View file

@ -1,5 +1,6 @@
package fr.free.nrw.commons.upload package fr.free.nrw.commons.upload
import fr.free.nrw.commons.contributions.ChunkInfo
import okhttp3.MediaType import okhttp3.MediaType
import okhttp3.RequestBody import okhttp3.RequestBody
import okio.* import okio.*

View file

@ -0,0 +1,12 @@
package fr.free.nrw.commons.upload
data class StashUploadResult(
val state: StashUploadState,
val fileKey: String?
)
enum class StashUploadState {
SUCCESS,
PAUSED,
FAILED
}

View file

@ -6,12 +6,16 @@ import android.content.Context;
import android.net.Uri; import android.net.Uri;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.contributions.ChunkInfo;
import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.upload.UploadService.NotificationUpdateProgressListener; import fr.free.nrw.commons.upload.UploadService.NotificationUpdateProgressListener;
import io.reactivex.Observable; import io.reactivex.Observable;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.functions.Consumer;
import java.io.File; import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.Date;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
@ -27,10 +31,17 @@ public class UploadClient {
private final int CHUNK_SIZE = 256 * 1024; // 256 KB private final int CHUNK_SIZE = 256 * 1024; // 256 KB
//This is maximum duration for which a stash is persisted on MediaWiki
// https://www.mediawiki.org/wiki/Manual:$wgUploadStashMaxAge
private final int MAX_CHUNK_AGE = 6 * 3600 * 1000; // 6 hours
private final UploadInterface uploadInterface; private final UploadInterface uploadInterface;
private final CsrfTokenClient csrfTokenClient; private final CsrfTokenClient csrfTokenClient;
private final PageContentsCreator pageContentsCreator; private final PageContentsCreator pageContentsCreator;
private final FileUtilsWrapper fileUtilsWrapper; private final FileUtilsWrapper fileUtilsWrapper;
private boolean pauseUploads = false;
private final CompositeDisposable compositeDisposable = new CompositeDisposable();
@Inject @Inject
public UploadClient(final UploadInterface uploadInterface, public UploadClient(final UploadInterface uploadInterface,
@ -47,32 +58,68 @@ public class UploadClient {
* Upload file to stash in chunks of specified size. Uploading files in chunks will make handling * Upload file to stash in chunks of specified size. Uploading files in chunks will make handling
* of large files easier. Also, it will be useful in supporting pause/resume of uploads * of large files easier. Also, it will be useful in supporting pause/resume of uploads
*/ */
Observable<UploadResult> uploadFileToStash( Observable<StashUploadResult> uploadFileToStash(
final Context context, final String filename, final File file, final Context context, final String filename, final Contribution contribution,
final NotificationUpdateProgressListener notificationUpdater) throws IOException { final NotificationUpdateProgressListener notificationUpdater) throws IOException {
pauseUploads = false;
File file = new File(contribution.getLocalUri().getPath());
final Observable<File> fileChunks = fileUtilsWrapper.getFileChunks(context, file, CHUNK_SIZE); final Observable<File> fileChunks = fileUtilsWrapper.getFileChunks(context, file, CHUNK_SIZE);
final MediaType mediaType = MediaType final MediaType mediaType = MediaType
.parse(FileUtils.getMimeType(context, Uri.parse(file.getPath()))); .parse(FileUtils.getMimeType(context, Uri.parse(file.getPath())));
final long[] offset = {0}; final AtomicInteger index = new AtomicInteger();
final String[] fileKey = {null}; final AtomicReference<ChunkInfo> chunkInfo = new AtomicReference<>();
final AtomicReference<UploadResult> result = new AtomicReference<>(); if (contribution.getChunkInfo() != null && isStashValid(contribution)) {
fileChunks.blockingForEach(chunkFile -> { chunkInfo.set(contribution.getChunkInfo());
}
compositeDisposable.add(fileChunks.forEach(chunkFile -> {
if (pauseUploads) {
return;
}
if (chunkInfo.get() != null && index.get() < chunkInfo.get().getLastChunkIndex()) {
index.getAndIncrement();
return;
}
final int offset =
chunkInfo.get() != null ? chunkInfo.get().getUploadResult().getOffset() : 0;
final String filekey =
chunkInfo.get() != null ? chunkInfo.get().getUploadResult().getFilekey() : null;
final RequestBody requestBody = RequestBody final RequestBody requestBody = RequestBody
.create(mediaType, chunkFile); .create(mediaType, chunkFile);
final CountingRequestBody countingRequestBody = new CountingRequestBody(requestBody, final CountingRequestBody countingRequestBody = new CountingRequestBody(requestBody,
notificationUpdater::onProgress, offset[0], file.length()); notificationUpdater::onProgress, offset,
uploadChunkToStash(filename, file.length());
compositeDisposable.add(uploadChunkToStash(filename,
file.length(), file.length(),
offset[0], offset,
fileKey[0], filekey,
countingRequestBody).blockingSubscribe(uploadResult -> { countingRequestBody).subscribe(uploadResult -> {
result.set(uploadResult); chunkInfo.set(new ChunkInfo(uploadResult, index.incrementAndGet()));
offset[0] = uploadResult.getOffset(); notificationUpdater.onChunkUploaded(contribution, chunkInfo.get());
fileKey[0] = uploadResult.getFilekey(); }, throwable -> {
}); Timber.e(throwable, "Error occurred in uploading chunk");
}); }));
return Observable.just(result.get()); }));
if (pauseUploads) {
return Observable.just(new StashUploadResult(StashUploadState.PAUSED, null));
} else if (chunkInfo.get() != null) {
return Observable.just(new StashUploadResult(StashUploadState.SUCCESS,
chunkInfo.get().getUploadResult().getFilekey()));
} else {
return Observable.just(new StashUploadResult(StashUploadState.FAILED, null));
}
}
/**
* Stash is valid for 6 hours. This function checks the validity of stash
* @param contribution
* @return
*/
private boolean isStashValid(Contribution contribution) {
return contribution.getDateModified()
.after(new Date(System.currentTimeMillis() - MAX_CHUNK_AGE));
} }
/** /**
@ -106,6 +153,20 @@ public class UploadClient {
} }
} }
/**
* Dispose the active disposable and sets the pause variable
*/
public void pauseUpload() {
pauseUploads = true;
if (!compositeDisposable.isDisposed()) {
compositeDisposable.dispose();
}
compositeDisposable.clear();
}
/**
* Converts string value to request body
*/
@Nullable @Nullable
private RequestBody toRequestBody(@Nullable final String value) { private RequestBody toRequestBody(@Nullable final String value) {
return value == null ? null : RequestBody.create(okhttp3.MultipartBody.FORM, value); return value == null ? null : RequestBody.create(okhttp3.MultipartBody.FORM, value);

View file

@ -1,18 +1,47 @@
package fr.free.nrw.commons.upload package fr.free.nrw.commons.upload
import android.os.Parcel
import android.os.Parcelable
import org.wikipedia.gallery.ImageInfo import org.wikipedia.gallery.ImageInfo
private const val RESULT_SUCCESS = "Success" private const val RESULT_SUCCESS = "Success"
data class UploadResult( data class UploadResult(
val result: String, val result: String,
val filekey: String, val filekey: String,
val offset: Int, val offset: Int,
val filename: String, val filename: String
val sessionkey: String, ) : Parcelable {
val imageinfo: ImageInfo constructor(parcel: Parcel) : this(
) { parcel.readString(),
parcel.readString(),
parcel.readInt(),
parcel.readString()
) {
}
fun isSuccessful(): Boolean = result == RESULT_SUCCESS fun isSuccessful(): Boolean = result == RESULT_SUCCESS
fun createCanonicalFileName() = "File:$filename" fun createCanonicalFileName() = "File:$filename"
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(result)
parcel.writeString(filekey)
parcel.writeInt(offset)
parcel.writeString(filename)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<UploadResult> {
override fun createFromParcel(parcel: Parcel): UploadResult {
return UploadResult(parcel)
}
override fun newArray(size: Int): Array<UploadResult?> {
return arrayOfNulls(size)
}
}
} }

View file

@ -5,7 +5,6 @@ import android.app.PendingIntent;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.content.Intent; import android.content.Intent;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Binder; import android.os.Binder;
import android.os.Bundle; import android.os.Bundle;
import android.os.IBinder; import android.os.IBinder;
@ -16,6 +15,7 @@ import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.Media; import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.contributions.ChunkInfo;
import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.contributions.ContributionDao; import fr.free.nrw.commons.contributions.ContributionDao;
import fr.free.nrw.commons.contributions.MainActivity; import fr.free.nrw.commons.contributions.MainActivity;
@ -28,8 +28,8 @@ import io.reactivex.Scheduler;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.processors.PublishProcessor; import io.reactivex.processors.PublishProcessor;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.Date;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.util.regex.Matcher; import java.util.regex.Matcher;
@ -76,6 +76,7 @@ public class UploadService extends CommonsDaggerService {
// Seriously, Android? // Seriously, Android?
public static final int NOTIFICATION_UPLOAD_IN_PROGRESS = 1; public static final int NOTIFICATION_UPLOAD_IN_PROGRESS = 1;
public static final int NOTIFICATION_UPLOAD_FAILED = 3; public static final int NOTIFICATION_UPLOAD_FAILED = 3;
public static final int NOTIFICATION_UPLOAD_PAUSED = 4;
protected class NotificationUpdateProgressListener { protected class NotificationUpdateProgressListener {
@ -119,6 +120,24 @@ public class UploadService extends CommonsDaggerService {
.subscribe()); .subscribe());
} }
public void onChunkUploaded(Contribution contribution, ChunkInfo chunkInfo) {
contribution.setChunkInfo(chunkInfo);
compositeDisposable.add(contributionDao.update(contribution)
.subscribeOn(ioThreadScheduler)
.subscribe());
}
}
/**
* Sets contribution state to paused and disposes the active disposable
* @param contribution
*/
public void pauseUpload(Contribution contribution) {
uploadClient.pauseUpload();
contribution.setState(Contribution.STATE_PAUSED);
compositeDisposable.add(contributionDao.update(contribution)
.subscribeOn(ioThreadScheduler)
.subscribe());
} }
@Override @Override
@ -208,13 +227,11 @@ public class UploadService extends CommonsDaggerService {
@SuppressLint("CheckResult") @SuppressLint("CheckResult")
private void uploadContribution(Contribution contribution) { private void uploadContribution(Contribution contribution) {
Uri localUri = contribution.getLocalUri(); if (contribution.getLocalUri() == null || contribution.getLocalUri().getPath() == null) {
if (localUri == null || localUri.getPath() == null) {
Timber.d("localUri/path is null"); Timber.d("localUri/path is null");
return; return;
} }
String notificationTag = localUri.toString(); String notificationTag = contribution.getLocalUri().toString();
File localFile = new File(localUri.getPath());
Timber.d("Before execution!"); Timber.d("Before execution!");
final Media media = contribution.getMedia(); final Media media = contribution.getMedia();
@ -243,7 +260,7 @@ public class UploadService extends CommonsDaggerService {
Observable.fromCallable(() -> "Temp_" + contribution.hashCode() + filename) Observable.fromCallable(() -> "Temp_" + contribution.hashCode() + filename)
.flatMap(stashFilename -> uploadClient .flatMap(stashFilename -> uploadClient
.uploadFileToStash(getApplicationContext(), stashFilename, localFile, .uploadFileToStash(getApplicationContext(), stashFilename, contribution,
notificationUpdater)) notificationUpdater))
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(Schedulers.io()) .observeOn(Schedulers.io())
@ -265,7 +282,7 @@ public class UploadService extends CommonsDaggerService {
Timber.d("Stash upload response 1 is %s", uploadStash.toString()); Timber.d("Stash upload response 1 is %s", uploadStash.toString());
if (uploadStash.isSuccessful()) { if (uploadStash.getState() == StashUploadState.SUCCESS) {
Timber.d("making sure of uniqueness of name: %s", filename); Timber.d("making sure of uniqueness of name: %s", filename);
String uniqueFilename = findUniqueFilename(filename); String uniqueFilename = findUniqueFilename(filename);
unfinishedUploads.add(uniqueFilename); unfinishedUploads.add(uniqueFilename);
@ -273,7 +290,11 @@ public class UploadService extends CommonsDaggerService {
getApplicationContext(), getApplicationContext(),
contribution, contribution,
uniqueFilename, uniqueFilename,
uploadStash.getFilekey()); uploadStash.getFileKey());
} else if (uploadStash.getState() == StashUploadState.PAUSED) {
Timber.d("Contribution upload paused");
showPausedNotification(contribution);
return Observable.never();
} else { } else {
Timber.d("Contribution upload failed. Wikidata entity won't be edited"); Timber.d("Contribution upload failed. Wikidata entity won't be edited");
showFailedNotification(contribution); showFailedNotification(contribution);
@ -308,7 +329,8 @@ public class UploadService extends CommonsDaggerService {
.add(wikidataEditService.addDepictionsAndCaptions(uploadResult, contribution)); .add(wikidataEditService.addDepictionsAndCaptions(uploadResult, contribution));
WikidataPlace wikidataPlace = contribution.getWikidataPlace(); WikidataPlace wikidataPlace = contribution.getWikidataPlace();
if (wikidataPlace != null && wikidataPlace.getImageValue() == null) { if (wikidataPlace != null && wikidataPlace.getImageValue() == null) {
wikidataEditService.createClaim(wikidataPlace, uploadResult.getFilename(), contribution.getMedia().getCaptions()); wikidataEditService.createClaim(wikidataPlace, uploadResult.getFilename(),
contribution.getMedia().getCaptions());
} }
saveCompletedContribution(contribution, uploadResult); saveCompletedContribution(contribution, uploadResult);
} }
@ -317,7 +339,10 @@ public class UploadService extends CommonsDaggerService {
compositeDisposable.add(mediaClient.getMedia("File:" + uploadResult.getFilename()) compositeDisposable.add(mediaClient.getMedia("File:" + uploadResult.getFilename())
.map(contribution::completeWith) .map(contribution::completeWith)
.flatMapCompletable( .flatMapCompletable(
newContribution -> contributionDao.saveAndDelete(contribution, newContribution)) newContribution -> {
newContribution.setDateModified(new Date());
return contributionDao.saveAndDelete(contribution, newContribution);
})
.subscribe()); .subscribe());
} }
@ -341,6 +366,24 @@ public class UploadService extends CommonsDaggerService {
.subscribe()); .subscribe());
} }
private void showPausedNotification(Contribution contribution) {
final String displayTitle = contribution.getMedia().getDisplayTitle();
curNotification.setTicker(getString(R.string.upload_paused_notification_title, displayTitle))
.setContentTitle(getString(R.string.upload_paused_notification_title, displayTitle))
.setContentText(getString(R.string.upload_paused_notification_subtitle))
.setProgress(0, 0, false)
.setOngoing(false);
notificationManager.notify(contribution.getLocalUri().toString(), NOTIFICATION_UPLOAD_PAUSED,
curNotification.build());
contribution.setState(Contribution.STATE_PAUSED);
compositeDisposable.add(contributionDao
.update(contribution)
.subscribeOn(ioThreadScheduler)
.subscribe());
}
private String findUniqueFilename(String fileName) throws IOException { private String findUniqueFilename(String fileName) throws IOException {
String sequenceFileName; String sequenceFileName;
for (int sequenceNumber = 1; true; sequenceNumber++) { for (int sequenceNumber = 1; true; sequenceNumber++) {

View file

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF">
<group android:scaleX="1.44427"
android:scaleY="1.44427"
android:translateX="-5.33124"
android:translateY="-5.33124">
<path
android:fillColor="@android:color/white"
android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z"/>
</group>
</vector>

View file

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF">
<group android:scaleX="1.44427"
android:scaleY="1.44427"
android:translateX="-7.4976454"
android:translateY="-5.33124">
<path
android:fillColor="@android:color/white"
android:pathData="M8,5v14l11,-7z"/>
</group>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

View file

@ -1,130 +1,130 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:fresco="http://schemas.android.com/tools" xmlns:fresco="http://schemas.android.com/tools"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/miniscule_margin"
android:paddingBottom="@dimen/dimen_0">
<TextView
android:id="@+id/contributionSequenceNumber"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:textColor="#33FFFFFF"
android:textSize="98sp"
android:typeface="serif" />
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/contributionImage"
android:layout_width="match_parent"
android:layout_height="@dimen/very_large_height"
android:background="?attr/mainBackground"
app:actualImageScaleType="centerCrop"
fresco:placeholderImage="@drawable/ic_image_black_24dp" />
<LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:padding="@dimen/miniscule_margin" android:layout_gravity="center|bottom"
android:paddingBottom="@dimen/dimen_0"> android:background="#AA000000"
android:orientation="horizontal">
<TextView
android:id="@+id/contributionSequenceNumber"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="98sp"
android:textColor="#33FFFFFF"
android:typeface="serif"
android:layout_gravity="end|bottom"
/>
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/contributionImage"
android:layout_width="match_parent"
android:layout_height="@dimen/very_large_height"
app:actualImageScaleType="centerCrop"
android:background="?attr/mainBackground"
fresco:placeholderImage="@drawable/ic_image_black_24dp"
/>
<LinearLayout <LinearLayout
android:layout_width="@dimen/dimen_0"
android:layout_height="wrap_content"
android:layout_gravity="center|bottom"
android:layout_weight="5"
android:orientation="vertical"
android:padding="@dimen/small_gap">
<ProgressBar
android:id="@+id/contributionProgress"
style="@style/ProgressBar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:indeterminateOnly="false"
android:layout_gravity="center|bottom" android:max="100"
android:background="#AA000000" android:visibility="gone" />
>
<LinearLayout <TextView
android:layout_width="@dimen/dimen_0" android:id="@+id/contributionState"
android:layout_height="wrap_content" style="?android:textAppearanceSmall"
android:layout_gravity="center|bottom" android:layout_width="wrap_content"
android:orientation="vertical" android:layout_height="wrap_content"
android:layout_weight="5" android:textColor="#FFFFFFFF"
android:padding="@dimen/small_gap" android:visibility="gone" />
>
<ProgressBar
android:id="@+id/contributionProgress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/ProgressBar"
android:indeterminateOnly="false"
android:max="100"
android:visibility="gone"
/>
<TextView <TextView
android:id="@+id/contributionState" android:id="@+id/contributionTitle"
android:layout_width="wrap_content" style="?android:textAppearanceMedium"
android:layout_height="wrap_content" android:layout_width="wrap_content"
style="?android:textAppearanceSmall" android:layout_height="wrap_content"
android:textColor="#FFFFFFFF" android:ellipsize="end"
android:visibility="gone" android:maxLines="2"
/> android:textColor="#FFFFFFFF" />
<TextView
android:id="@+id/contributionTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#FFFFFFFF"
style="?android:textAppearanceMedium"
android:maxLines="2"
android:ellipsize="end"
/>
</LinearLayout>
<RelativeLayout
android:id="@+id/image_options"
android:layout_width="@dimen/dimen_0"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_weight="2.6"
android:visibility="visible"
android:layout_gravity="right"
android:gravity="right"
android:padding="@dimen/tiny_gap"
>
<ImageButton
android:id="@+id/cancelButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_cancel_white"
android:text="@string/menu_cancel_upload"
android:background="@android:color/transparent"
android:padding="@dimen/activity_margin_horizontal"
android:layout_toStartOf="@id/retryButton"
android:layout_marginEnd="@dimen/tiny_padding"
/>
<ImageButton
android:id="@+id/retryButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_retry_white"
android:text="@string/menu_retry_upload"
android:background="@android:color/transparent"
android:padding="@dimen/activity_margin_horizontal"
android:layout_toStartOf="@id/wikipediaButton"
android:layout_marginEnd="@dimen/tiny_padding"
/>
<ImageButton
android:id="@+id/wikipediaButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_wikipedia"
android:text="@string/menu_cancel_upload"
android:background="@android:color/transparent"
android:visibility="visible"
android:layout_alignParentEnd="true"
android:padding="@dimen/activity_margin_horizontal"
android:layout_marginEnd="@dimen/tiny_padding"
/>
</RelativeLayout>
</LinearLayout> </LinearLayout>
<RelativeLayout
android:id="@+id/image_options"
android:layout_width="@dimen/dimen_0"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:layout_weight="2.6"
android:gravity="right"
android:orientation="horizontal"
android:padding="@dimen/tiny_gap"
android:visibility="visible">
<ImageButton
android:id="@+id/pauseResumeButton"
android:layout_width="@dimen/dimen_40"
android:layout_height="@dimen/dimen_40"
android:layout_marginEnd="@dimen/tiny_padding"
android:layout_toStartOf="@id/cancelButton"
android:background="@android:color/transparent"
android:tag="@string/pause"
app:srcCompat="@drawable/pause_icon" />
<ImageButton
android:id="@+id/cancelButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/tiny_padding"
android:layout_toStartOf="@id/retryButton"
android:background="@android:color/transparent"
android:padding="@dimen/activity_margin_horizontal"
android:src="@drawable/ic_cancel_white"
android:text="@string/menu_cancel_upload" />
<ImageButton
android:id="@+id/retryButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/tiny_padding"
android:layout_toStartOf="@id/wikipediaButton"
android:background="@android:color/transparent"
android:padding="@dimen/activity_margin_horizontal"
android:src="@drawable/ic_retry_white"
android:text="@string/menu_retry_upload" />
<ImageButton
android:id="@+id/wikipediaButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_marginEnd="@dimen/tiny_padding"
android:background="@android:color/transparent"
android:padding="@dimen/activity_margin_horizontal"
android:src="@drawable/ic_wikipedia"
android:text="@string/menu_cancel_upload"
android:visibility="visible" />
</RelativeLayout>
</LinearLayout>
</FrameLayout> </FrameLayout>

View file

@ -60,7 +60,9 @@
<string name="upload_progress_notification_title_in_progress">%1$s uploading</string> <string name="upload_progress_notification_title_in_progress">%1$s uploading</string>
<string name="upload_progress_notification_title_finishing">Finishing uploading %1$s</string> <string name="upload_progress_notification_title_finishing">Finishing uploading %1$s</string>
<string name="upload_failed_notification_title">Uploading %1$s failed</string> <string name="upload_failed_notification_title">Uploading %1$s failed</string>
<string name="upload_paused_notification_title">Uploading %1$s paused</string>
<string name="upload_failed_notification_subtitle">Tap to view</string> <string name="upload_failed_notification_subtitle">Tap to view</string>
<string name="upload_paused_notification_subtitle">Tap to view</string>
<string name="title_activity_contributions">My Recent Uploads</string> <string name="title_activity_contributions">My Recent Uploads</string>
<string name="contribution_state_queued">Queued</string> <string name="contribution_state_queued">Queued</string>
<string name="contribution_state_failed">Failed</string> <string name="contribution_state_failed">Failed</string>
@ -649,4 +651,7 @@ Upload your first media by tapping on the add button.</string>
<string name="wikipedia_instructions_step_6">6. Edit the wikitext for appropriate positioning, if necessary. For more information, see &lt;a href="https://en.wikipedia.org/wiki/Wikipedia:Manual_of_Style/Images#How_to_place_an_image"&gt;here&lt;/a&gt;.</string> <string name="wikipedia_instructions_step_6">6. Edit the wikitext for appropriate positioning, if necessary. For more information, see &lt;a href="https://en.wikipedia.org/wiki/Wikipedia:Manual_of_Style/Images#How_to_place_an_image"&gt;here&lt;/a&gt;.</string>
<string name="wikipedia_instructions_step_7">7. Publish the article</string> <string name="wikipedia_instructions_step_7">7. Publish the article</string>
<string name="copy_wikicode_to_clipboard">Copy wikicode to clipboard</string> <string name="copy_wikicode_to_clipboard">Copy wikicode to clipboard</string>
<string name="pause">pause</string>
<string name="resume">resume</string>
<string name="paused">Paused</string>
</resources> </resources>