With changes for limited connection mode (#3934)

* With changed for limited connection mode

* Java docs

* With minor fix

* Fix cosmetic issues

* Fix ANR

* Add Unit test
This commit is contained in:
Vivek Maskara 2020-09-25 05:57:22 -07:00 committed by GitHub
parent 59ee7b8df2
commit 66f6e2e648
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 389 additions and 15 deletions

View file

@ -36,6 +36,7 @@ import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.logging.FileLoggingTree; import fr.free.nrw.commons.logging.FileLoggingTree;
import fr.free.nrw.commons.logging.LogUtils; import fr.free.nrw.commons.logging.LogUtils;
import fr.free.nrw.commons.media.CustomOkHttpNetworkFetcher;
import fr.free.nrw.commons.settings.Prefs; import fr.free.nrw.commons.settings.Prefs;
import fr.free.nrw.commons.upload.FileUtils; import fr.free.nrw.commons.upload.FileUtils;
import fr.free.nrw.commons.utils.ConfigUtils; import fr.free.nrw.commons.utils.ConfigUtils;
@ -77,10 +78,19 @@ import timber.log.Timber;
) )
public class CommonsApplication extends MultiDexApplication { public class CommonsApplication extends MultiDexApplication {
@Inject SessionManager sessionManager;
@Inject DBOpenHelper dbOpenHelper;
@Inject @Named("default_preferences") JsonKvStore defaultPrefs; public static final String IS_LIMITED_CONNECTION_MODE_ENABLED = "is_limited_connection_mode_enabled";
@Inject
SessionManager sessionManager;
@Inject
DBOpenHelper dbOpenHelper;
@Inject
@Named("default_preferences")
JsonKvStore defaultPrefs;
@Inject
CustomOkHttpNetworkFetcher customOkHttpNetworkFetcher;
/** /**
* Constants begin * Constants begin
@ -147,6 +157,7 @@ public class CommonsApplication extends MultiDexApplication {
// Set DownsampleEnabled to True to downsample the image in case it's heavy // Set DownsampleEnabled to True to downsample the image in case it's heavy
ImagePipelineConfig config = ImagePipelineConfig.newBuilder(this) ImagePipelineConfig config = ImagePipelineConfig.newBuilder(this)
.setNetworkFetcher(customOkHttpNetworkFetcher)
.setDownsampleEnabled(true) .setDownsampleEnabled(true)
.build(); .build();
try { try {

View file

@ -84,6 +84,7 @@ data class Contribution constructor(
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 const val STATE_PAUSED = 4
const val STATE_QUEUED_LIMITED_CONNECTION_MODE=5
/** /**
* Formatting captions to the Wikibase format for sending labels * Formatting captions to the Wikibase format for sending labels

View file

@ -61,6 +61,9 @@ public abstract class ContributionDao {
@Query("SELECT * from contribution WHERE pageId=:pageId") @Query("SELECT * from contribution WHERE pageId=:pageId")
public abstract Contribution getContribution(String pageId); public abstract Contribution getContribution(String pageId);
@Query("SELECT * from contribution WHERE state=:state")
public abstract Single<List<Contribution>> getContribution(int state);
@Query("UPDATE contribution SET state=:state WHERE state in (:toUpdateStates)") @Query("UPDATE contribution SET state=:state WHERE state in (:toUpdateStates)")
public abstract Single<Integer> updateStates(int state, int[] toUpdateStates); public abstract Single<Integer> updateStates(int state, int[] toUpdateStates);

View file

@ -65,6 +65,11 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
this.contribution = contribution; this.contribution = contribution;
this.position = position; this.position = position;
titleView.setText(contribution.getMedia().getMostRelevantCaption()); titleView.setText(contribution.getMedia().getMostRelevantCaption());
imageView.getHierarchy().setPlaceholderImage(R.drawable.image_placeholder);
imageView.getHierarchy().setFailureImage(R.drawable.image_placeholder);
final String imageSource = chooseImageSource(contribution.getMedia().getThumbUrl(), final String imageSource = chooseImageSource(contribution.getMedia().getThumbUrl(),
contribution.getLocalUri()); contribution.getLocalUri());
if (!TextUtils.isEmpty(imageSource)) { if (!TextUtils.isEmpty(imageSource)) {
@ -88,6 +93,7 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
checkIfMediaExistsOnWikipediaPage(contribution); checkIfMediaExistsOnWikipediaPage(contribution);
break; break;
case Contribution.STATE_QUEUED: case Contribution.STATE_QUEUED:
case Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE:
stateView.setVisibility(View.VISIBLE); stateView.setVisibility(View.VISIBLE);
progressView.setVisibility(View.GONE); progressView.setVisibility(View.GONE);
stateView.setText(R.string.contribution_state_queued); stateView.setText(R.string.contribution_state_queued);

View file

@ -474,7 +474,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 || contribution.getState() == STATE_PAUSED && null != uploadService) { if (contribution.getState() == STATE_FAILED || contribution.getState() == STATE_PAUSED || contribution.getState()==Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE && null != uploadService) {
uploadService.queue(contribution); uploadService.queue(contribution);
Timber.d("Restarting for %s", contribution.toString()); Timber.d("Restarting for %s", contribution.toString());
} else { } else {

View file

@ -3,6 +3,8 @@ package fr.free.nrw.commons.contributions;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.content.Intent; import android.content.Intent;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
@ -10,6 +12,7 @@ import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.Switch;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.view.GravityCompat; import androidx.core.view.GravityCompat;
@ -21,6 +24,7 @@ import androidx.viewpager.widget.ViewPager;
import butterknife.BindView; import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import com.google.android.material.tabs.TabLayout; import com.google.android.material.tabs.TabLayout;
import fr.free.nrw.commons.CommonsApplication;
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.location.LocationServiceManager; import fr.free.nrw.commons.location.LocationServiceManager;
@ -33,6 +37,7 @@ import fr.free.nrw.commons.quiz.QuizChecker;
import fr.free.nrw.commons.theme.NavigationBaseActivity; import fr.free.nrw.commons.theme.NavigationBaseActivity;
import fr.free.nrw.commons.upload.UploadService; import fr.free.nrw.commons.upload.UploadService;
import fr.free.nrw.commons.utils.ViewUtil; import fr.free.nrw.commons.utils.ViewUtil;
import fr.free.nrw.commons.utils.ViewUtilWrapper;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import java.util.List; import java.util.List;
@ -56,6 +61,8 @@ public class MainActivity extends NavigationBaseActivity implements FragmentMana
NotificationController notificationController; NotificationController notificationController;
@Inject @Inject
QuizChecker quizChecker; QuizChecker quizChecker;
@Inject
ViewUtilWrapper viewUtilWrapper;
public ContributionsActivityPagerAdapter contributionsActivityPagerAdapter; public ContributionsActivityPagerAdapter contributionsActivityPagerAdapter;
@ -70,6 +77,7 @@ public class MainActivity extends NavigationBaseActivity implements FragmentMana
private TextView notificationCount; private TextView notificationCount;
private NearbyParentFragment nearbyParentFragment; private NearbyParentFragment nearbyParentFragment;
@Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_contributions); setContentView(R.layout.activity_contributions);
@ -280,9 +288,25 @@ public class MainActivity extends NavigationBaseActivity implements FragmentMana
this.menu = menu; this.menu = menu;
updateMenuItem(); updateMenuItem();
setNotificationCount(); setNotificationCount();
updateLimitedConnectionToggle(menu);
return true; return true;
} }
private void updateLimitedConnectionToggle(Menu menu) {
MenuItem checkable = menu.findItem(R.id.toggle_limited_connection_mode);
boolean isEnabled = defaultKvStore
.getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false);
checkable.setChecked(isEnabled);
final Switch switchToggleLimitedConnectionMode = checkable.getActionView()
.findViewById(R.id.switch_toggle_limited_connection_mode);
switchToggleLimitedConnectionMode.setChecked(isEnabled);
switchToggleLimitedConnectionMode.setOnCheckedChangeListener(
(buttonView, isChecked) -> toggleLimitedConnectionMode());
}
@SuppressLint("CheckResult") @SuppressLint("CheckResult")
private void setNotificationCount() { private void setNotificationCount() {
compositeDisposable.add(notificationController.getNotifications(false) compositeDisposable.add(notificationController.getNotifications(false)
@ -339,6 +363,27 @@ public class MainActivity extends NavigationBaseActivity implements FragmentMana
} }
} }
private void toggleLimitedConnectionMode() {
defaultKvStore.putBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED,
!defaultKvStore
.getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false));
if (defaultKvStore
.getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false)) {
viewUtilWrapper
.showShortToast(getBaseContext(), getString(R.string.limited_connection_enabled));
} else {
Intent intent = new Intent(this, UploadService.class);
intent.setAction(UploadService.PROCESS_PENDING_LIMITED_CONNECTION_MODE_UPLOADS);
if (VERSION.SDK_INT >= VERSION_CODES.O) {
startForegroundService(intent);
} else {
startService(intent);
}
viewUtilWrapper
.showShortToast(getBaseContext(), getString(R.string.limited_connection_disabled));
}
}
public class ContributionsActivityPagerAdapter extends FragmentPagerAdapter { public class ContributionsActivityPagerAdapter extends FragmentPagerAdapter {
FragmentManager fragmentManager; FragmentManager fragmentManager;

View file

@ -0,0 +1,229 @@
package fr.free.nrw.commons.media;
import android.net.Uri;
import android.os.Looper;
import android.os.SystemClock;
import androidx.annotation.Nullable;
import com.facebook.imagepipeline.common.BytesRange;
import com.facebook.imagepipeline.image.EncodedImage;
import com.facebook.imagepipeline.producers.BaseNetworkFetcher;
import com.facebook.imagepipeline.producers.BaseProducerContextCallbacks;
import com.facebook.imagepipeline.producers.Consumer;
import com.facebook.imagepipeline.producers.FetchState;
import com.facebook.imagepipeline.producers.NetworkFetcher;
import com.facebook.imagepipeline.producers.ProducerContext;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executor;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import okhttp3.CacheControl;
import okhttp3.Call;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import timber.log.Timber;
// Custom implementation of Fresco's Network fetcher to skip downloading of images when limited connection mode is enabled
// https://github.com/facebook/fresco/blob/master/imagepipeline-backends/imagepipeline-okhttp3/src/main/java/com/facebook/imagepipeline/backends/okhttp3/OkHttpNetworkFetcher.java
@Singleton
public class CustomOkHttpNetworkFetcher
extends BaseNetworkFetcher<CustomOkHttpNetworkFetcher.OkHttpNetworkFetchState> {
private static final String QUEUE_TIME = "queue_time";
private static final String FETCH_TIME = "fetch_time";
private static final String TOTAL_TIME = "total_time";
private static final String IMAGE_SIZE = "image_size";
private final Call.Factory mCallFactory;
private final @Nullable
CacheControl mCacheControl;
private Executor mCancellationExecutor;
private JsonKvStore defaultKvStore;
/**
* @param okHttpClient client to use
*/
@Inject
public CustomOkHttpNetworkFetcher(OkHttpClient okHttpClient,
@Named("default_preferences") JsonKvStore defaultKvStore) {
this(okHttpClient, okHttpClient.dispatcher().executorService(), defaultKvStore);
}
/**
* @param callFactory custom {@link Call.Factory} for fetching image from the network
* @param cancellationExecutor executor on which fetching cancellation is performed if
* cancellation is requested from the UI Thread
*/
public CustomOkHttpNetworkFetcher(Call.Factory callFactory, Executor cancellationExecutor,
JsonKvStore defaultKvStore) {
this(callFactory, cancellationExecutor, defaultKvStore, true);
}
/**
* @param callFactory custom {@link Call.Factory} for fetching image from the network
* @param cancellationExecutor executor on which fetching cancellation is performed if
* cancellation is requested from the UI Thread
* @param disableOkHttpCache true if network requests should not be cached by OkHttp
*/
public CustomOkHttpNetworkFetcher(
Call.Factory callFactory, Executor cancellationExecutor, JsonKvStore defaultKvStore,
boolean disableOkHttpCache) {
this.defaultKvStore = defaultKvStore;
mCallFactory = callFactory;
mCancellationExecutor = cancellationExecutor;
mCacheControl = disableOkHttpCache ? new CacheControl.Builder().noStore().build() : null;
}
@Override
public OkHttpNetworkFetchState createFetchState(
Consumer<EncodedImage> consumer, ProducerContext context) {
return new OkHttpNetworkFetchState(consumer, context);
}
@Override
public void fetch(
final OkHttpNetworkFetchState fetchState, final NetworkFetcher.Callback callback) {
fetchState.submitTime = SystemClock.elapsedRealtime();
final Uri uri = fetchState.getUri();
try {
if (defaultKvStore
.getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false)) {
Timber.d("Skipping loading of image as limited connection mode is enabled");
callback.onFailure(
new Exception("Failing image request as limited connection mode is enabled"));
return;
}
final Request.Builder requestBuilder = new Request.Builder().url(uri.toString()).get();
if (mCacheControl != null) {
requestBuilder.cacheControl(mCacheControl);
}
final BytesRange bytesRange = fetchState.getContext().getImageRequest().getBytesRange();
if (bytesRange != null) {
requestBuilder.addHeader("Range", bytesRange.toHttpRangeHeaderValue());
}
fetchWithRequest(fetchState, callback, requestBuilder.build());
} catch (Exception e) {
// handle error while creating the request
callback.onFailure(e);
}
}
@Override
public void onFetchCompletion(OkHttpNetworkFetchState fetchState, int byteSize) {
fetchState.fetchCompleteTime = SystemClock.elapsedRealtime();
}
@Override
public Map<String, String> getExtraMap(OkHttpNetworkFetchState fetchState, int byteSize) {
Map<String, String> extraMap = new HashMap<>(4);
extraMap.put(QUEUE_TIME, Long.toString(fetchState.responseTime - fetchState.submitTime));
extraMap.put(FETCH_TIME, Long.toString(fetchState.fetchCompleteTime - fetchState.responseTime));
extraMap.put(TOTAL_TIME, Long.toString(fetchState.fetchCompleteTime - fetchState.submitTime));
extraMap.put(IMAGE_SIZE, Integer.toString(byteSize));
return extraMap;
}
protected void fetchWithRequest(
final OkHttpNetworkFetchState fetchState,
final NetworkFetcher.Callback callback,
final Request request) {
final Call call = mCallFactory.newCall(request);
fetchState
.getContext()
.addCallbacks(
new BaseProducerContextCallbacks() {
@Override
public void onCancellationRequested() {
if (Looper.myLooper() != Looper.getMainLooper()) {
call.cancel();
} else {
mCancellationExecutor.execute(
new Runnable() {
@Override
public void run() {
call.cancel();
}
});
}
}
});
call.enqueue(
new okhttp3.Callback() {
@Override
public void onResponse(Call call, Response response) throws IOException {
fetchState.responseTime = SystemClock.elapsedRealtime();
final ResponseBody body = response.body();
try {
if (!response.isSuccessful()) {
handleException(
call, new IOException("Unexpected HTTP code " + response), callback);
return;
}
BytesRange responseRange =
BytesRange.fromContentRangeHeader(response.header("Content-Range"));
if (responseRange != null
&& !(responseRange.from == 0
&& responseRange.to == BytesRange.TO_END_OF_CONTENT)) {
// Only treat as a partial image if the range is not all of the content
fetchState.setResponseBytesRange(responseRange);
fetchState.setOnNewResultStatusFlags(Consumer.IS_PARTIAL_RESULT);
}
long contentLength = body.contentLength();
if (contentLength < 0) {
contentLength = 0;
}
callback.onResponse(body.byteStream(), (int) contentLength);
} catch (Exception e) {
handleException(call, e, callback);
} finally {
body.close();
}
}
@Override
public void onFailure(Call call, IOException e) {
handleException(call, e, callback);
}
});
}
/**
* Handles exceptions.
*
* <p>OkHttp notifies callers of cancellations via an IOException. If IOException is caught after
* request cancellation, then the exception is interpreted as successful cancellation and
* onCancellation is called. Otherwise onFailure is called.
*/
private void handleException(final Call call, final Exception e, final Callback callback) {
if (call.isCanceled()) {
callback.onCancellation();
} else {
callback.onFailure(e);
}
}
public static class OkHttpNetworkFetchState extends FetchState {
public long submitTime;
public long responseTime;
public long fetchCompleteTime;
public OkHttpNetworkFetchState(
Consumer<EncodedImage> consumer, ProducerContext producerContext) {
super(consumer, producerContext);
}
}
}

View file

@ -429,6 +429,13 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
* - when the high resolution image is available, it replaces the low resolution image * - when the high resolution image is available, it replaces the low resolution image
*/ */
private void setupImageView() { private void setupImageView() {
image.getHierarchy().setPlaceholderImage(R.drawable.image_placeholder);
image.getHierarchy().setFailureImage(R.drawable.image_placeholder);
imageLandscape.getHierarchy().setPlaceholderImage(R.drawable.image_placeholder);
imageLandscape.getHierarchy().setFailureImage(R.drawable.image_placeholder);
DraweeController controller = Fresco.newDraweeControllerBuilder() DraweeController controller = Fresco.newDraweeControllerBuilder()
.setLowResImageRequest(ImageRequest.fromUri(media.getThumbUrl())) .setLowResImageRequest(ImageRequest.fromUri(media.getThumbUrl()))
.setImageRequest(ImageRequest.fromUri(media.getImageUrl())) .setImageRequest(ImageRequest.fromUri(media.getImageUrl()))

View file

@ -1,22 +1,21 @@
package fr.free.nrw.commons.upload; package fr.free.nrw.commons.upload;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import fr.free.nrw.commons.CommonsApplication;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.filepicker.UploadableFile; import fr.free.nrw.commons.filepicker.UploadableFile;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.repository.UploadRepository; import fr.free.nrw.commons.repository.UploadRepository;
import io.reactivex.Observer; import io.reactivex.Observer;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable; import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import java.lang.reflect.Proxy;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import timber.log.Timber; import timber.log.Timber;
/** /**
@ -29,13 +28,16 @@ public class UploadPresenter implements UploadContract.UserActionListener {
UploadContract.View.class.getClassLoader(), UploadContract.View.class.getClassLoader(),
new Class[]{UploadContract.View.class}, (proxy, method, methodArgs) -> null); new Class[]{UploadContract.View.class}, (proxy, method, methodArgs) -> null);
private final UploadRepository repository; private final UploadRepository repository;
private final JsonKvStore defaultKvStore;
private UploadContract.View view = DUMMY; private UploadContract.View view = DUMMY;
private CompositeDisposable compositeDisposable; private CompositeDisposable compositeDisposable;
@Inject @Inject
UploadPresenter(UploadRepository uploadRepository) { UploadPresenter(UploadRepository uploadRepository,
@Named("default_preferences") JsonKvStore defaultKvStore) {
this.repository = uploadRepository; this.repository = uploadRepository;
this.defaultKvStore = defaultKvStore;
compositeDisposable = new CompositeDisposable(); compositeDisposable = new CompositeDisposable();
} }
@ -54,7 +56,14 @@ public class UploadPresenter implements UploadContract.UserActionListener {
@Override @Override
public void onSubscribe(Disposable d) { public void onSubscribe(Disposable d) {
view.showProgress(false); view.showProgress(false);
view.showMessage(R.string.uploading_started); if (defaultKvStore
.getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED,
false)) {
view.showMessage(R.string.uploading_queued);
} else {
view.showMessage(R.string.uploading_started);
}
compositeDisposable.add(d); compositeDisposable.add(d);
} }

View file

@ -21,13 +21,17 @@ import fr.free.nrw.commons.contributions.ContributionDao;
import fr.free.nrw.commons.contributions.MainActivity; import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.di.CommonsApplicationModule; import fr.free.nrw.commons.di.CommonsApplicationModule;
import fr.free.nrw.commons.di.CommonsDaggerService; import fr.free.nrw.commons.di.CommonsDaggerService;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.media.MediaClient; import fr.free.nrw.commons.media.MediaClient;
import fr.free.nrw.commons.utils.ViewUtil; import fr.free.nrw.commons.utils.ViewUtil;
import fr.free.nrw.commons.wikidata.WikidataEditService; import fr.free.nrw.commons.wikidata.WikidataEditService;
import io.reactivex.Completable;
import io.reactivex.Observable; import io.reactivex.Observable;
import io.reactivex.ObservableSource;
import io.reactivex.Scheduler; import io.reactivex.Scheduler;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.functions.Consumer; import io.reactivex.functions.Consumer;
import io.reactivex.functions.Function;
import io.reactivex.processors.PublishProcessor; import io.reactivex.processors.PublishProcessor;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import java.io.IOException; import java.io.IOException;
@ -50,6 +54,7 @@ public class UploadService extends CommonsDaggerService {
.asList("uploadstash-file-not-found", "stashfailed", "verification-error", "chunk-too-small"); .asList("uploadstash-file-not-found", "stashfailed", "verification-error", "chunk-too-small");
public static final String ACTION_START_SERVICE = EXTRA_PREFIX + ".upload"; public static final String ACTION_START_SERVICE = EXTRA_PREFIX + ".upload";
public static final String PROCESS_PENDING_LIMITED_CONNECTION_MODE_UPLOADS = EXTRA_PREFIX + "process_limited_connection_mode_uploads";
public static final String EXTRA_FILES = EXTRA_PREFIX + ".files"; public static final String EXTRA_FILES = EXTRA_PREFIX + ".files";
@Inject @Inject
WikidataEditService wikidataEditService; WikidataEditService wikidataEditService;
@ -67,6 +72,9 @@ public class UploadService extends CommonsDaggerService {
@Inject @Inject
@Named(CommonsApplicationModule.IO_THREAD) @Named(CommonsApplicationModule.IO_THREAD)
Scheduler ioThreadScheduler; Scheduler ioThreadScheduler;
@Inject
@Named("default_preferences")
public JsonKvStore defaultKvStore;
private NotificationManagerCompat notificationManager; private NotificationManagerCompat notificationManager;
private NotificationCompat.Builder curNotification; private NotificationCompat.Builder curNotification;
@ -203,11 +211,21 @@ public class UploadService extends CommonsDaggerService {
private boolean freshStart = true; private boolean freshStart = true;
public void queue(Contribution contribution) { public void queue(Contribution contribution) {
if (defaultKvStore
.getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false)) {
contribution.setState(Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE);
contributionDao.save(contribution)
.subscribeOn(ioThreadScheduler)
.subscribe();
return;
}
contributionsToUpload.offer(contribution); contributionsToUpload.offer(contribution);
} }
@Override @Override
public int onStartCommand(Intent intent, int flags, int startId) { public int onStartCommand(Intent intent, int flags, int startId) {
startForeground(NOTIFICATION_UPLOAD_IN_PROGRESS,
curNotification.setContentText(getText(R.string.starting_uploads)).build());
if (ACTION_START_SERVICE.equals(intent.getAction()) && freshStart) { if (ACTION_START_SERVICE.equals(intent.getAction()) && freshStart) {
compositeDisposable.add(contributionDao.updateStates(Contribution.STATE_FAILED, compositeDisposable.add(contributionDao.updateStates(Contribution.STATE_FAILED,
new int[]{Contribution.STATE_QUEUED, Contribution.STATE_IN_PROGRESS}) new int[]{Contribution.STATE_QUEUED, Contribution.STATE_IN_PROGRESS})
@ -215,7 +233,14 @@ public class UploadService extends CommonsDaggerService {
.subscribeOn(ioThreadScheduler) .subscribeOn(ioThreadScheduler)
.subscribe()); .subscribe());
freshStart = false; freshStart = false;
} } else if (PROCESS_PENDING_LIMITED_CONNECTION_MODE_UPLOADS.equals(intent.getAction())) {
contributionDao.getContribution(Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE)
.flatMapObservable(
(Function<List<Contribution>, ObservableSource<Contribution>>) contributions -> Observable.fromIterable(contributions))
.concatMapCompletable(contribution -> Completable.fromAction(() -> queue(contribution)))
.subscribeOn(ioThreadScheduler)
.subscribe();
}
return START_REDELIVER_INTENT; return START_REDELIVER_INTENT;
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<Switch xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:id="@+id/switch_toggle_limited_connection_mode"/>

View file

@ -1,5 +1,12 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu 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">
<item android:id="@+id/toggle_limited_connection_mode"
android:title="@string/limited_connection"
app:showAsAction="always"
android:checkable="true"
app:actionLayout="@layout/menu_switch"
/>
<item android:id="@+id/notifications" <item android:id="@+id/notifications"
android:title="@string/notifications" android:title="@string/notifications"
app:showAsAction="ifRoom|withText" app:showAsAction="ifRoom|withText"

View file

@ -10,6 +10,7 @@
<item quantity="one">(%1$d)</item> <item quantity="one">(%1$d)</item>
<item quantity="other">(%1$d)</item> <item quantity="other">(%1$d)</item>
</plurals> </plurals>
<string name="starting_uploads"> Starting Uploads</string>
<plurals name="starting_multiple_uploads"> <plurals name="starting_multiple_uploads">
<item quantity="one">Starting %1$d upload</item> <item quantity="one">Starting %1$d upload</item>
<item quantity="other">Starting %1$d uploads</item> <item quantity="other">Starting %1$d uploads</item>
@ -54,6 +55,7 @@
<string name="upload_failed">File not found. Please try another file.</string> <string name="upload_failed">File not found. Please try another file.</string>
<string name="authentication_failed">Authentication failed, please login again</string> <string name="authentication_failed">Authentication failed, please login again</string>
<string name="uploading_started">Upload started!</string> <string name="uploading_started">Upload started!</string>
<string name="uploading_queued">Upload queued (limited connection mode enabled)</string>
<string name="upload_completed_notification_title">%1$s uploaded!</string> <string name="upload_completed_notification_title">%1$s uploaded!</string>
<string name="upload_completed_notification_text">Tap to view your upload</string> <string name="upload_completed_notification_text">Tap to view your upload</string>
<string name="upload_progress_notification_title_start">Starting %1$s upload</string> <string name="upload_progress_notification_title_start">Starting %1$s upload</string>
@ -692,4 +694,7 @@ Upload your first media by tapping on the add button.</string>
<string name="mapbox_telemetry">Telemetry Opt Out</string> <string name="mapbox_telemetry">Telemetry Opt Out</string>
<string name="telemetry_opt_out_summary">Send anonymized location and usage data to Mapbox when using Nearby feature</string> <string name="telemetry_opt_out_summary">Send anonymized location and usage data to Mapbox when using Nearby feature</string>
<string name="map_attribution" translatable="false"><![CDATA[&#169; <a href="https://www.mapbox.com/about/maps/">Mapbox</a> &#169; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> <a href="https://www.mapbox.com/map-feedback/">Improve this map</a>]]></string> <string name="map_attribution" translatable="false"><![CDATA[&#169; <a href="https://www.mapbox.com/about/maps/">Mapbox</a> &#169; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> <a href="https://www.mapbox.com/map-feedback/">Improve this map</a>]]></string>
<string name="limited_connection_enabled">Limited connection mode enabled!</string>
<string name="limited_connection_disabled">Limited connection mode disabled. Pending uploads will resume now.</string>
<string name="limited_connection">Limited Connection</string>
</resources> </resources>

View file

@ -1,8 +1,10 @@
package fr.free.nrw.commons.upload package fr.free.nrw.commons.upload
import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.verify
import fr.free.nrw.commons.CommonsApplication
import fr.free.nrw.commons.contributions.Contribution import fr.free.nrw.commons.contributions.Contribution
import fr.free.nrw.commons.filepicker.UploadableFile import fr.free.nrw.commons.filepicker.UploadableFile
import fr.free.nrw.commons.kvstore.JsonKvStore
import fr.free.nrw.commons.repository.UploadRepository import fr.free.nrw.commons.repository.UploadRepository
import io.reactivex.Observable import io.reactivex.Observable
import org.junit.Before import org.junit.Before
@ -27,6 +29,9 @@ class UploadPresenterTest {
@Mock @Mock
lateinit var contribution: Contribution lateinit var contribution: Contribution
@Mock
lateinit var defaultKvStore: JsonKvStore
@Mock @Mock
private lateinit var uploadableFile: UploadableFile private lateinit var uploadableFile: UploadableFile
@ -65,6 +70,22 @@ class UploadPresenterTest {
verify(repository).buildContributions() verify(repository).buildContributions()
} }
@Test
fun handleSubmitTestUserLoggedInAndLimitedConnectionOn() {
`when`(
defaultKvStore
.getBoolean(
CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED,
false
)).thenReturn(true)
`when`(view.isLoggedIn).thenReturn(true)
uploadPresenter.handleSubmit()
verify(view).isLoggedIn
verify(view).showProgress(true)
verify(repository).buildContributions()
verify(repository).buildContributions()
}
/** /**
* unit test case for method UploadPresenter.handleSubmit * unit test case for method UploadPresenter.handleSubmit
*/ */