Fixes #3790- Use WorkManagers to upload contributions (#4298)

* Fixes #3790
Use WorkManagers to process upload contributions
** Removed UploadService and Added UploadWorker to process contributions Upload
** Made nescessary changes to remove the usages of the Service from the classes
** UI Fxies- Minor changes in the retry and cancel uplaod icons to give them a clickable area of 48 dp

* Fixes #3790
Use WorkManagers to process upload contributions
** Removed UploadService and Added UploadWorker to process contributions Upload
** Made nescessary changes to remove the usages of the Service from the classes
** UI Fxies- Minor changes in the retry and cancel uplaod icons to give them a clickable area of 48 dp

* Updated JavaDocs in UploadWorker, Fixed Test cases

* Updated JavaDocs in UploadWorker, Fixed Test cases

* Updated gradle

* Revert "Updated gradle"

This reverts commit c8979fe6dc.

* rolledback to compileSDKVersion 28, fixed tests

* Don't call the show notifications on the main thread

* Bug Fix- Duplicate contributions, handle upload stash errors
This commit is contained in:
Ashish 2021-04-08 18:29:07 +05:30 committed by GitHub
parent fd2a7a9c56
commit ecbff7e3b8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 692 additions and 802 deletions

View file

@ -67,9 +67,11 @@ dependencies {
implementation "com.squareup.okhttp3:logging-interceptor:$OKHTTP_VERSION" implementation "com.squareup.okhttp3:logging-interceptor:$OKHTTP_VERSION"
// Dependency injector // Dependency injector
implementation "com.google.dagger:dagger-android:$DAGGER_VERSION"
implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION" implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION"
kapt "com.google.dagger:dagger-android-processor:$DAGGER_VERSION" kapt "com.google.dagger:dagger-android-processor:$DAGGER_VERSION"
kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION" kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
annotationProcessor "com.google.dagger:dagger-android-processor:$DAGGER_VERSION"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$KOTLIN_VERSION" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$KOTLIN_VERSION"
implementation "org.jetbrains.kotlin:kotlin-reflect:$KOTLIN_VERSION" implementation "org.jetbrains.kotlin:kotlin-reflect:$KOTLIN_VERSION"
@ -145,6 +147,10 @@ dependencies {
implementation "androidx.preference:preference-ktx:$PREFERENCE_VERSION" implementation "androidx.preference:preference-ktx:$PREFERENCE_VERSION"
implementation "androidx.multidex:multidex:$MULTIDEX_VERSION" implementation "androidx.multidex:multidex:$MULTIDEX_VERSION"
def work_version = "2.4.0"
// Kotlin + coroutines
implementation "androidx.work:work-runtime-ktx:$work_version"
} }
android { android {

View file

@ -135,8 +135,7 @@
android:name=".review.ReviewActivity" android:name=".review.ReviewActivity"
android:label="@string/title_activity_review" /> android:label="@string/title_activity_review" />
<service android:name=".upload.UploadService" /> <service
<service
android:name=".auth.WikiAccountAuthenticatorService" android:name=".auth.WikiAccountAuthenticatorService"
android:exported="true" android:exported="true"
android:process=":auth"> android:process=":auth">

View file

@ -47,7 +47,9 @@ import io.reactivex.internal.functions.Functions;
import io.reactivex.plugins.RxJavaPlugins; import io.reactivex.plugins.RxJavaPlugins;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import java.io.File; import java.io.File;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Map;
import java.util.Set; import java.util.Set;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
@ -126,7 +128,12 @@ public class CommonsApplication extends MultiDexApplication {
@Inject ContributionDao contributionDao; @Inject ContributionDao contributionDao;
/** /**
* In memory list of contributios whose uploads ahve been paused by the user
*/
public static Map<String, Boolean> pauseUploads = new HashMap<>();
/**
* Used to declare and initialize various components and dependencies * Used to declare and initialize various components and dependencies
*/ */
@Override @Override

View file

@ -136,4 +136,14 @@ public class SessionManager {
currentAccount = null; currentAccount = null;
}); });
} }
/**
* Return a corresponding boolean preference
*
* @param key
* @return
*/
public boolean getPreference(String key) {
return defaultKvStore.getBoolean(key);
}
} }

View file

@ -5,7 +5,7 @@ import android.os.Parcelable
import fr.free.nrw.commons.upload.UploadResult import fr.free.nrw.commons.upload.UploadResult
data class ChunkInfo( data class ChunkInfo(
val uploadResult: UploadResult, val uploadResult: UploadResult?,
val indexOfNextChunkToUpload: Int, val indexOfNextChunkToUpload: Int,
val totalChunks: Int val totalChunks: Int
) : Parcelable { ) : Parcelable {

View file

@ -1,13 +1,11 @@
package fr.free.nrw.commons.contributions; package fr.free.nrw.commons.contributions;
import static fr.free.nrw.commons.upload.UploadService.EXTRA_FILES;
import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT; import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT;
import android.Manifest; import android.Manifest;
import android.app.Activity; import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.filepicker.DefaultCallback; import fr.free.nrw.commons.filepicker.DefaultCallback;
@ -29,7 +27,6 @@ import javax.inject.Singleton;
public class ContributionController { public class ContributionController {
public static final String ACTION_INTERNAL_UPLOADS = "internalImageUploads"; public static final String ACTION_INTERNAL_UPLOADS = "internalImageUploads";
private final JsonKvStore defaultKvStore; private final JsonKvStore defaultKvStore;
@Inject @Inject
@ -130,7 +127,8 @@ public class ContributionController {
List<UploadableFile> imagesFiles) { List<UploadableFile> imagesFiles) {
Intent shareIntent = new Intent(context, UploadActivity.class); Intent shareIntent = new Intent(context, UploadActivity.class);
shareIntent.setAction(ACTION_INTERNAL_UPLOADS); shareIntent.setAction(ACTION_INTERNAL_UPLOADS);
shareIntent.putParcelableArrayListExtra(EXTRA_FILES, new ArrayList<>(imagesFiles)); shareIntent
.putParcelableArrayListExtra(UploadActivity.EXTRA_FILES, new ArrayList<>(imagesFiles));
Place place = defaultKvStore.getJson(PLACE_OBJECT, Place.class); Place place = defaultKvStore.getJson(PLACE_OBJECT, Place.class);
if (place != null) { if (place != null) {

View file

@ -39,12 +39,6 @@ public abstract class ContributionDao {
saveSynchronous(newContribution); saveSynchronous(newContribution);
} }
public Completable saveAndDelete(final Contribution oldContribution,
final Contribution newContribution) {
return Completable
.fromAction(() -> deleteAndSaveContribution(oldContribution, newContribution));
}
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
public abstract Single<List<Long>> save(List<Contribution> contribution); public abstract Single<List<Long>> save(List<Contribution> contribution);
@ -62,11 +56,8 @@ 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") @Query("SELECT * from contribution WHERE state IN (:states) order by media_dateUploaded DESC")
public abstract Single<List<Contribution>> getContribution(int state); public abstract Single<List<Contribution>> getContribution(List<Integer> states);
@Query("UPDATE contribution SET state=:state WHERE state in (:toUpdateStates)")
public abstract Single<Integer> updateStates(int state, int[] toUpdateStates);
@Query("SELECT COUNT(*) from contribution WHERE state in (:toUpdateStates)") @Query("SELECT COUNT(*) from contribution WHERE state in (:toUpdateStates)")
public abstract Single<Integer> getPendingUploads(int[] toUpdateStates); public abstract Single<Integer> getPendingUploads(int[] toUpdateStates);

View file

@ -1,5 +1,6 @@
package fr.free.nrw.commons.contributions; package fr.free.nrw.commons.contributions;
import android.content.Context;
import fr.free.nrw.commons.BasePresenter; import fr.free.nrw.commons.BasePresenter;
/** /**
@ -10,6 +11,8 @@ public class ContributionsContract {
public interface View { public interface View {
void showMessage(String localizedMessage); void showMessage(String localizedMessage);
Context getContext();
} }
public interface UserActionListener extends BasePresenter<ContributionsContract.View> { public interface UserActionListener extends BasePresenter<ContributionsContract.View> {
@ -18,5 +21,6 @@ public class ContributionsContract {
void deleteUpload(Contribution contribution); void deleteUpload(Contribution contribution);
void saveContribution(Contribution contribution);
} }
} }

View file

@ -6,20 +6,14 @@ import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween;
import android.Manifest; import android.Manifest;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle; import android.os.Bundle;
import android.os.IBinder;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.MenuItem.OnMenuItemClickListener; import android.view.MenuItem.OnMenuItemClickListener;
import android.view.View; import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.CheckBox; import android.widget.CheckBox;
import android.widget.LinearLayout; import android.widget.LinearLayout;
@ -27,28 +21,15 @@ import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.widget.SwitchCompat;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager.OnBackStackChangedListener; import androidx.fragment.app.FragmentManager.OnBackStackChangedListener;
import androidx.fragment.app.FragmentTransaction; import androidx.fragment.app.FragmentTransaction;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.MediaDataExtractor;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.notification.Notification;
import fr.free.nrw.commons.notification.NotificationController;
import fr.free.nrw.commons.theme.BaseActivity;
import fr.free.nrw.commons.upload.UploadService.ServiceCallback;
import io.reactivex.disposables.Disposable;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import butterknife.BindView; import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
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.campaigns.Campaign; import fr.free.nrw.commons.campaigns.Campaign;
import fr.free.nrw.commons.campaigns.CampaignView; import fr.free.nrw.commons.campaigns.CampaignView;
import fr.free.nrw.commons.campaigns.CampaignsPresenter; import fr.free.nrw.commons.campaigns.CampaignsPresenter;
@ -66,8 +47,10 @@ import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import fr.free.nrw.commons.nearby.NearbyController; import fr.free.nrw.commons.nearby.NearbyController;
import fr.free.nrw.commons.nearby.NearbyNotificationCardView; import fr.free.nrw.commons.nearby.NearbyNotificationCardView;
import fr.free.nrw.commons.nearby.Place; import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.notification.Notification;
import fr.free.nrw.commons.notification.NotificationActivity; import fr.free.nrw.commons.notification.NotificationActivity;
import fr.free.nrw.commons.upload.UploadService; import fr.free.nrw.commons.notification.NotificationController;
import fr.free.nrw.commons.theme.BaseActivity;
import fr.free.nrw.commons.utils.ConfigUtils; import fr.free.nrw.commons.utils.ConfigUtils;
import fr.free.nrw.commons.utils.DialogUtil; import fr.free.nrw.commons.utils.DialogUtil;
import fr.free.nrw.commons.utils.NetworkUtils; import fr.free.nrw.commons.utils.NetworkUtils;
@ -77,6 +60,9 @@ import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import timber.log.Timber; import timber.log.Timber;
public class ContributionsFragment public class ContributionsFragment
@ -85,7 +71,7 @@ public class ContributionsFragment
OnBackStackChangedListener, OnBackStackChangedListener,
LocationUpdateListener, LocationUpdateListener,
MediaDetailProvider, MediaDetailProvider,
ICampaignsView, ContributionsContract.View, Callback , ServiceCallback { ICampaignsView, ContributionsContract.View, Callback{
@Inject @Named("default_preferences") JsonKvStore store; @Inject @Named("default_preferences") JsonKvStore store;
@Inject NearbyController nearbyController; @Inject NearbyController nearbyController;
@Inject OkHttpJsonApiClient okHttpJsonApiClient; @Inject OkHttpJsonApiClient okHttpJsonApiClient;
@ -93,8 +79,6 @@ public class ContributionsFragment
@Inject LocationServiceManager locationManager; @Inject LocationServiceManager locationManager;
@Inject NotificationController notificationController; @Inject NotificationController notificationController;
private UploadService uploadService;
private boolean isUploadServiceConnected;
private CompositeDisposable compositeDisposable = new CompositeDisposable(); private CompositeDisposable compositeDisposable = new CompositeDisposable();
private ContributionsListFragment contributionsListFragment; private ContributionsListFragment contributionsListFragment;
@ -127,33 +111,7 @@ public class ContributionsFragment
return fragment; return fragment;
} }
/**
* Since we will need to use parent activity on onAuthCookieAcquired, we have to wait
* fragment to be attached. Latch will be responsible for this sync.
*/
private ServiceConnection uploadServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName componentName, IBinder binder) {
uploadService = (UploadService) ((UploadService.UploadServiceLocalBinder) binder)
.getService();
uploadService.setServiceCallback(ContributionsFragment.this);
isUploadServiceConnected = true;
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
// this should never happen
Timber.e(new RuntimeException("UploadService died but the rest of the process did not!"));
isUploadServiceConnected = false;
}
@Override
public void onBindingDied(final ComponentName name) {
isUploadServiceConnected = false;
}
};
private boolean shouldShowMediaDetailsFragment; private boolean shouldShowMediaDetailsFragment;
private boolean isAuthCookieAcquired;
@Override @Override
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
@ -272,7 +230,6 @@ public class ContributionsFragment
until fragment life time ends. until fragment life time ends.
*/ */
if (!isFragmentAttachedBefore && getActivity() != null) { if (!isFragmentAttachedBefore && getActivity() != null) {
onAuthCookieAcquired();
isFragmentAttachedBefore = true; isFragmentAttachedBefore = true;
} }
} }
@ -312,19 +269,6 @@ public class ContributionsFragment
fetchCampaigns(); fetchCampaigns();
} }
/**
* Called when onAuthCookieAcquired is called on authenticated parent activity
*/
void onAuthCookieAcquired() {
// Since we call onAuthCookieAcquired method from onAttach, isAdded is still false. So don't use it
isAuthCookieAcquired=true;
if (getActivity() != null) { // If fragment is attached to parent activity
getActivity().bindService(getUploadServiceIntent(), uploadServiceConnection, Context.BIND_AUTO_CREATE);
isUploadServiceConnected = true;
}
}
private void initFragments() { private void initFragments() {
if (null == contributionsListFragment) { if (null == contributionsListFragment) {
contributionsListFragment = new ContributionsListFragment(); contributionsListFragment = new ContributionsListFragment();
@ -381,13 +325,6 @@ public class ContributionsFragment
getChildFragmentManager().executePendingTransactions(); getChildFragmentManager().executePendingTransactions();
} }
public Intent getUploadServiceIntent(){
Intent intent = new Intent(getActivity(), UploadService.class);
intent.setAction(UploadService.ACTION_START_SERVICE);
return intent;
}
@SuppressWarnings("ConstantConditions") @SuppressWarnings("ConstantConditions")
private void setUploadCount() { private void setUploadCount() {
compositeDisposable.add(okHttpJsonApiClient compositeDisposable.add(okHttpJsonApiClient
@ -524,14 +461,6 @@ public class ContributionsFragment
locationManager.unregisterLocationManager(); locationManager.unregisterLocationManager();
locationManager.removeLocationListener(this); locationManager.removeLocationListener(this);
super.onDestroy(); super.onDestroy();
if (isUploadServiceConnected) {
if (getActivity() != null) {
uploadService.setServiceCallback(null);
getActivity().unbindService(uploadServiceConnection);
isUploadServiceConnected = false;
}
}
} catch (IllegalArgumentException | IllegalStateException exception) { } catch (IllegalArgumentException | IllegalStateException exception) {
Timber.e(exception); Timber.e(exception);
} }
@ -594,7 +523,6 @@ public class ContributionsFragment
@Override public void onDestroyView() { @Override public void onDestroyView() {
super.onDestroyView(); super.onDestroyView();
isUploadServiceConnected = false;
presenter.onDetachView(); presenter.onDetachView();
} }
@ -606,8 +534,9 @@ 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 || contribution.getState()==Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE && null != uploadService) { if (contribution.getState() == STATE_FAILED || contribution.getState() == STATE_PAUSED || contribution.getState()==Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE) {
uploadService.queue(contribution); contribution.setState(Contribution.STATE_QUEUED);
contributionsPresenter.saveContribution(contribution);
Timber.d("Restarting for %s", contribution.toString()); Timber.d("Restarting for %s", contribution.toString());
} else { } else {
Timber.d("Skipping re-upload for non-failed %s", contribution.toString()); Timber.d("Skipping re-upload for non-failed %s", contribution.toString());
@ -624,7 +553,11 @@ public class ContributionsFragment
*/ */
@Override @Override
public void pauseUpload(Contribution contribution) { public void pauseUpload(Contribution contribution) {
uploadService.pauseUpload(contribution); //Pause the upload in the global singleton
CommonsApplication.pauseUploads.put(contribution.getPageId(), true);
//Retain the paused state in DB
contribution.setState(STATE_PAUSED);
contributionsPresenter.saveContribution(contribution);
} }
/** /**
@ -677,10 +610,5 @@ public class ContributionsFragment
public MediaDetailPagerFragment getMediaDetailPagerFragment() { public MediaDetailPagerFragment getMediaDetailPagerFragment() {
return mediaDetailPagerFragment; return mediaDetailPagerFragment;
} }
@Override
public void updateUploadCount() {
setUploadCount();
}
} }

View file

@ -3,7 +3,6 @@ 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;
@ -22,8 +21,8 @@ class ContributionsLocalDataSource {
@Inject @Inject
public ContributionsLocalDataSource( public ContributionsLocalDataSource(
@Named("default_preferences") JsonKvStore defaultKVStore, @Named("default_preferences") final JsonKvStore defaultKVStore,
ContributionDao contributionDao) { final ContributionDao contributionDao) {
this.defaultKVStore = defaultKVStore; this.defaultKVStore = defaultKVStore;
this.contributionDao = contributionDao; this.contributionDao = contributionDao;
} }
@ -31,14 +30,14 @@ class ContributionsLocalDataSource {
/** /**
* Fetch default number of contributions to be show, based on user preferences * Fetch default number of contributions to be show, based on user preferences
*/ */
public String getString(String key) { public String getString(final String key) {
return defaultKVStore.getString(key); return defaultKVStore.getString(key);
} }
/** /**
* Fetch default number of contributions to be show, based on user preferences * Fetch default number of contributions to be show, based on user preferences
*/ */
public long getLong(String key) { public long getLong(final String key) {
return defaultKVStore.getLong(key); return defaultKVStore.getLong(key);
} }
@ -47,8 +46,8 @@ class ContributionsLocalDataSource {
* @param uri * @param uri
* @return * @return
*/ */
public Contribution getContributionWithFileName(String uri) { public Contribution getContributionWithFileName(final String uri) {
List<Contribution> contributionWithUri = contributionDao.getContributionWithTitle(uri); final List<Contribution> contributionWithUri = contributionDao.getContributionWithTitle(uri);
if(!contributionWithUri.isEmpty()){ if(!contributionWithUri.isEmpty()){
return contributionWithUri.get(0); return contributionWithUri.get(0);
} }
@ -60,7 +59,7 @@ class ContributionsLocalDataSource {
* @param contribution * @param contribution
* @return * @return
*/ */
public Completable deleteContribution(Contribution contribution) { public Completable deleteContribution(final Contribution contribution) {
return contributionDao.delete(contribution); return contributionDao.delete(contribution);
} }
@ -68,10 +67,10 @@ class ContributionsLocalDataSource {
return contributionDao.fetchContributions(); return contributionDao.fetchContributions();
} }
public Single<List<Long>> saveContributions(List<Contribution> contributions) { public Single<List<Long>> saveContributions(final List<Contribution> contributions) {
List<Contribution> contributionList = new ArrayList<>(); final List<Contribution> contributionList = new ArrayList<>();
for(Contribution contribution: contributions) { for(final Contribution contribution: contributions) {
Contribution oldContribution = contributionDao.getContribution(contribution.getPageId()); final Contribution oldContribution = contributionDao.getContribution(contribution.getPageId());
if(oldContribution != null) { if(oldContribution != null) {
contribution.setWikidataPlace(oldContribution.getWikidataPlace()); contribution.setWikidataPlace(oldContribution.getWikidataPlace());
} }
@ -80,11 +79,15 @@ class ContributionsLocalDataSource {
return contributionDao.save(contributionList); return contributionDao.save(contributionList);
} }
public void set(String key, long value) { public Completable saveContributions(Contribution contribution) {
return contributionDao.save(contribution);
}
public void set(final String key, final long value) {
defaultKVStore.putLong(key,value); defaultKVStore.putLong(key,value);
} }
public Completable updateContribution(Contribution contribution) { public Completable updateContribution(final Contribution contribution) {
return contributionDao.update(contribution); return contributionDao.update(contribution);
} }
} }

View file

@ -1,10 +1,18 @@
package fr.free.nrw.commons.contributions; package fr.free.nrw.commons.contributions;
import androidx.work.ExistingWorkPolicy;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkManager;
import fr.free.nrw.commons.MediaDataExtractor; import fr.free.nrw.commons.MediaDataExtractor;
import fr.free.nrw.commons.contributions.ContributionsContract.UserActionListener; import fr.free.nrw.commons.contributions.ContributionsContract.UserActionListener;
import fr.free.nrw.commons.di.CommonsApplicationModule; import fr.free.nrw.commons.di.CommonsApplicationModule;
import fr.free.nrw.commons.upload.worker.UploadWorker;
import io.reactivex.Scheduler; import io.reactivex.Scheduler;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.functions.Action;
import io.reactivex.functions.Consumer;
import java.util.Collections;
import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
@ -14,7 +22,6 @@ import javax.inject.Named;
public class ContributionsPresenter implements UserActionListener { public class ContributionsPresenter implements UserActionListener {
private final ContributionsRepository repository; private final ContributionsRepository repository;
private final Scheduler mainThreadScheduler;
private final Scheduler ioThreadScheduler; private final Scheduler ioThreadScheduler;
private CompositeDisposable compositeDisposable; private CompositeDisposable compositeDisposable;
private ContributionsContract.View view; private ContributionsContract.View view;
@ -23,9 +30,9 @@ public class ContributionsPresenter implements UserActionListener {
MediaDataExtractor mediaDataExtractor; MediaDataExtractor mediaDataExtractor;
@Inject @Inject
ContributionsPresenter(ContributionsRepository repository, @Named(CommonsApplicationModule.MAIN_THREAD) Scheduler mainThreadScheduler,@Named(CommonsApplicationModule.IO_THREAD) Scheduler ioThreadScheduler) { ContributionsPresenter(ContributionsRepository repository,
@Named(CommonsApplicationModule.IO_THREAD) Scheduler ioThreadScheduler) {
this.repository = repository; this.repository = repository;
this.mainThreadScheduler=mainThreadScheduler;
this.ioThreadScheduler=ioThreadScheduler; this.ioThreadScheduler=ioThreadScheduler;
} }
@ -57,4 +64,23 @@ public class ContributionsPresenter implements UserActionListener {
.subscribeOn(ioThreadScheduler) .subscribeOn(ioThreadScheduler)
.subscribe()); .subscribe());
} }
/**
* Update the contribution's state in the databse, upon completion, trigger the workmanager to
* process this contribution
*
* @param contribution
*/
@Override
public void saveContribution(Contribution contribution) {
compositeDisposable.add(repository
.save(contribution)
.subscribeOn(ioThreadScheduler)
.subscribe(() -> {
WorkManager.getInstance(view.getContext().getApplicationContext())
.enqueueUniqueWork(
UploadWorker.class.getSimpleName(),
ExistingWorkPolicy.KEEP, OneTimeWorkRequest.from(UploadWorker.class));
}));
}
} }

View file

@ -53,6 +53,10 @@ public class ContributionsRepository {
return localDataSource.saveContributions(contributions); return localDataSource.saveContributions(contributions);
} }
public Completable save(Contribution contributions){
return localDataSource.saveContributions(contributions);
}
public void set(String key, long value) { public void set(String key, long value) {
localDataSource.set(key,value); localDataSource.set(key,value);
} }

View file

@ -13,13 +13,15 @@ import androidx.annotation.Nullable;
import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import androidx.work.ExistingWorkPolicy;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkManager;
import butterknife.BindView; import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import fr.free.nrw.commons.CommonsApplication; 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.bookmarks.BookmarkFragment; import fr.free.nrw.commons.bookmarks.BookmarkFragment;
import fr.free.nrw.commons.category.CategoryImagesCallback;
import fr.free.nrw.commons.explore.ExploreFragment; import fr.free.nrw.commons.explore.ExploreFragment;
import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.location.LocationServiceManager; import fr.free.nrw.commons.location.LocationServiceManager;
@ -29,7 +31,6 @@ import fr.free.nrw.commons.navtab.MoreBottomSheetLoggedOutFragment;
import fr.free.nrw.commons.navtab.NavTab; import fr.free.nrw.commons.navtab.NavTab;
import fr.free.nrw.commons.navtab.NavTabLayout; import fr.free.nrw.commons.navtab.NavTabLayout;
import fr.free.nrw.commons.navtab.NavTabLoggedOut; import fr.free.nrw.commons.navtab.NavTabLoggedOut;
import fr.free.nrw.commons.nearby.NearbyNotificationCardView;
import fr.free.nrw.commons.nearby.Place; import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.nearby.fragments.NearbyParentFragment; import fr.free.nrw.commons.nearby.fragments.NearbyParentFragment;
import fr.free.nrw.commons.nearby.fragments.NearbyParentFragment.NearbyParentFragmentInstanceReadyCallback; import fr.free.nrw.commons.nearby.fragments.NearbyParentFragment.NearbyParentFragmentInstanceReadyCallback;
@ -37,7 +38,7 @@ import fr.free.nrw.commons.notification.NotificationActivity;
import fr.free.nrw.commons.notification.NotificationController; import fr.free.nrw.commons.notification.NotificationController;
import fr.free.nrw.commons.quiz.QuizChecker; import fr.free.nrw.commons.quiz.QuizChecker;
import fr.free.nrw.commons.theme.BaseActivity; import fr.free.nrw.commons.theme.BaseActivity;
import fr.free.nrw.commons.upload.UploadService; import fr.free.nrw.commons.upload.worker.UploadWorker;
import fr.free.nrw.commons.utils.ViewUtilWrapper; import fr.free.nrw.commons.utils.ViewUtilWrapper;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
@ -122,7 +123,6 @@ public class MainActivity extends BaseActivity
loadFragment(ContributionsFragment.newInstance(),false); loadFragment(ContributionsFragment.newInstance(),false);
} }
setUpPager(); setUpPager();
initMain();
} }
} }
@ -257,13 +257,6 @@ public class MainActivity extends BaseActivity
} }
} }
private void initMain() {
//Do not remove this, this triggers the sync service
Intent uploadServiceIntent = new Intent(this, UploadService.class);
uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE);
startService(uploadServiceIntent);
}
@Override @Override
public void onBackPressed() { public void onBackPressed() {
if (contributionsFragment != null && activeFragment == ActiveFragment.CONTRIBUTIONS) { if (contributionsFragment != null && activeFragment == ActiveFragment.CONTRIBUTIONS) {
@ -323,13 +316,10 @@ public class MainActivity extends BaseActivity
viewUtilWrapper viewUtilWrapper
.showShortToast(getBaseContext(), getString(R.string.limited_connection_enabled)); .showShortToast(getBaseContext(), getString(R.string.limited_connection_enabled));
} else { } else {
Intent intent = new Intent(this, UploadService.class); WorkManager.getInstance(getApplicationContext()).enqueueUniqueWork(
intent.setAction(UploadService.PROCESS_PENDING_LIMITED_CONNECTION_MODE_UPLOADS); UploadWorker.class.getSimpleName(),
if (VERSION.SDK_INT >= VERSION_CODES.O) { ExistingWorkPolicy.KEEP, OneTimeWorkRequest.from(UploadWorker.class));
startForegroundService(intent);
} else {
startService(intent);
}
viewUtilWrapper viewUtilWrapper
.showShortToast(getBaseContext(), getString(R.string.limited_connection_disabled)); .showShortToast(getBaseContext(), getString(R.string.limited_connection_disabled));
} }

View file

@ -39,7 +39,7 @@ class WikipediaInstructionsDialogFragment : DialogFragment() {
callback?.onConfirmClicked(contribution, checkbox_copy_wikicode.isChecked) callback?.onConfirmClicked(contribution, checkbox_copy_wikicode.isChecked)
} }
dialog!!.window.setSoftInputMode( dialog!!.window?.setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN
) )
} }

View file

@ -8,6 +8,7 @@ import android.content.Context;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import dagger.android.HasAndroidInjector;
import javax.inject.Inject; import javax.inject.Inject;
import dagger.android.AndroidInjector; import dagger.android.AndroidInjector;
@ -25,6 +26,7 @@ import dagger.android.support.HasSupportFragmentInjector;
*/ */
public class ApplicationlessInjection public class ApplicationlessInjection
implements implements
HasAndroidInjector,
HasActivityInjector, HasActivityInjector,
HasFragmentInjector, HasFragmentInjector,
HasSupportFragmentInjector, HasSupportFragmentInjector,
@ -34,6 +36,7 @@ public class ApplicationlessInjection
private static ApplicationlessInjection instance = null; private static ApplicationlessInjection instance = null;
@Inject DispatchingAndroidInjector<Object> androidInjector;
@Inject DispatchingAndroidInjector<Activity> activityInjector; @Inject DispatchingAndroidInjector<Activity> activityInjector;
@Inject DispatchingAndroidInjector<BroadcastReceiver> broadcastReceiverInjector; @Inject DispatchingAndroidInjector<BroadcastReceiver> broadcastReceiverInjector;
@Inject DispatchingAndroidInjector<android.app.Fragment> fragmentInjector; @Inject DispatchingAndroidInjector<android.app.Fragment> fragmentInjector;
@ -49,6 +52,11 @@ public class ApplicationlessInjection
commonsApplicationComponent.inject(this); commonsApplicationComponent.inject(this);
} }
@Override
public AndroidInjector<Object> androidInjector() {
return androidInjector;
}
@Override @Override
public DispatchingAndroidInjector<Activity> activityInjector() { public DispatchingAndroidInjector<Activity> activityInjector() {
return activityInjector; return activityInjector;
@ -94,5 +102,4 @@ public class ApplicationlessInjection
return instance; return instance;
} }
} }

View file

@ -6,6 +6,7 @@ import fr.free.nrw.commons.explore.categories.CategoriesModule;
import fr.free.nrw.commons.navtab.MoreBottomSheetFragment; import fr.free.nrw.commons.navtab.MoreBottomSheetFragment;
import fr.free.nrw.commons.navtab.MoreBottomSheetLoggedOutFragment; import fr.free.nrw.commons.navtab.MoreBottomSheetLoggedOutFragment;
import fr.free.nrw.commons.navtab.NavTabLayout; import fr.free.nrw.commons.navtab.NavTabLayout;
import fr.free.nrw.commons.upload.worker.UploadWorker;
import javax.inject.Singleton; import javax.inject.Singleton;
import dagger.Component; import dagger.Component;
@ -47,6 +48,8 @@ import fr.free.nrw.commons.widget.PicOfDayAppWidget;
public interface CommonsApplicationComponent extends AndroidInjector<ApplicationlessInjection> { public interface CommonsApplicationComponent extends AndroidInjector<ApplicationlessInjection> {
void inject(CommonsApplication application); void inject(CommonsApplication application);
void inject(UploadWorker worker);
void inject(LoginActivity activity); void inject(LoginActivity activity);
void inject(SettingsFragment fragment); void inject(SettingsFragment fragment);

View file

@ -165,7 +165,7 @@ public class CommonsApplicationModule {
@Provides @Provides
public UploadController providesUploadController(SessionManager sessionManager, public UploadController providesUploadController(SessionManager sessionManager,
@Named("default_preferences") JsonKvStore kvStore, @Named("default_preferences") JsonKvStore kvStore,
Context context) { Context context, ContributionDao contributionDao) {
return new UploadController(sessionManager, context, kvStore); return new UploadController(sessionManager, context, kvStore);
} }

View file

@ -3,7 +3,6 @@ package fr.free.nrw.commons.di;
import dagger.Module; import dagger.Module;
import dagger.android.ContributesAndroidInjector; import dagger.android.ContributesAndroidInjector;
import fr.free.nrw.commons.auth.WikiAccountAuthenticatorService; import fr.free.nrw.commons.auth.WikiAccountAuthenticatorService;
import fr.free.nrw.commons.upload.UploadService;
/** /**
* This Class Represents the Module for dependency injection (using dagger) * This Class Represents the Module for dependency injection (using dagger)
@ -14,9 +13,6 @@ import fr.free.nrw.commons.upload.UploadService;
@SuppressWarnings({"WeakerAccess", "unused"}) @SuppressWarnings({"WeakerAccess", "unused"})
public abstract class ServiceBuilderModule { public abstract class ServiceBuilderModule {
@ContributesAndroidInjector
abstract UploadService bindUploadService();
@ContributesAndroidInjector @ContributesAndroidInjector
abstract WikiAccountAuthenticatorService bindWikiAccountAuthenticatorService(); abstract WikiAccountAuthenticatorService bindWikiAccountAuthenticatorService();

View file

@ -3,6 +3,7 @@ package fr.free.nrw.commons.repository;
import fr.free.nrw.commons.category.CategoriesModel; import fr.free.nrw.commons.category.CategoriesModel;
import fr.free.nrw.commons.category.CategoryItem; import fr.free.nrw.commons.category.CategoryItem;
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.filepicker.UploadableFile; import fr.free.nrw.commons.filepicker.UploadableFile;
import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.nearby.NearbyPlaces; import fr.free.nrw.commons.nearby.NearbyPlaces;
@ -36,18 +37,21 @@ public class UploadRepository {
private final DepictModel depictModel; private final DepictModel depictModel;
private static final double NEARBY_RADIUS_IN_KILO_METERS = 0.1; //100 meters private static final double NEARBY_RADIUS_IN_KILO_METERS = 0.1; //100 meters
private final ContributionDao contributionDao;
@Inject @Inject
public UploadRepository(UploadModel uploadModel, public UploadRepository(UploadModel uploadModel,
UploadController uploadController, UploadController uploadController,
CategoriesModel categoriesModel, CategoriesModel categoriesModel,
NearbyPlaces nearbyPlaces, NearbyPlaces nearbyPlaces,
DepictModel depictModel) { DepictModel depictModel,
ContributionDao contributionDao) {
this.uploadModel = uploadModel; this.uploadModel = uploadModel;
this.uploadController = uploadController; this.uploadController = uploadController;
this.categoriesModel = categoriesModel; this.categoriesModel = categoriesModel;
this.nearbyPlaces = nearbyPlaces; this.nearbyPlaces = nearbyPlaces;
this.depictModel = depictModel; this.depictModel = depictModel;
this.contributionDao=contributionDao;
} }
/** /**
@ -64,8 +68,14 @@ public class UploadRepository {
* *
* @param contribution * @param contribution
*/ */
public void startUpload(Contribution contribution) {
uploadController.startUpload(contribution); public void prepareMedia(Contribution contribution) {
uploadController.prepareMedia(contribution);
}
public void saveContribution(Contribution contribution) {
contributionDao.save(contribution).blockingAwait();
} }
/** /**
@ -77,13 +87,6 @@ public class UploadRepository {
return uploadModel.getUploads(); return uploadModel.getUploads();
} }
/**
* asks the RemoteDataSource to prepare the Upload Service
*/
public void prepareService() {
uploadController.prepareService();
}
/** /**
*Prepare for a fresh upload *Prepare for a fresh upload
*/ */

View file

@ -1,7 +1,6 @@
package fr.free.nrw.commons.upload; package fr.free.nrw.commons.upload;
import static fr.free.nrw.commons.contributions.ContributionController.ACTION_INTERNAL_UPLOADS; import static fr.free.nrw.commons.contributions.ContributionController.ACTION_INTERNAL_UPLOADS;
import static fr.free.nrw.commons.upload.UploadService.EXTRA_FILES;
import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT; import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT;
import android.Manifest; import android.Manifest;
@ -24,6 +23,9 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager; import androidx.viewpager.widget.ViewPager;
import androidx.work.ExistingWorkPolicy;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkManager;
import butterknife.BindView; import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import butterknife.OnClick; import butterknife.OnClick;
@ -42,6 +44,7 @@ import fr.free.nrw.commons.upload.depicts.DepictsFragment;
import fr.free.nrw.commons.upload.license.MediaLicenseFragment; import fr.free.nrw.commons.upload.license.MediaLicenseFragment;
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment; import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment;
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.UploadMediaDetailFragmentCallback; import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.UploadMediaDetailFragmentCallback;
import fr.free.nrw.commons.upload.worker.UploadWorker;
import fr.free.nrw.commons.utils.PermissionUtils; import fr.free.nrw.commons.utils.PermissionUtils;
import fr.free.nrw.commons.utils.ViewUtil; import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
@ -55,6 +58,7 @@ import javax.inject.Named;
import timber.log.Timber; import timber.log.Timber;
public class UploadActivity extends BaseActivity implements UploadContract.View, UploadBaseFragment.Callback { public class UploadActivity extends BaseActivity implements UploadContract.View, UploadBaseFragment.Callback {
@Inject @Inject
ContributionController contributionController; ContributionController contributionController;
@Inject @Inject
@ -104,6 +108,8 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
private List<UploadableFile> uploadableFiles = Collections.emptyList(); private List<UploadableFile> uploadableFiles = Collections.emptyList();
private int currentSelectedPosition = 0; private int currentSelectedPosition = 0;
public static final String EXTRA_FILES = "commons_image_exta";
@SuppressLint("CheckResult") @SuppressLint("CheckResult")
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
@ -279,6 +285,13 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
.getQuantityString(R.plurals.upload_count_title, uploadableFiles.size(), uploadableFiles.size())); .getQuantityString(R.plurals.upload_count_title, uploadableFiles.size(), uploadableFiles.size()));
} }
@Override
public void makeUploadRequest() {
WorkManager.getInstance(getApplicationContext()).enqueueUniqueWork(
UploadWorker.class.getSimpleName(),
ExistingWorkPolicy.KEEP, OneTimeWorkRequest.from(UploadWorker.class));
}
@Override @Override
public void askUserToLogIn() { public void askUserToLogIn() {
Timber.d("current session is null, asking user to login"); Timber.d("current session is null, asking user to login");

View file

@ -9,12 +9,11 @@ import com.google.gson.Gson;
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.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.worker.UploadWorker.NotificationUpdateProgressListener;
import io.reactivex.Observable; import io.reactivex.Observable;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.CompositeDisposable;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
@ -48,8 +47,6 @@ public class UploadClient {
private final FileUtilsWrapper fileUtilsWrapper; private final FileUtilsWrapper fileUtilsWrapper;
private final Gson gson; private final Gson gson;
private Map<String, Boolean> pauseUploads;
private final CompositeDisposable compositeDisposable = new CompositeDisposable(); private final CompositeDisposable compositeDisposable = new CompositeDisposable();
@Inject @Inject
@ -62,14 +59,13 @@ public class UploadClient {
this.pageContentsCreator = pageContentsCreator; this.pageContentsCreator = pageContentsCreator;
this.fileUtilsWrapper = fileUtilsWrapper; this.fileUtilsWrapper = fileUtilsWrapper;
this.gson = gson; this.gson = gson;
this.pauseUploads = new HashMap<>();
} }
/** /**
* 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<StashUploadResult> uploadFileToStash( public Observable<StashUploadResult> uploadFileToStash(
final Context context, final String filename, final Contribution contribution, final Context context, final String filename, final Contribution contribution,
final NotificationUpdateProgressListener notificationUpdater) throws IOException { final NotificationUpdateProgressListener notificationUpdater) throws IOException {
if (contribution.getChunkInfo() != null if (contribution.getChunkInfo() != null
@ -79,7 +75,7 @@ public class UploadClient {
contribution.getChunkInfo().getUploadResult().getFilekey())); contribution.getChunkInfo().getUploadResult().getFilekey()));
} }
pauseUploads.put(contribution.getPageId(), false); CommonsApplication.pauseUploads.put(contribution.getPageId(), false);
final File file = new File(contribution.getLocalUri().getPath()); final File file = new File(contribution.getLocalUri().getPath());
final List<File> fileChunks = fileUtilsWrapper.getFileChunks(context, file, CHUNK_SIZE); final List<File> fileChunks = fileUtilsWrapper.getFileChunks(context, file, CHUNK_SIZE);
@ -102,7 +98,7 @@ public class UploadClient {
final AtomicBoolean failures = new AtomicBoolean(); final AtomicBoolean failures = new AtomicBoolean();
compositeDisposable.add(Observable.fromIterable(fileChunks).forEach(chunkFile -> { compositeDisposable.add(Observable.fromIterable(fileChunks).forEach(chunkFile -> {
if (pauseUploads.get(contribution.getPageId()) || failures.get()) { if (CommonsApplication.pauseUploads.get(contribution.getPageId()) || failures.get()) {
return; return;
} }
@ -141,7 +137,7 @@ public class UploadClient {
})); }));
})); }));
if (pauseUploads.get(contribution.getPageId())) { if (CommonsApplication.pauseUploads.get(contribution.getPageId())) {
Timber.d("Upload stash paused %s", contribution.getPageId()); Timber.d("Upload stash paused %s", contribution.getPageId());
return Observable.just(new StashUploadResult(StashUploadState.PAUSED, null)); return Observable.just(new StashUploadResult(StashUploadState.PAUSED, null));
} else if (failures.get()) { } else if (failures.get()) {
@ -201,18 +197,6 @@ public class UploadClient {
} }
} }
/**
* Dispose the active disposable and sets the pause variable
* @param pageId
*/
public void pauseUpload(String pageId) {
pauseUploads.put(pageId, true);
if (!compositeDisposable.isDisposed()) {
compositeDisposable.dispose();
}
compositeDisposable.clear();
}
/** /**
* Converts string value to request body * Converts string value to request body
*/ */
@ -222,7 +206,7 @@ public class UploadClient {
} }
Observable<UploadResult> uploadFileFromStash(final Context context, public Observable<UploadResult> uploadFileFromStash(
final Contribution contribution, final Contribution contribution,
final String uniqueFileName, final String uniqueFileName,
final String fileKey) { final String fileKey) {

View file

@ -29,6 +29,8 @@ public interface UploadContract {
void onUploadMediaDeleted(int index); void onUploadMediaDeleted(int index);
void updateTopCardTitle(); void updateTopCardTitle();
void makeUploadRequest();
} }
public interface UserActionListener extends BasePresenter<View> { public interface UserActionListener extends BasePresenter<View> {

View file

@ -13,12 +13,16 @@ import android.net.Uri;
import android.os.IBinder; import android.os.IBinder;
import android.provider.MediaStore; import android.provider.MediaStore;
import android.text.TextUtils; import android.text.TextUtils;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkManager;
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.Contribution; import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.contributions.ContributionDao;
import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.settings.Prefs; import fr.free.nrw.commons.settings.Prefs;
import fr.free.nrw.commons.upload.worker.UploadWorker;
import fr.free.nrw.commons.utils.ViewUtil; import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.Single; import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
@ -35,62 +39,27 @@ import timber.log.Timber;
@Singleton @Singleton
public class UploadController { public class UploadController {
private UploadService uploadService;
private final SessionManager sessionManager; private final SessionManager sessionManager;
private final Context context; private final Context context;
private final JsonKvStore store; private final JsonKvStore store;
@Inject @Inject
public UploadController(final SessionManager sessionManager, public UploadController(final SessionManager sessionManager,
final Context context, final Context context,
final JsonKvStore store) { final JsonKvStore store) {
this.sessionManager = sessionManager; this.sessionManager = sessionManager;
this.context = context; this.context = context;
this.store = store; this.store = store;
} }
private boolean isUploadServiceConnected;
public ServiceConnection uploadServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(final ComponentName componentName, final IBinder binder) {
uploadService = ((UploadService.UploadServiceLocalBinder) binder).getService();
isUploadServiceConnected = true;
}
@Override
public void onServiceDisconnected(final ComponentName componentName) {
// this should never happen
isUploadServiceConnected = false;
Timber.e(new RuntimeException("UploadService died but the rest of the process did not!"));
}
};
/**
* Prepares the upload service.
*/
public void prepareService() {
final Intent uploadServiceIntent = new Intent(context, UploadService.class);
uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE);
context.startService(uploadServiceIntent);
context.bindService(uploadServiceIntent, uploadServiceConnection, Context.BIND_AUTO_CREATE);
}
/**
* Disconnects the upload service.
*/
public void cleanup() {
if (isUploadServiceConnected) {
context.unbindService(uploadServiceConnection);
}
}
/** /**
* Starts a new upload task. * Starts a new upload task.
* *
* @param contribution the contribution object * @param contribution the contribution object
*/ */
@SuppressLint("StaticFieldLeak") @SuppressLint("StaticFieldLeak")
public void startUpload(final Contribution contribution) { public void prepareMedia(final Contribution contribution) {
//Set creator, desc, and license //Set creator, desc, and license
// If author name is enabled and set, use it // If author name is enabled and set, use it
@ -118,20 +87,7 @@ public class UploadController {
final String license = store.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_3); final String license = store.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_3);
media.setLicense(license); media.setLicense(license);
uploadTask(contribution); buildUpload(contribution);
}
/**
* Initiates the upload task
* @param contribution
* @return
*/
private Disposable uploadTask(final Contribution contribution) {
return Single.just(contribution)
.map(this::buildUpload)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::upload);
} }
/** /**
@ -139,7 +95,7 @@ public class UploadController {
* @param contribution * @param contribution
* @return * @return
*/ */
private Contribution buildUpload(final Contribution contribution) { private void buildUpload(final Contribution contribution) {
final ContentResolver contentResolver = context.getContentResolver(); final ContentResolver contentResolver = context.getContentResolver();
contribution.setDataLength(resolveDataLength(contentResolver, contribution)); contribution.setDataLength(resolveDataLength(contentResolver, contribution));
@ -153,8 +109,6 @@ public class UploadController {
contribution.setDateCreated(resolveDateTakenOrNow(contentResolver, contribution)); contribution.setDateCreated(resolveDateTakenOrNow(contentResolver, contribution));
} }
} }
return contribution;
} }
private String resolveMimeType(final ContentResolver contentResolver, final Contribution contribution) { private String resolveMimeType(final ContentResolver contentResolver, final Contribution contribution) {
@ -202,15 +156,6 @@ public class UploadController {
new String[]{MediaStore.Images.ImageColumns.DATE_TAKEN}, null, null, null); new String[]{MediaStore.Images.ImageColumns.DATE_TAKEN}, null, null, null);
} }
/**
* When the contribution object is completely formed, the item is queued to the upload service
* @param contribution
*/
private void upload(final Contribution contribution) {
//Starts the upload. If commented out, user can proceed to next Fragment but upload doesn't happen
uploadService.queue(contribution);
}
/** /**
* Counts the number of bytes in {@code stream}. * Counts the number of bytes in {@code stream}.

View file

@ -69,7 +69,9 @@ public class UploadPresenter implements UploadContract.UserActionListener {
@Override @Override
public void onNext(Contribution contribution) { public void onNext(Contribution contribution) {
repository.startUpload(contribution); repository.prepareMedia(contribution);
contribution.setState(Contribution.STATE_QUEUED);
repository.saveContribution(contribution);
} }
@Override @Override
@ -83,6 +85,7 @@ public class UploadPresenter implements UploadContract.UserActionListener {
@Override @Override
public void onComplete() { public void onComplete() {
view.makeUploadRequest();
repository.cleanup(); repository.cleanup();
view.finish(); view.finish();
compositeDisposable.clear(); compositeDisposable.clear();
@ -119,7 +122,6 @@ public class UploadPresenter implements UploadContract.UserActionListener {
@Override @Override
public void onAttachView(UploadContract.View view) { public void onAttachView(UploadContract.View view) {
this.view = view; this.view = view;
repository.prepareService();
} }
@Override @Override

View file

@ -14,10 +14,10 @@ data class UploadResult(
val filename: String val filename: String
) : Parcelable { ) : Parcelable {
constructor(parcel: Parcel) : this( constructor(parcel: Parcel) : this(
parcel.readString(), parcel.readString()?:"",
parcel.readString(), parcel.readString()?:"",
parcel.readInt(), parcel.readInt()?:0,
parcel.readString() parcel.readString()?:""
) { ) {
} }

View file

@ -1,488 +0,0 @@
package fr.free.nrw.commons.upload;
import android.annotation.SuppressLint;
import android.app.PendingIntent;
import android.content.ContentResolver;
import android.content.Intent;
import android.graphics.BitmapFactory;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.contributions.ChunkInfo;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.contributions.ContributionDao;
import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.di.CommonsApplicationModule;
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.utils.ViewUtil;
import fr.free.nrw.commons.wikidata.WikidataEditService;
import io.reactivex.Completable;
import io.reactivex.Observable;
import io.reactivex.ObservableSource;
import io.reactivex.Scheduler;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.functions.Consumer;
import io.reactivex.functions.Function;
import io.reactivex.processors.PublishProcessor;
import io.reactivex.schedulers.Schedulers;
import java.io.IOException;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.inject.Inject;
import javax.inject.Named;
import timber.log.Timber;
public class UploadService extends CommonsDaggerService {
private static final String EXTRA_PREFIX = "fr.free.nrw.commons.upload";
private static final List<String> STASH_ERROR_CODES = Arrays
.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 PROCESS_PENDING_LIMITED_CONNECTION_MODE_UPLOADS = EXTRA_PREFIX + "process_limited_connection_mode_uploads";
public static final String EXTRA_FILES = EXTRA_PREFIX + ".files";
@Inject
WikidataEditService wikidataEditService;
@Inject
SessionManager sessionManager;
@Inject
ContributionDao contributionDao;
@Inject
UploadClient uploadClient;
@Inject
MediaClient mediaClient;
@Inject
@Named(CommonsApplicationModule.MAIN_THREAD)
Scheduler mainThreadScheduler;
@Inject
@Named(CommonsApplicationModule.IO_THREAD)
Scheduler ioThreadScheduler;
@Inject
@Named("default_preferences")
public JsonKvStore defaultKvStore;
private NotificationManagerCompat notificationManager;
private NotificationCompat.Builder curNotification;
private int toUpload;
private CompositeDisposable compositeDisposable;
private ServiceCallback serviceCallback;
/**
* The filePath names of unfinished uploads, used to prevent overwriting
*/
private Set<String> unfinishedUploads = new HashSet<>();
// DO NOT HAVE NOTIFICATION ID OF 0 FOR ANYTHING
// See http://stackoverflow.com/questions/8725909/startforeground-does-not-show-my-notification
// Seriously, Android?
public static final int NOTIFICATION_UPLOAD_IN_PROGRESS = 1;
public static final int NOTIFICATION_UPLOAD_FAILED = 3;
public static final int NOTIFICATION_UPLOAD_PAUSED = 4;
protected class NotificationUpdateProgressListener {
String notificationTag;
boolean notificationTitleChanged;
Contribution contribution;
String notificationProgressTitle;
String notificationFinishingTitle;
NotificationUpdateProgressListener(String notificationTag, String notificationProgressTitle,
String notificationFinishingTitle, Contribution contribution) {
this.notificationTag = notificationTag;
this.notificationProgressTitle = notificationProgressTitle;
this.notificationFinishingTitle = notificationFinishingTitle;
this.contribution = contribution;
}
public void onProgress(long transferred, long total) {
if (!notificationTitleChanged) {
curNotification.setContentTitle(notificationProgressTitle);
notificationTitleChanged = true;
contribution.setState(Contribution.STATE_IN_PROGRESS);
}
if (transferred == total) {
// Completed!
curNotification.setContentTitle(notificationFinishingTitle)
.setTicker(notificationFinishingTitle)
.setProgress(0, 100, true);
} else {
curNotification
.setProgress(100, (int) (((double) transferred / (double) total) * 100), false);
}
notificationManager
.notify(notificationTag, NOTIFICATION_UPLOAD_IN_PROGRESS, curNotification.build());
contribution.setTransferred(transferred);
compositeDisposable.add(contributionDao.update(contribution)
.subscribeOn(ioThreadScheduler)
.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.getPageId());
contribution.setState(Contribution.STATE_PAUSED);
compositeDisposable.add(contributionDao.update(contribution)
.subscribeOn(ioThreadScheduler)
.subscribe());
}
@Override
public void onDestroy() {
super.onDestroy();
compositeDisposable.dispose();
Timber.d("UploadService.onDestroy; %s are yet to be uploaded", unfinishedUploads);
}
public class UploadServiceLocalBinder extends Binder {
public UploadService getService() {
return UploadService.this;
}
}
private final IBinder localBinder = new UploadServiceLocalBinder();
private PublishProcessor<Contribution> contributionsToUpload;
@Override
public IBinder onBind(Intent intent) {
return localBinder;
}
@Override
public void onCreate() {
super.onCreate();
CommonsApplication.createNotificationChannel(getApplicationContext());
compositeDisposable = new CompositeDisposable();
notificationManager = NotificationManagerCompat.from(this);
curNotification = getNotificationBuilder(CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL);
contributionsToUpload = PublishProcessor.create();
compositeDisposable.add(contributionsToUpload.subscribe(this::handleUpload));
}
public void handleUpload(Contribution contribution) {
contribution.setState(Contribution.STATE_QUEUED);
contribution.setTransferred(0);
toUpload++;
if (curNotification != null && toUpload != 1) {
curNotification.setContentText(getResources()
.getQuantityString(R.plurals.uploads_pending_notification_indicator, toUpload, toUpload));
Timber.d("%d uploads left", toUpload);
notificationManager
.notify(contribution.getLocalUri().toString(), NOTIFICATION_UPLOAD_IN_PROGRESS,
curNotification.build());
}
compositeDisposable.add(contributionDao
.save(contribution)
.subscribeOn(ioThreadScheduler)
.subscribe(() -> uploadContribution(contribution)));
}
private boolean freshStart = true;
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);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
showUploadNotification();
if (ACTION_START_SERVICE.equals(intent.getAction()) && freshStart) {
compositeDisposable.add(contributionDao.updateStates(Contribution.STATE_FAILED,
new int[]{Contribution.STATE_QUEUED, Contribution.STATE_IN_PROGRESS})
.observeOn(mainThreadScheduler)
.subscribeOn(ioThreadScheduler)
.subscribe());
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;
}
private void showUploadNotification() {
compositeDisposable.add(contributionDao
.getPendingUploads(new int[]{Contribution.STATE_IN_PROGRESS, Contribution.STATE_QUEUED})
.subscribe(count -> {
if (count > 0) {
startForeground(NOTIFICATION_UPLOAD_IN_PROGRESS,
curNotification.setContentText(getText(R.string.starting_uploads)).build());
}
}));
}
@SuppressLint("StringFormatInvalid")
private NotificationCompat.Builder getNotificationBuilder(String channelId) {
return new NotificationCompat.Builder(this, channelId)
.setAutoCancel(true)
.setSmallIcon(R.drawable.ic_launcher)
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher))
.setAutoCancel(true)
.setOnlyAlertOnce(true)
.setProgress(100, 0, true)
.setOngoing(true)
.setContentIntent(
PendingIntent.getActivity(this, 0, new Intent(this, MainActivity.class), 0));
}
@SuppressLint("CheckResult")
private void uploadContribution(Contribution contribution) {
if (contribution.getLocalUri() == null || contribution.getLocalUri().getPath() == null) {
Timber.d("localUri/path is null");
return;
}
String notificationTag = contribution.getLocalUri().toString();
Timber.d("Before execution!");
final Media media = contribution.getMedia();
final String displayTitle = media.getDisplayTitle();
curNotification.setContentTitle(getString(R.string.upload_progress_notification_title_start,
displayTitle))
.setContentText(getResources()
.getQuantityString(R.plurals.uploads_pending_notification_indicator, toUpload,
toUpload))
.setTicker(getString(R.string.upload_progress_notification_title_in_progress,
displayTitle))
.setOngoing(true);
notificationManager
.notify(notificationTag, NOTIFICATION_UPLOAD_IN_PROGRESS, curNotification.build());
String filename = media.getFilename();
NotificationUpdateProgressListener notificationUpdater = new NotificationUpdateProgressListener(
notificationTag,
getString(R.string.upload_progress_notification_title_in_progress,
displayTitle),
getString(R.string.upload_progress_notification_title_finishing,
displayTitle),
contribution
);
Observable.fromCallable(() -> "Temp_" + contribution.hashCode() + filename)
.flatMap(stashFilename -> uploadClient
.uploadFileToStash(getApplicationContext(), stashFilename, contribution,
notificationUpdater))
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.doFinally(() -> {
if (filename != null) {
unfinishedUploads.remove(filename);
}
toUpload--;
if (toUpload == 0) {
// Sync modifications right after all uploads are processed
ContentResolver
.requestSync(sessionManager.getCurrentAccount(), BuildConfig.MODIFICATION_AUTHORITY,
new Bundle());
stopForeground(true);
}
})
.flatMap(uploadStash -> {
Timber.d("Upload stash result %s", uploadStash.toString());
notificationManager.cancel(notificationTag, NOTIFICATION_UPLOAD_IN_PROGRESS);
if (uploadStash.getState() == StashUploadState.SUCCESS) {
Timber.d("making sure of uniqueness of name: %s", filename);
String uniqueFilename = findUniqueFilename(filename);
unfinishedUploads.add(uniqueFilename);
return uploadClient.uploadFileFromStash(
getApplicationContext(),
contribution,
uniqueFilename,
uploadStash.getFileKey()).doOnError(new Consumer<Throwable>() {
@Override
public void accept(Throwable throwable) throws Exception {
Timber.e(throwable, "Error occurred in uploading file from stash");
if (STASH_ERROR_CODES.contains(throwable.getMessage())) {
clearChunks(contribution);
}
}
});
} else if (uploadStash.getState() == StashUploadState.PAUSED) {
showPausedNotification(contribution);
return Observable.never();
} else {
Timber.d("Contribution upload failed. Wikidata entity won't be edited");
showFailedNotification(contribution);
return Observable.never();
}
})
.subscribe(
uploadResult -> onUpload(contribution, notificationTag, uploadResult),
throwable -> {
Timber.w(throwable, "Exception during upload");
notificationManager.cancel(notificationTag, NOTIFICATION_UPLOAD_IN_PROGRESS);
showFailedNotification(contribution);
});
}
private void clearChunks(Contribution contribution) {
contribution.setChunkInfo(null);
compositeDisposable.add(contributionDao.update(contribution)
.subscribeOn(ioThreadScheduler)
.subscribe());
}
private void onUpload(Contribution contribution, String notificationTag,
UploadResult uploadResult) {
notificationManager.cancel(notificationTag, NOTIFICATION_UPLOAD_IN_PROGRESS);
if (uploadResult.isSuccessful()) {
onSuccessfulUpload(contribution, uploadResult);
} else {
Timber.d("Contribution upload failed. Wikidata entity won't be edited");
showFailedNotification(contribution);
}
}
private void onSuccessfulUpload(Contribution contribution, UploadResult uploadResult) {
compositeDisposable
.add(wikidataEditService.addDepictionsAndCaptions(uploadResult, contribution));
WikidataPlace wikidataPlace = contribution.getWikidataPlace();
if (wikidataPlace != null && wikidataPlace.getImageValue() == null) {
if (!contribution.hasInvalidLocation()) {
wikidataEditService.createClaim(wikidataPlace, uploadResult.getFilename(),
contribution.getMedia().getCaptions());
} else {
ViewUtil.showShortToast(this, getString(R.string.wikidata_edit_failure));
Timber
.d("Image location and nearby place location mismatched, so Wikidata item won't be edited");
}
}
saveCompletedContribution(contribution, uploadResult);
if(serviceCallback!=null) {
//this function update the tatol number media Uploaded or contributions
serviceCallback.updateUploadCount();
}
}
private void saveCompletedContribution(Contribution contribution, UploadResult uploadResult) {
compositeDisposable.add(mediaClient.getMedia("File:" + uploadResult.getFilename())
.map(contribution::completeWith)
.flatMapCompletable(
newContribution -> {
newContribution.setDateModified(new Date());
return contributionDao.saveAndDelete(contribution, newContribution);
})
.subscribe());
}
@SuppressLint("StringFormatInvalid")
@SuppressWarnings("deprecation")
private void showFailedNotification(final Contribution contribution) {
final String displayTitle = contribution.getMedia().getDisplayTitle();
curNotification.setTicker(getString(R.string.upload_failed_notification_title, displayTitle))
.setContentTitle(getString(R.string.upload_failed_notification_title, displayTitle))
.setContentText(getString(R.string.upload_failed_notification_subtitle))
.setProgress(0, 0, false)
.setOngoing(false);
notificationManager.notify(contribution.getLocalUri().toString(), NOTIFICATION_UPLOAD_FAILED,
curNotification.build());
contribution.setState(Contribution.STATE_FAILED);
contribution.setChunkInfo(null);
compositeDisposable.add(contributionDao
.update(contribution)
.subscribeOn(ioThreadScheduler)
.subscribe());
}
private void showPausedNotification(final Contribution contribution) {
final String displayTitle = contribution.getMedia().getDisplayTitle();
curNotification.setTicker(getString(R.string.upload_paused_notification_title, displayTitle))
.setContentTitle(getString(R.string.upload_paused_notification_title, displayTitle))
.setContentText(getString(R.string.upload_paused_notification_subtitle))
.setProgress(0, 0, false)
.setOngoing(false);
notificationManager.notify(contribution.getLocalUri().toString(), NOTIFICATION_UPLOAD_PAUSED,
curNotification.build());
contribution.setState(Contribution.STATE_PAUSED);
compositeDisposable.add(contributionDao
.update(contribution)
.subscribeOn(ioThreadScheduler)
.subscribe());
}
private String findUniqueFilename(String fileName) throws IOException {
String sequenceFileName;
for (int sequenceNumber = 1; true; sequenceNumber++) {
if (sequenceNumber == 1) {
sequenceFileName = fileName;
} else {
if (fileName.indexOf('.') == -1) {
// We really should have appended a filePath type suffix already.
// But... we might not.
sequenceFileName = fileName + " " + sequenceNumber;
} else {
Pattern regex = Pattern.compile("^(.*)(\\..+?)$");
Matcher regexMatcher = regex.matcher(fileName);
sequenceFileName = regexMatcher.replaceAll("$1 " + sequenceNumber + "$2");
}
}
if (!mediaClient.checkPageExistsUsingTitle(String.format("File:%s", sequenceFileName))
.blockingGet()
&& !unfinishedUploads.contains(sequenceFileName)) {
break;
}
}
return sequenceFileName;
}
public interface ServiceCallback{
void updateUploadCount() ;
}
public void setServiceCallback(ServiceCallback serviceCallback) {
this.serviceCallback = serviceCallback;
}
}

View file

@ -0,0 +1,480 @@
package fr.free.nrw.commons.upload.worker
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.BitmapFactory
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.mapbox.mapboxsdk.plugins.localization.BuildConfig
import dagger.android.ContributesAndroidInjector
import fr.free.nrw.commons.CommonsApplication
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.R
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.contributions.ChunkInfo
import fr.free.nrw.commons.contributions.Contribution
import fr.free.nrw.commons.contributions.ContributionDao
import fr.free.nrw.commons.di.ApplicationlessInjection
import fr.free.nrw.commons.media.MediaClient
import fr.free.nrw.commons.upload.StashUploadState
import fr.free.nrw.commons.upload.UploadClient
import fr.free.nrw.commons.upload.UploadResult
import fr.free.nrw.commons.wikidata.WikidataEditService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.util.*
import java.util.regex.Pattern
import javax.inject.Inject
import kotlin.collections.ArrayList
class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
CoroutineWorker(appContext, workerParams) {
private var notificationManager: NotificationManagerCompat? = null
@Inject
lateinit var wikidataEditService: WikidataEditService
@Inject
lateinit var sessionManager: SessionManager
@Inject
lateinit var contributionDao: ContributionDao
@Inject
lateinit var uploadClient: UploadClient
@Inject
lateinit var mediaClient: MediaClient
private val PROCESSING_UPLOADS_NOTIFICATION_TAG = BuildConfig.APPLICATION_ID + " : upload_tag"
private val PROCESSING_UPLOADS_NOTIFICATION_ID = 101
//Attributes of the current-upload notification
private var currentNotificationID: Int = -1// lateinit is not allowed with primitives
private lateinit var currentNotificationTag: String
private var curentNotification: NotificationCompat.Builder
private val statesToProcess= ArrayList<Int>()
private val STASH_ERROR_CODES = Arrays
.asList(
"uploadstash-file-not-found",
"stashfailed",
"verification-error",
"chunk-too-small"
)
init {
ApplicationlessInjection
.getInstance(appContext)
.commonsApplicationComponent
.inject(this)
curentNotification =
getNotificationBuilder(CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL)!!
statesToProcess.add(Contribution.STATE_QUEUED)
statesToProcess.add(Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE)
}
@dagger.Module
interface Module {
@ContributesAndroidInjector
fun worker(): UploadWorker
}
open inner class NotificationUpdateProgressListener(
private var notificationFinishingTitle: String?,
var contribution: Contribution?
) {
fun onProgress(transferred: Long, total: Long) {
if (transferred == total) {
// Completed!
curentNotification.setContentTitle(notificationFinishingTitle)
.setProgress(0, 100, true)
} else {
curentNotification
.setProgress(
100,
(transferred.toDouble() / total.toDouble() * 100).toInt(),
false
)
}
notificationManager?.notify(
currentNotificationTag,
currentNotificationID,
curentNotification.build()!!
)
contribution!!.transferred = transferred
contributionDao.update(contribution).blockingAwait()
}
open fun onChunkUploaded(contribution: Contribution, chunkInfo: ChunkInfo?) {
contribution.chunkInfo = chunkInfo
contributionDao.update(contribution).blockingAwait()
}
}
private fun getNotificationBuilder(channelId: String): NotificationCompat.Builder? {
return NotificationCompat.Builder(appContext, channelId)
.setAutoCancel(true)
.setSmallIcon(R.drawable.ic_launcher)
.setLargeIcon(
BitmapFactory.decodeResource(
appContext.resources,
R.drawable.ic_launcher
)
)
.setAutoCancel(true)
.setOnlyAlertOnce(true)
.setProgress(100, 0, true)
.setOngoing(true)
}
override suspend fun doWork(): Result {
notificationManager = NotificationManagerCompat.from(appContext)
val processingUploads = getNotificationBuilder(
CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL
)!!
withContext(Dispatchers.IO) {
//Doing this so that retry requests do not create new work requests and while a work is
// already running, all the requests should go through this, so kind of a queue
while (contributionDao.getContribution(statesToProcess)
.blockingGet().isNotEmpty()
) {
val queuedContributions = contributionDao.getContribution(statesToProcess)
.blockingGet()
//Showing initial notification for the number of uploads being processed
processingUploads.setContentTitle(appContext.getString(R.string.starting_uploads))
processingUploads.setContentText(
appContext.resources.getQuantityString(
R.plurals.starting_multiple_uploads,
queuedContributions.size,
queuedContributions.size
)
)
notificationManager?.notify(
PROCESSING_UPLOADS_NOTIFICATION_TAG,
PROCESSING_UPLOADS_NOTIFICATION_ID,
processingUploads.build()
)
queuedContributions.asFlow().map { contribution ->
/**
* If the limited connection mode is on, lets iterate through the queued
* contributions
* and set the state as STATE_QUEUED_LIMITED_CONNECTION_MODE ,
* otherwise proceed with the upload
*/
if(isLimitedConnectionModeEnabled()){
if (contribution.state == Contribution.STATE_QUEUED) {
contribution.state = Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE
contributionDao.save(contribution)
}
} else {
contribution.transferred = 0
contribution.state = Contribution.STATE_IN_PROGRESS
contributionDao.save(contribution)
uploadContribution(contribution = contribution)
}
}.collect()
//Dismiss the global notification
notificationManager?.cancel(
PROCESSING_UPLOADS_NOTIFICATION_TAG,
PROCESSING_UPLOADS_NOTIFICATION_ID
)
//No need to keep looking if the limited connection mode is on,
//If the user toggles it, the work manager will be started again
if(isLimitedConnectionModeEnabled()){
break;
}
}
}
//TODO make this smart, think of handling retries in the future
return Result.success()
}
/**
* Returns true is the limited connection mode is enabled
*/
private fun isLimitedConnectionModeEnabled(): Boolean {
return sessionManager.getPreference(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED)
}
/**
* Upload the contribution
* @param contribution
*/
@SuppressLint("StringFormatInvalid")
private suspend fun uploadContribution(contribution: Contribution) {
if (contribution.localUri == null || contribution.localUri.path == null) {
Timber.e("""upload: ${contribution.media.filename} failed, file path is null""")
}
val media = contribution.media
val displayTitle = contribution.media.displayTitle
currentNotificationTag = contribution.localUri.toString()
currentNotificationID =
(contribution.localUri.toString() + contribution.media.filename).hashCode()
curentNotification
getNotificationBuilder(CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL)!!
curentNotification.setContentTitle(
appContext.getString(
R.string.upload_progress_notification_title_start,
displayTitle
)
)
notificationManager?.notify(
currentNotificationTag,
currentNotificationID,
curentNotification.build()!!
)
val filename = media.filename
val notificationProgressUpdater = NotificationUpdateProgressListener(
appContext.getString(
R.string.upload_progress_notification_title_finishing,
displayTitle
),
contribution
)
try {
//Upload the file to stash
val stashUploadResult = uploadClient.uploadFileToStash(
appContext, filename, contribution, notificationProgressUpdater
).blockingSingle()
when (stashUploadResult.state) {
StashUploadState.SUCCESS -> {
//If the stash upload succeeds
Timber.d("Upload to stash success for fileName: $filename")
Timber.d("Ensure uniqueness of filename");
val uniqueFileName = findUniqueFileName(filename!!)
try {
//Upload the file from stash
val uploadResult = uploadClient.uploadFileFromStash(
contribution, uniqueFileName, stashUploadResult.fileKey
).blockingSingle()
if (uploadResult.isSuccessful()) {
Timber.d(
"Stash Upload success..proceeding to make wikidata edit"
)
if(contribution.wikidataPlace==null){
Timber.d(
"WikiDataEdit not required, upload success"
)
saveCompletedContribution(contribution,uploadResult)
showSuccessNotification(contribution)
}else{
Timber.d(
"WikiDataEdit not required, making wikidata edit"
)
makeWikiDataEdit(uploadResult, contribution)
}
} else {
Timber.e("Stash Upload failed")
showFailedNotification(contribution)
contribution.state = Contribution.STATE_FAILED
contribution.chunkInfo = null
contributionDao.save(contribution).blockingAwait()
}
}catch (exception : Exception){
Timber.e(exception)
Timber.e("Upload from stash failed for contribution : $filename")
showFailedNotification(contribution)
if (STASH_ERROR_CODES.contains(exception.message)) {
clearChunks(contribution)
}
}
}
StashUploadState.PAUSED -> {
showPausedNotification(contribution)
contribution.state = Contribution.STATE_PAUSED
contributionDao.save(contribution).blockingGet()
}
else -> {
Timber.e("""upload file to stash failed with status: ${stashUploadResult.state}""")
showFailedNotification(contribution)
contribution.state = Contribution.STATE_FAILED
contribution.chunkInfo = null
contributionDao.save(contribution).blockingAwait()
}
}
}catch (exception: Exception){
Timber.e(exception)
Timber.e("Stash upload failed for contribution: $filename")
showFailedNotification(contribution)
}
}
private fun clearChunks(contribution: Contribution) {
contribution.chunkInfo=null
contributionDao.save(contribution).blockingAwait()
}
/**
* Make the WikiData Edit, if applicable
*/
private suspend fun makeWikiDataEdit(uploadResult: UploadResult, contribution: Contribution) {
wikidataEditService.addDepictionsAndCaptions(uploadResult, contribution)
val wikiDataPlace = contribution.wikidataPlace
if (wikiDataPlace != null && wikiDataPlace.imageValue == null) {
if (!contribution.hasInvalidLocation()) {
var revisionID: Long?=null
try {
revisionID = wikidataEditService.createClaim(
wikiDataPlace, uploadResult.filename,
contribution.media.captions
)
if (null != revisionID) {
showSuccessNotification(contribution)
}
}catch (exception: Exception){
Timber.e(exception)
}
withContext(Dispatchers.Main) {
wikidataEditService.handleImageClaimResult(
contribution.wikidataPlace,
revisionID
)
}
} else {
withContext(Dispatchers.Main) {
wikidataEditService.handleImageClaimResult(
contribution.wikidataPlace, null
)
}
}
}
saveCompletedContribution(contribution, uploadResult)
}
private fun saveCompletedContribution(contribution: Contribution, uploadResult: UploadResult) {
val contributionFromUpload = mediaClient.getMedia("File:" + uploadResult.filename)
.map { media: Media? -> contribution.completeWith(media!!) }
.blockingGet()
contributionFromUpload.dateModified=Date()
contributionDao.deleteAndSaveContribution(contribution, contributionFromUpload)
}
private fun findUniqueFileName(fileName: String): String {
var sequenceFileName: String?
var sequenceNumber = 1
while (true) {
sequenceFileName = if (sequenceNumber == 1) {
fileName
} else {
if (fileName.indexOf('.') == -1) {
"$fileName $sequenceNumber"
} else {
val regex =
Pattern.compile("^(.*)(\\..+?)$")
val regexMatcher = regex.matcher(fileName)
regexMatcher.replaceAll("$1 $sequenceNumber$2")
}
}
if (!mediaClient.checkPageExistsUsingTitle(
String.format(
"File:%s",
sequenceFileName
)
)
.blockingGet()
) {
break
}
sequenceNumber++
}
return sequenceFileName!!
}
/**
* Notify that the current upload has succeeded
* @param contribution
*/
@SuppressLint("StringFormatInvalid")
private fun showSuccessNotification(contribution: Contribution) {
val displayTitle = contribution.media.displayTitle
contribution.state=Contribution.STATE_COMPLETED
curentNotification.setContentTitle(
appContext.getString(
R.string.upload_completed_notification_title,
displayTitle
)
)
.setContentText(appContext.getString(R.string.upload_completed_notification_text))
.setProgress(0, 0, false)
.setOngoing(false)
notificationManager?.notify(
currentNotificationTag, currentNotificationID,
curentNotification.build()
)
}
/**
* Notify that the current upload has failed
* @param contribution
*/
@SuppressLint("StringFormatInvalid")
private fun showFailedNotification(contribution: Contribution) {
val displayTitle = contribution.media.displayTitle
curentNotification.setContentTitle(
appContext.getString(
R.string.upload_failed_notification_title,
displayTitle
)
)
.setContentText(appContext.getString(R.string.upload_failed_notification_subtitle))
.setProgress(0, 0, false)
.setOngoing(false)
notificationManager?.notify(
currentNotificationTag, currentNotificationID,
curentNotification.build()
)
}
/**
* Notify that the current upload is paused
* @param contribution
*/
private fun showPausedNotification(contribution: Contribution) {
val displayTitle = contribution.media.displayTitle
curentNotification.setContentTitle(
appContext.getString(
R.string.upload_paused_notification_title,
displayTitle
)
)
.setContentText(appContext.getString(R.string.upload_paused_notification_subtitle))
.setProgress(0, 0, false)
.setOngoing(false)
notificationManager!!.notify(
currentNotificationTag, currentNotificationID,
curentNotification.build()
)
}
}

View file

@ -16,13 +16,11 @@ import fr.free.nrw.commons.upload.WikidataPlace;
import fr.free.nrw.commons.utils.ConfigUtils; import fr.free.nrw.commons.utils.ConfigUtils;
import fr.free.nrw.commons.utils.ViewUtil; import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.Observable; import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable; import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
@ -139,17 +137,17 @@ public class WikidataEditService {
} }
} }
public void createClaim(@Nullable final WikidataPlace wikidataPlace, final String fileName, final public Long createClaim(@Nullable final WikidataPlace wikidataPlace, final String fileName, final
Map<String, String> captions) { Map<String, String> captions) {
if (!(directKvStore.getBoolean("Picture_Has_Correct_Location", true))) { if (!(directKvStore.getBoolean("Picture_Has_Correct_Location", true))) {
Timber Timber
.d("Image location and nearby place location mismatched, so Wikidata item won't be edited"); .d("Image location and nearby place location mismatched, so Wikidata item won't be edited");
return; return null;
} }
addImageAndMediaLegends(wikidataPlace, fileName, captions); return addImageAndMediaLegends(wikidataPlace, fileName, captions);
} }
public void addImageAndMediaLegends(final WikidataItem wikidataItem, final String fileName, public Long addImageAndMediaLegends(final WikidataItem wikidataItem, final String fileName,
final Map<String, String> captions) { final Map<String, String> captions) {
final Snak_partial p18 = new Snak_partial("value", WikidataProperties.IMAGE.getPropertyName(), final Snak_partial p18 = new Snak_partial("value", WikidataProperties.IMAGE.getPropertyName(),
new ValueString(fileName.replace("File:", ""))); new ValueString(fileName.replace("File:", "")));
@ -166,17 +164,10 @@ public class WikidataEditService {
Collections.singletonMap(WikidataProperties.MEDIA_LEGENDS.getPropertyName(), snaks), Collections.singletonMap(WikidataProperties.MEDIA_LEGENDS.getPropertyName(), snaks),
Arrays.asList(WikidataProperties.MEDIA_LEGENDS.getPropertyName())); Arrays.asList(WikidataProperties.MEDIA_LEGENDS.getPropertyName()));
wikidataClient.setClaim(claim, COMMONS_APP_TAG).subscribeOn(Schedulers.io()) return wikidataClient.setClaim(claim, COMMONS_APP_TAG).blockingSingle();
.observeOn(AndroidSchedulers.mainThread())
.subscribe(revisionId -> handleImageClaimResult(wikidataItem, String.valueOf(revisionId)),
throwable -> {
Timber.e(throwable, "Error occurred while making claim");
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
});
;
} }
private void handleImageClaimResult(final WikidataItem wikidataItem, final String revisionId) { public void handleImageClaimResult(final WikidataItem wikidataItem, final Long revisionId) {
if (revisionId != null) { if (revisionId != null) {
if (wikidataEditListener != null) { if (wikidataEditListener != null) {
wikidataEditListener.onSuccessfulWikidataEdit(); wikidataEditListener.onSuccessfulWikidataEdit();

View file

@ -76,6 +76,8 @@
<ImageView <ImageView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:padding="12dp"
android:scaleType="centerInside"
app:srcCompat="@drawable/ic_baseline_person_14"/> app:srcCompat="@drawable/ic_baseline_person_14"/>
<TextView <TextView
@ -114,8 +116,8 @@
<ImageButton <ImageButton
android:id="@+id/cancelButton" android:id="@+id/cancelButton"
android:layout_width="wrap_content" android:layout_width="48dp"
android:layout_height="wrap_content" android:layout_height="48dp"
android:layout_marginEnd="@dimen/tiny_padding" android:layout_marginEnd="@dimen/tiny_padding"
android:layout_toStartOf="@id/retryButton" android:layout_toStartOf="@id/retryButton"
android:background="@android:color/transparent" android:background="@android:color/transparent"
@ -126,8 +128,8 @@
<ImageButton <ImageButton
android:id="@+id/retryButton" android:id="@+id/retryButton"
android:layout_width="wrap_content" android:layout_width="48dp"
android:layout_height="wrap_content" android:layout_height="48dp"
android:layout_marginEnd="@dimen/tiny_padding" android:layout_marginEnd="@dimen/tiny_padding"
android:layout_toStartOf="@id/wikipediaButton" android:layout_toStartOf="@id/wikipediaButton"
android:background="@android:color/transparent" android:background="@android:color/transparent"
@ -138,8 +140,8 @@
<ImageButton <ImageButton
android:id="@+id/wikipediaButton" android:id="@+id/wikipediaButton"
android:layout_width="wrap_content" android:layout_width="48dp"
android:layout_height="wrap_content" android:layout_height="48dp"
android:layout_alignParentEnd="true" android:layout_alignParentEnd="true"
android:layout_marginEnd="@dimen/tiny_padding" android:layout_marginEnd="@dimen/tiny_padding"
android:background="@android:color/transparent" android:background="@android:color/transparent"

View file

@ -24,7 +24,12 @@
<item quantity="one">%1$d tập tin đã tải lên</item> <item quantity="one">%1$d tập tin đã tải lên</item>
<item quantity="other">%1$d tập tin đã tải lên</item> <item quantity="other">%1$d tập tin đã tải lên</item>
</plurals> </plurals>
<string name="starting_multiple_uploads">Đang bắt đầu tải lên %1$d tập tin</string>
<plurals name="starting_multiple_uploads">
<item quantity="one">Đang bắt đầu tải lên %1$d tập tin</item>
<item quantity="other">Đang bắt đầu tải lên %1$d tập tin</item>
</plurals>
<plurals name="multiple_uploads_title"> <plurals name="multiple_uploads_title">
<item quantity="one">%1$d tập tin đã tải lên</item> <item quantity="one">%1$d tập tin đã tải lên</item>
<item quantity="other">%1$d tập tin đã tải lên</item> <item quantity="other">%1$d tập tin đã tải lên</item>

View file

@ -10,12 +10,12 @@
</plurals> </plurals>
<string name="starting_uploads"> Starting Uploads</string> <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">Processing %d upload</item>
<item quantity="other">Starting %1$d uploads</item> <item quantity="other">Processing %d uploads</item>
</plurals> </plurals>
<plurals name="multiple_uploads_title"> <plurals name="multiple_uploads_title">
<item quantity="one">%1$d upload</item> <item quantity="one">%d upload</item>
<item quantity="other">%1$d uploads</item> <item quantity="other">%d uploads</item>
</plurals> </plurals>
<plurals name="share_license_summary"> <plurals name="share_license_summary">
<item quantity="one">This image will be licensed under %1$s</item> <item quantity="one">This image will be licensed under %1$s</item>
@ -54,7 +54,7 @@
<string name="uploading_queued">Upload queued (limited connection mode enabled)</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">Uploading file: %s</string>
<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>

View file

@ -124,7 +124,7 @@ class BookmarkPictureDaoTest {
whenever(client.query(any(), any(), any(), any(), anyOrNull())).thenReturn(createCursor(1)) whenever(client.query(any(), any(), any(), any(), anyOrNull())).thenReturn(createCursor(1))
assertFalse(testObject.updateBookmark(exampleBookmark)) assertFalse(testObject.updateBookmark(exampleBookmark))
verify(client).delete(eq(exampleBookmark.contentUri), isNull(), isNull()) verify(client).delete(eq(exampleBookmark.contentUri!!), isNull(), isNull())
} }
@Test @Test

View file

@ -2,28 +2,21 @@ package fr.free.nrw.commons.contributions
import android.database.Cursor import android.database.Cursor
import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.loader.content.CursorLoader import androidx.loader.content.CursorLoader
import androidx.loader.content.Loader import androidx.loader.content.Loader
import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever import com.nhaarman.mockitokotlin2.whenever
import io.reactivex.Completable import io.reactivex.Completable
import io.reactivex.Scheduler
import io.reactivex.Single
import io.reactivex.schedulers.TestScheduler import io.reactivex.schedulers.TestScheduler
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.mockito.ArgumentMatchers import org.mockito.ArgumentMatchers
import org.mockito.ArgumentMatchers.*
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito import org.mockito.Mockito
import org.mockito.MockitoAnnotations import org.mockito.MockitoAnnotations
import java.util.concurrent.TimeUnit
/** /**
* The unit test class for ContributionsPresenter * The unit test class for ContributionsPresenter
@ -58,7 +51,7 @@ class ContributionsPresenterTest {
scheduler=TestScheduler() scheduler=TestScheduler()
cursor = Mockito.mock(Cursor::class.java) cursor = Mockito.mock(Cursor::class.java)
contribution = Mockito.mock(Contribution::class.java) contribution = Mockito.mock(Contribution::class.java)
contributionsPresenter = ContributionsPresenter(repository,scheduler,scheduler) contributionsPresenter = ContributionsPresenter(repository, scheduler)
loader = Mockito.mock(CursorLoader::class.java) loader = Mockito.mock(CursorLoader::class.java)
contributionsPresenter.onAttachView(view) contributionsPresenter.onAttachView(view)
liveData=MutableLiveData() liveData=MutableLiveData()

View file

@ -152,7 +152,7 @@ class RecentSearchesDaoTest {
testObject.save(recentSearch) testObject.save(recentSearch)
verify(client).update(eq(recentSearch.contentUri), captor.capture(), isNull(), isNull()) verify(client).update(eq(recentSearch.contentUri!!), captor.capture(), isNull(), isNull())
captor.firstValue.let { cv -> captor.firstValue.let { cv ->
assertEquals(2, cv.size()) assertEquals(2, cv.size())
assertEquals(recentSearch.query, cv.getAsString(COLUMN_NAME)) assertEquals(recentSearch.query, cv.getAsString(COLUMN_NAME))

View file

@ -1,29 +1,28 @@
package fr.free.nrw.commons.upload package fr.free.nrw.commons.upload
import android.content.ComponentName import android.content.ContentResolver
import android.content.Context import android.content.Context
import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.whenever import com.nhaarman.mockitokotlin2.whenever
import fr.free.nrw.commons.Media import fr.free.nrw.commons.Media
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.contributions.Contribution import fr.free.nrw.commons.contributions.Contribution
import fr.free.nrw.commons.kvstore.JsonKvStore import fr.free.nrw.commons.kvstore.JsonKvStore
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.mockito.InjectMocks import org.mockito.InjectMocks
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito.`when`
import org.mockito.Mockito.mock import org.mockito.Mockito.mock
import org.mockito.MockitoAnnotations import org.mockito.MockitoAnnotations
class UploadControllerTest { class UploadControllerTest {
@Mock
internal var sessionManager: SessionManager? = null
@Mock @Mock
internal var context: Context? = null internal var context: Context? = null
@Mock @Mock
internal var prefs: JsonKvStore? = null internal lateinit var store: JsonKvStore
@Mock
internal lateinit var contentResolver: ContentResolver
@InjectMocks @InjectMocks
var uploadController: UploadController? = null var uploadController: UploadController? = null
@ -31,20 +30,6 @@ class UploadControllerTest {
@Before @Before
fun setup() { fun setup() {
MockitoAnnotations.initMocks(this) MockitoAnnotations.initMocks(this)
val uploadService = mock(UploadService::class.java)
val binder = mock(UploadService.UploadServiceLocalBinder::class.java)
`when`(binder.service).thenReturn(uploadService)
uploadController!!.uploadServiceConnection.onServiceConnected(mock(ComponentName::class.java), binder)
}
@Test
fun prepareService() {
uploadController!!.prepareService()
}
@Test
fun cleanup() {
uploadController!!.cleanup()
} }
@Test @Test
@ -53,6 +38,7 @@ class UploadControllerTest {
val media = mock<Media>() val media = mock<Media>()
whenever(contribution.media).thenReturn(media) whenever(contribution.media).thenReturn(media)
whenever(media.author).thenReturn("Creator") whenever(media.author).thenReturn("Creator")
uploadController!!.startUpload(contribution) whenever(context?.contentResolver).thenReturn(contentResolver)
uploadController?.prepareMedia(contribution)
} }
} }

View file

@ -19,7 +19,7 @@ android.enableBuildCache=true
KOTLIN_VERSION=1.3.72 KOTLIN_VERSION=1.3.72
BUTTERKNIFE_VERSION=10.1.0 BUTTERKNIFE_VERSION=10.1.0
LEAK_CANARY_VERSION=1.6.2 LEAK_CANARY_VERSION=1.6.2
DAGGER_VERSION=2.21 DAGGER_VERSION=2.23
ROOM_VERSION=2.2.3 ROOM_VERSION=2.2.3
PREFERENCE_VERSION=1.1.0 PREFERENCE_VERSION=1.1.0
CORE_KTX_VERSION=1.2.0 CORE_KTX_VERSION=1.2.0