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
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -25,6 +25,7 @@ data class Contribution constructor(
|
|||
val decimalCoords: String? = null,
|
||||
var dateCreatedSource: String? = null,
|
||||
var wikidataPlace: WikidataPlace? = null,
|
||||
var chunkInfo: ChunkInfo? = null,
|
||||
/**
|
||||
* @return array list of entityids for the depictions
|
||||
*/
|
||||
|
|
@ -36,7 +37,8 @@ data class Contribution constructor(
|
|||
var mimeType: String? = null,
|
||||
val localUri: Uri? = null,
|
||||
var dataLength: Long = 0,
|
||||
var dateCreated: Date? = null
|
||||
var dateCreated: Date? = null,
|
||||
var dateModified: Date? = null
|
||||
) : Parcelable {
|
||||
|
||||
fun completeWith(media: Media): Contribution {
|
||||
|
|
@ -68,6 +70,7 @@ data class Contribution constructor(
|
|||
const val STATE_FAILED = 1
|
||||
const val STATE_QUEUED = 2
|
||||
const val STATE_IN_PROGRESS = 3
|
||||
const val STATE_PAUSED = 4
|
||||
|
||||
/**
|
||||
* Formatting captions to the Wikibase format for sending labels
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import androidx.room.Transaction;
|
|||
import androidx.room.Update;
|
||||
import io.reactivex.Completable;
|
||||
import io.reactivex.Single;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@Dao
|
||||
|
|
@ -23,7 +25,10 @@ public abstract class ContributionDao {
|
|||
|
||||
public Completable save(final Contribution contribution) {
|
||||
return Completable
|
||||
.fromAction(() -> saveSynchronous(contribution));
|
||||
.fromAction(() -> {
|
||||
contribution.setDateModified(Calendar.getInstance().getTime());
|
||||
saveSynchronous(contribution);
|
||||
});
|
||||
}
|
||||
|
||||
@Transaction
|
||||
|
|
@ -67,6 +72,9 @@ public abstract class ContributionDao {
|
|||
|
||||
public Completable update(final Contribution contribution) {
|
||||
return Completable
|
||||
.fromAction(() -> updateSynchronous(contribution));
|
||||
.fromAction(() -> {
|
||||
contribution.setDateModified(Calendar.getInstance().getTime());
|
||||
updateSynchronous(contribution);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,8 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
|
|||
ImageButton retryButton;
|
||||
@BindView(R.id.cancelButton)
|
||||
ImageButton cancelButton;
|
||||
@BindView(R.id.pauseResumeButton)
|
||||
ImageButton pauseResumeButton;
|
||||
|
||||
|
||||
private int position;
|
||||
|
|
@ -93,7 +95,11 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
|
|||
case Contribution.STATE_IN_PROGRESS:
|
||||
stateView.setVisibility(View.GONE);
|
||||
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 transferred = contribution.getTransferred();
|
||||
if (transferred == 0 || transferred >= total) {
|
||||
|
|
@ -102,10 +108,23 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
|
|||
progressView.setProgress((int) (((double) transferred / (double) total) * 100));
|
||||
}
|
||||
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:
|
||||
stateView.setVisibility(View.VISIBLE);
|
||||
stateView.setText(R.string.contribution_state_failed);
|
||||
progressView.setVisibility(View.GONE);
|
||||
cancelButton.setVisibility(View.VISIBLE);
|
||||
retryButton.setVisibility(View.VISIBLE);
|
||||
pauseResumeButton.setVisibility(View.GONE);
|
||||
imageOptions.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
}
|
||||
|
|
@ -187,4 +206,34 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
|
|||
public void wikipediaButtonClicked() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
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_PAUSED;
|
||||
import static fr.free.nrw.commons.contributions.MainActivity.CONTRIBUTIONS_TAB_POSITION;
|
||||
import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween;
|
||||
|
||||
|
|
@ -454,7 +455,7 @@ public class ContributionsFragment
|
|||
@Override
|
||||
public void retryUpload(Contribution contribution) {
|
||||
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);
|
||||
Timber.d("Restarting for %s", contribution.toString());
|
||||
} 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
|
||||
* mediaDetailPagerFragment, and preserve previous state in back stack. Called when user selects a
|
||||
|
|
|
|||
|
|
@ -78,5 +78,9 @@ public class ContributionsListAdapter extends
|
|||
void openMediaDetail(int contribution);
|
||||
|
||||
void addImageToWikipedia(Contribution contribution);
|
||||
|
||||
void pauseUpload(Contribution contribution);
|
||||
|
||||
void resumeUpload(Contribution contribution);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import java.util.Locale;
|
|||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import org.wikipedia.dataclient.WikiSite;
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* Created by root on 01.06.2018.
|
||||
|
|
@ -90,6 +91,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
|
|||
private final int SPAN_COUNT_PORTRAIT = 1;
|
||||
|
||||
|
||||
@Override
|
||||
public View onCreateView(
|
||||
final LayoutInflater inflater, @Nullable final ViewGroup container,
|
||||
@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.
|
||||
*/
|
||||
@Override
|
||||
public void showWelcomeTip(final boolean shouldShow) {
|
||||
noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE);
|
||||
}
|
||||
|
|
@ -201,10 +204,12 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
|
|||
*
|
||||
* @param shouldShow True when contributions list should be hidden.
|
||||
*/
|
||||
@Override
|
||||
public void showProgress(final boolean shouldShow) {
|
||||
progressBar.setVisibility(shouldShow ? VISIBLE : GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showNoContributionsUI(final boolean shouldShow) {
|
||||
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
|
||||
*
|
||||
|
|
@ -311,6 +334,8 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
|
|||
|
||||
void retryUpload(Contribution contribution);
|
||||
|
||||
void pauseUpload(Contribution contribution);
|
||||
|
||||
void showDetail(int position);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package fr.free.nrw.commons.contributions;
|
|||
import androidx.paging.DataSource.Factory;
|
||||
import io.reactivex.Completable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import fr.free.nrw.commons.contributions.ContributionDao
|
|||
* 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)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun contributionDao(): ContributionDao
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import androidx.room.TypeConverter;
|
|||
import com.google.gson.Gson;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
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.location.LatLng;
|
||||
import fr.free.nrw.commons.upload.WikidataPlace;
|
||||
|
|
@ -82,6 +83,16 @@ public class Converters {
|
|||
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
|
||||
public static String depictionListToString(List<DepictedItem> depictedItems) {
|
||||
return writeObjectToString(depictedItems);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package fr.free.nrw.commons.upload
|
||||
|
||||
import fr.free.nrw.commons.contributions.ChunkInfo
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.RequestBody
|
||||
import okio.*
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -6,12 +6,16 @@ import android.content.Context;
|
|||
import android.net.Uri;
|
||||
import androidx.annotation.Nullable;
|
||||
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.upload.UploadService.NotificationUpdateProgressListener;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.functions.Consumer;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Date;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
|
@ -27,10 +31,17 @@ public class UploadClient {
|
|||
|
||||
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 CsrfTokenClient csrfTokenClient;
|
||||
private final PageContentsCreator pageContentsCreator;
|
||||
private final FileUtilsWrapper fileUtilsWrapper;
|
||||
private boolean pauseUploads = false;
|
||||
|
||||
private final CompositeDisposable compositeDisposable = new CompositeDisposable();
|
||||
|
||||
@Inject
|
||||
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
|
||||
* of large files easier. Also, it will be useful in supporting pause/resume of uploads
|
||||
*/
|
||||
Observable<UploadResult> uploadFileToStash(
|
||||
final Context context, final String filename, final File file,
|
||||
Observable<StashUploadResult> uploadFileToStash(
|
||||
final Context context, final String filename, final Contribution contribution,
|
||||
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 MediaType mediaType = MediaType
|
||||
.parse(FileUtils.getMimeType(context, Uri.parse(file.getPath())));
|
||||
|
||||
final long[] offset = {0};
|
||||
final String[] fileKey = {null};
|
||||
final AtomicReference<UploadResult> result = new AtomicReference<>();
|
||||
fileChunks.blockingForEach(chunkFile -> {
|
||||
final AtomicInteger index = new AtomicInteger();
|
||||
final AtomicReference<ChunkInfo> chunkInfo = new AtomicReference<>();
|
||||
if (contribution.getChunkInfo() != null && isStashValid(contribution)) {
|
||||
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
|
||||
.create(mediaType, chunkFile);
|
||||
final CountingRequestBody countingRequestBody = new CountingRequestBody(requestBody,
|
||||
notificationUpdater::onProgress, offset[0], file.length());
|
||||
uploadChunkToStash(filename,
|
||||
notificationUpdater::onProgress, offset,
|
||||
file.length());
|
||||
|
||||
compositeDisposable.add(uploadChunkToStash(filename,
|
||||
file.length(),
|
||||
offset[0],
|
||||
fileKey[0],
|
||||
countingRequestBody).blockingSubscribe(uploadResult -> {
|
||||
result.set(uploadResult);
|
||||
offset[0] = uploadResult.getOffset();
|
||||
fileKey[0] = uploadResult.getFilekey();
|
||||
});
|
||||
});
|
||||
return Observable.just(result.get());
|
||||
offset,
|
||||
filekey,
|
||||
countingRequestBody).subscribe(uploadResult -> {
|
||||
chunkInfo.set(new ChunkInfo(uploadResult, index.incrementAndGet()));
|
||||
notificationUpdater.onChunkUploaded(contribution, chunkInfo.get());
|
||||
}, throwable -> {
|
||||
Timber.e(throwable, "Error occurred in uploading chunk");
|
||||
}));
|
||||
}));
|
||||
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
|
||||
private RequestBody toRequestBody(@Nullable final String value) {
|
||||
return value == null ? null : RequestBody.create(okhttp3.MultipartBody.FORM, value);
|
||||
|
|
|
|||
|
|
@ -1,18 +1,47 @@
|
|||
package fr.free.nrw.commons.upload
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import org.wikipedia.gallery.ImageInfo
|
||||
|
||||
private const val RESULT_SUCCESS = "Success"
|
||||
|
||||
|
||||
data class UploadResult(
|
||||
val result: String,
|
||||
val filekey: String,
|
||||
val offset: Int,
|
||||
val filename: String,
|
||||
val sessionkey: String,
|
||||
val imageinfo: ImageInfo
|
||||
) {
|
||||
val filename: String
|
||||
) : Parcelable {
|
||||
constructor(parcel: Parcel) : this(
|
||||
parcel.readString(),
|
||||
parcel.readString(),
|
||||
parcel.readInt(),
|
||||
parcel.readString()
|
||||
) {
|
||||
}
|
||||
|
||||
fun isSuccessful(): Boolean = result == RESULT_SUCCESS
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import android.app.PendingIntent;
|
|||
import android.content.ContentResolver;
|
||||
import android.content.Intent;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.net.Uri;
|
||||
import android.os.Binder;
|
||||
import android.os.Bundle;
|
||||
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.R;
|
||||
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.ContributionDao;
|
||||
import fr.free.nrw.commons.contributions.MainActivity;
|
||||
|
|
@ -28,8 +28,8 @@ import io.reactivex.Scheduler;
|
|||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.processors.PublishProcessor;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
|
|
@ -76,6 +76,7 @@ public class UploadService extends CommonsDaggerService {
|
|||
// Seriously, Android?
|
||||
public static final int NOTIFICATION_UPLOAD_IN_PROGRESS = 1;
|
||||
public static final int NOTIFICATION_UPLOAD_FAILED = 3;
|
||||
public static final int NOTIFICATION_UPLOAD_PAUSED = 4;
|
||||
|
||||
protected class NotificationUpdateProgressListener {
|
||||
|
||||
|
|
@ -119,6 +120,24 @@ public class UploadService extends CommonsDaggerService {
|
|||
.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
|
||||
|
|
@ -208,13 +227,11 @@ public class UploadService extends CommonsDaggerService {
|
|||
|
||||
@SuppressLint("CheckResult")
|
||||
private void uploadContribution(Contribution contribution) {
|
||||
Uri localUri = contribution.getLocalUri();
|
||||
if (localUri == null || localUri.getPath() == null) {
|
||||
if (contribution.getLocalUri() == null || contribution.getLocalUri().getPath() == null) {
|
||||
Timber.d("localUri/path is null");
|
||||
return;
|
||||
}
|
||||
String notificationTag = localUri.toString();
|
||||
File localFile = new File(localUri.getPath());
|
||||
String notificationTag = contribution.getLocalUri().toString();
|
||||
|
||||
Timber.d("Before execution!");
|
||||
final Media media = contribution.getMedia();
|
||||
|
|
@ -243,7 +260,7 @@ public class UploadService extends CommonsDaggerService {
|
|||
|
||||
Observable.fromCallable(() -> "Temp_" + contribution.hashCode() + filename)
|
||||
.flatMap(stashFilename -> uploadClient
|
||||
.uploadFileToStash(getApplicationContext(), stashFilename, localFile,
|
||||
.uploadFileToStash(getApplicationContext(), stashFilename, contribution,
|
||||
notificationUpdater))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(Schedulers.io())
|
||||
|
|
@ -265,7 +282,7 @@ public class UploadService extends CommonsDaggerService {
|
|||
|
||||
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);
|
||||
String uniqueFilename = findUniqueFilename(filename);
|
||||
unfinishedUploads.add(uniqueFilename);
|
||||
|
|
@ -273,7 +290,11 @@ public class UploadService extends CommonsDaggerService {
|
|||
getApplicationContext(),
|
||||
contribution,
|
||||
uniqueFilename,
|
||||
uploadStash.getFilekey());
|
||||
uploadStash.getFileKey());
|
||||
} else if (uploadStash.getState() == StashUploadState.PAUSED) {
|
||||
Timber.d("Contribution upload paused");
|
||||
showPausedNotification(contribution);
|
||||
return Observable.never();
|
||||
} else {
|
||||
Timber.d("Contribution upload failed. Wikidata entity won't be edited");
|
||||
showFailedNotification(contribution);
|
||||
|
|
@ -308,7 +329,8 @@ public class UploadService extends CommonsDaggerService {
|
|||
.add(wikidataEditService.addDepictionsAndCaptions(uploadResult, contribution));
|
||||
WikidataPlace wikidataPlace = contribution.getWikidataPlace();
|
||||
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);
|
||||
}
|
||||
|
|
@ -317,7 +339,10 @@ public class UploadService extends CommonsDaggerService {
|
|||
compositeDisposable.add(mediaClient.getMedia("File:" + uploadResult.getFilename())
|
||||
.map(contribution::completeWith)
|
||||
.flatMapCompletable(
|
||||
newContribution -> contributionDao.saveAndDelete(contribution, newContribution))
|
||||
newContribution -> {
|
||||
newContribution.setDateModified(new Date());
|
||||
return contributionDao.saveAndDelete(contribution, newContribution);
|
||||
})
|
||||
.subscribe());
|
||||
}
|
||||
|
||||
|
|
@ -341,6 +366,24 @@ public class UploadService extends CommonsDaggerService {
|
|||
.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 {
|
||||
String sequenceFileName;
|
||||
for (int sequenceNumber = 1; true; sequenceNumber++) {
|
||||
|
|
|
|||
15
app/src/main/res/drawable-anydpi-v24/pause_icon.xml
Normal 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>
|
||||
15
app/src/main/res/drawable-anydpi-v24/play_icon.xml
Normal 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>
|
||||
BIN
app/src/main/res/drawable-hdpi/pause_icon.png
Normal file
|
After Width: | Height: | Size: 139 B |
BIN
app/src/main/res/drawable-hdpi/play_icon.png
Normal file
|
After Width: | Height: | Size: 272 B |
BIN
app/src/main/res/drawable-mdpi/pause_icon.png
Normal file
|
After Width: | Height: | Size: 102 B |
BIN
app/src/main/res/drawable-mdpi/play_icon.png
Normal file
|
After Width: | Height: | Size: 189 B |
BIN
app/src/main/res/drawable-xhdpi/pause_icon.png
Normal file
|
After Width: | Height: | Size: 149 B |
BIN
app/src/main/res/drawable-xhdpi/play_icon.png
Normal file
|
After Width: | Height: | Size: 299 B |
BIN
app/src/main/res/drawable-xxhdpi/pause_icon.png
Normal file
|
After Width: | Height: | Size: 196 B |
BIN
app/src/main/res/drawable-xxhdpi/play_icon.png
Normal file
|
After Width: | Height: | Size: 442 B |
|
|
@ -1,130 +1,130 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:fresco="http://schemas.android.com/tools"
|
||||
android:orientation="vertical"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:fresco="http://schemas.android.com/tools"
|
||||
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_height="wrap_content"
|
||||
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: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"
|
||||
/>
|
||||
android:layout_gravity="center|bottom"
|
||||
android:background="#AA000000"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<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_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_gravity="center|bottom"
|
||||
android:background="#AA000000"
|
||||
>
|
||||
android:indeterminateOnly="false"
|
||||
android:max="100"
|
||||
android:visibility="gone" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="@dimen/dimen_0"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center|bottom"
|
||||
android:orientation="vertical"
|
||||
android:layout_weight="5"
|
||||
android:padding="@dimen/small_gap"
|
||||
>
|
||||
<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
|
||||
android:id="@+id/contributionState"
|
||||
style="?android:textAppearanceSmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="#FFFFFFFF"
|
||||
android:visibility="gone" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/contributionState"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="?android:textAppearanceSmall"
|
||||
android:textColor="#FFFFFFFF"
|
||||
android:visibility="gone"
|
||||
/>
|
||||
|
||||
<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>
|
||||
<TextView
|
||||
android:id="@+id/contributionTitle"
|
||||
style="?android:textAppearanceMedium"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2"
|
||||
android:textColor="#FFFFFFFF" />
|
||||
|
||||
</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>
|
||||
|
|
@ -60,7 +60,9 @@
|
|||
<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_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_paused_notification_subtitle">Tap to view</string>
|
||||
<string name="title_activity_contributions">My Recent Uploads</string>
|
||||
<string name="contribution_state_queued">Queued</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 <a href="https://en.wikipedia.org/wiki/Wikipedia:Manual_of_Style/Images#How_to_place_an_image">here</a>.</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="pause">pause</string>
|
||||
<string name="resume">resume</string>
|
||||
<string name="paused">Paused</string>
|
||||
</resources>
|
||||
|
|
|
|||