mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 20:33:53 +01:00
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:
parent
fd2a7a9c56
commit
ecbff7e3b8
36 changed files with 692 additions and 802 deletions
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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> {
|
||||||
|
|
|
||||||
|
|
@ -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}.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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()?:""
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue