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,
|
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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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.*
|
||||||
|
|
|
||||||
|
|
@ -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 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);
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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++) {
|
||||||
|
|
|
||||||
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"?>
|
<?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>
|
||||||
|
|
@ -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 <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_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="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>
|
||||||
|
|
|
||||||