mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-27 04:43:54 +01:00
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:
parent
59ee7b8df2
commit
66f6e2e648
15 changed files with 389 additions and 15 deletions
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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()))
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
if (defaultKvStore
|
||||||
|
.getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED,
|
||||||
|
false)) {
|
||||||
|
view.showMessage(R.string.uploading_queued);
|
||||||
|
} else {
|
||||||
view.showMessage(R.string.uploading_started);
|
view.showMessage(R.string.uploading_started);
|
||||||
|
}
|
||||||
|
|
||||||
compositeDisposable.add(d);
|
compositeDisposable.add(d);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,6 +233,13 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
BIN
app/src/main/res/drawable/image_placeholder.png
Normal file
BIN
app/src/main/res/drawable/image_placeholder.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.5 KiB |
5
app/src/main/res/layout/menu_switch.xml
Normal file
5
app/src/main/res/layout/menu_switch.xml
Normal 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"/>
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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[© <a href="https://www.mapbox.com/about/maps/">Mapbox</a> © <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[© <a href="https://www.mapbox.com/about/maps/">Mapbox</a> © <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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue