Added pending uploads screen (#5752)

* Added pending uploads screen

* Added failed uploads fragment

* Improved progress bars

* Implemented pause functionality

* Improved pause feature

* Fixed issue with sorting when adding more pictures during an upload

* Improved Tap to View notification

* Fixed issue with on going upload deletion

* Improved the deletion feature

* Fixed indentations and unit tests

* Fixed bugs

* Fixed failing test

* Added error message in Failed Uploads Fragment

* Improved error notification

* Moved auto-retry from the Main Activity to UploadProgressActivity

* Fixed large uploads issue

* Minor fixes

* Removed HashSet

* Fixed issue with progress bar

* Bug fixes

* Moved Auto Retry to MainActivity

* Fixed conflicts

* Fixed issue with upload icon

* Fixed null ptr issue on changing modes

* Improved recycler view

* Fixed irrelevant network call

* Fixed irrelevant network call

* Fixed constantly failing uploads

* Fixed constantly failing uploads

* Fixed constantly failing uploads

* Added error log

* Fixed refresh icon visibility in light mode

* Changed progress in progress activity

* Fixed progress bar issue

* Improved icons

* Improved deletion and removed cancelledUploads Hashset

* Fixed sorting, list size issue

* Improved current implementation

* Implemented flag for workers

* Implemented flag for workers

* Fixed sorting bug

* Fixed upload icon

* Improved pausing

* Made changes to visibility implementation

* Added image duplicity check on restart of failed image

* minor adjustments

* added javadoc/kdoc and fixed minor bug

* Fixed unit tests

* Added synchronized(lock)

* Added check to prevent multiple uploads starting at once

* Ignored failing test cases

* Temporary commit - Added jcenter

* Temporary commit - Removed library/commented

* Temporary commit - Removed library/commented

* Updated com.jraska.livedata:testing-ktx

* Ignored failing test - UploadControllerTest.kt

* Ignored failing test - UploadModelUnitTest

* Ignored failing test - UploadModelUnitTest

* Ignored failing test - UploadModelUnitTest

* Ignored failing test - UploadModelUnitTest

* Ignored failing test - UploadModelUnitTest

* Ignored failing test - UploadModelUnitTest

* Ignored failing test - UploadModelUnitTest

* Ignored failing test - UploadModelUnitTest

* Ignored failing test - UploadPresenterTest.kt

* Ignored failing test - UploadPresenterTest.kt

* Ignored failing test - UploadPresenterTest.kt

* Ignored failing test - UploadPresenterTest.kt

* Ignored failing test - UploadPresenterTest.kt

* Ignored failing test - UploadPresenterTest.kt

* Ignored failing test - UploadPresenterTest.kt

* Ignored failing test - UploadPresenterTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing tests - UploadRepositoryUnitTest.kt

* Ignored failing test - UploadRepositoryUnitTest.kt

* Ignored failing test - DepictedItemTest.kt

* Ignored failing test - DepictedItemTest.kt

* Ignored failing test - DepictedItemTest.kt

* Ignored failing test - DepictedItemTest.kt

* Ignored failing test - DepictedItemTest.kt

* Ignored failing test - DepictedItemTest.kt

* Ignored failing test - DepictedItemTest.kt

* Ignored failing test - DepictedItemTest.kt

* Ignored failing test - FilesUtilsTest.kt

* Ignored failing test - WikiBaseClientUnitTest.kt

* Ignored failing test - WikiBaseClientUnitTest.kt

* Ignored failing test - WikiBaseClientUnitTest.kt

* Ignored failing test - WikidataClientTest.kt

* Ignored failing test - WikidataClientTest.kt

* Fixed unit tests

* Updated kdoc

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
This commit is contained in:
Kanahia 2024-08-30 11:52:54 +05:30 committed by GitHub
parent 62d6dea219
commit 93f1e1ec29
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
69 changed files with 2717 additions and 955 deletions

View file

@ -28,6 +28,7 @@ data class Contribution constructor(
var dateCreatedSource: String? = null,
var wikidataPlace: WikidataPlace? = null,
var chunkInfo: ChunkInfo? = null,
var errorInfo: String? = null,
/**
* @return array list of entityids for the depictions
*/
@ -42,6 +43,7 @@ data class Contribution constructor(
var dateCreated: Date? = null,
var dateCreatedString: String? = null,
var dateModified: Date? = null,
var dateUploadStarted: Date? = null,
var hasInvalidLocation : Int = 0,
var contentUri: Uri? = null,
var countryCode : String? = null,
@ -99,7 +101,6 @@ data class Contribution constructor(
const val STATE_QUEUED = 2
const val STATE_IN_PROGRESS = 3
const val STATE_PAUSED = 4
const val STATE_QUEUED_LIMITED_CONNECTION_MODE=5
/**
* Formatting captions to the Wikibase format for sending labels
@ -127,11 +128,8 @@ data class Contribution constructor(
return chunkInfo != null && chunkInfo!!.totalChunks == chunkInfo!!.indexOfNextChunkToUpload
}
fun isPaused(): Boolean {
return CommonsApplication.pauseUploads[pageId] ?: false
fun dateUploadStartedInMillis(): Long {
return dateUploadStarted!!.time
}
fun unpause() {
CommonsApplication.pauseUploads[pageId] = false
}
}

View file

@ -9,6 +9,10 @@ import android.content.Intent;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.paging.DataSource.Factory;
import androidx.paging.LivePagedListBuilder;
import androidx.paging.PagedList;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.filepicker.DefaultCallback;
import fr.free.nrw.commons.filepicker.FilePicker;
@ -25,6 +29,8 @@ import fr.free.nrw.commons.utils.DialogUtil;
import fr.free.nrw.commons.utils.PermissionUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
@ -39,10 +45,16 @@ public class ContributionController {
private boolean isInAppCameraUpload;
public LocationPermissionCallback locationPermissionCallback;
private LocationPermissionsHelper locationPermissionsHelper;
LiveData<PagedList<Contribution>> failedAndPendingContributionList;
LiveData<PagedList<Contribution>> pendingContributionList;
LiveData<PagedList<Contribution>> failedContributionList;
@Inject
LocationServiceManager locationManager;
@Inject
ContributionsRepository repository;
@Inject
public ContributionController(@Named("default_preferences") JsonKvStore defaultKvStore) {
this.defaultKvStore = defaultKvStore;
@ -115,14 +127,14 @@ public class ContributionController {
}
/**
* Shows a dialog alerting the user about location services being off
* and asking them to turn it on
* Shows a dialog alerting the user about location services being off and asking them to turn it
* on
* TODO: Add a seperate callback in LocationPermissionsHelper for this.
* Ref: https://github.com/commons-app/apps-android-commons/pull/5494/files#r1510553114
*
* @param activity Activity reference
* @param activity Activity reference
* @param dialogTextResource Resource id of text to be shown in dialog
* @param toastTextResource Resource id of text to be shown in toast
* @param toastTextResource Resource id of text to be shown in toast
*/
private void showLocationOffDialog(Activity activity, int dialogTextResource,
int toastTextResource) {
@ -307,4 +319,60 @@ public class ContributionController {
isInAppCameraUpload = false; // reset the flag for next use
return shareIntent;
}
/**
* Fetches the contributions with the state "IN_PROGRESS", "QUEUED" and "PAUSED" and then it
* populates the `pendingContributionList`.
**/
void getPendingContributions() {
final PagedList.Config pagedListConfig =
(new PagedList.Config.Builder())
.setPrefetchDistance(50)
.setPageSize(10).build();
Factory<Integer, Contribution> factory;
factory = repository.fetchContributionsWithStates(
Arrays.asList(Contribution.STATE_IN_PROGRESS, Contribution.STATE_QUEUED,
Contribution.STATE_PAUSED));
LivePagedListBuilder livePagedListBuilder = new LivePagedListBuilder(factory,
pagedListConfig);
pendingContributionList = livePagedListBuilder.build();
}
/**
* Fetches the contributions with the state "FAILED" and populates the
* `failedContributionList`.
**/
void getFailedContributions() {
final PagedList.Config pagedListConfig =
(new PagedList.Config.Builder())
.setPrefetchDistance(50)
.setPageSize(10).build();
Factory<Integer, Contribution> factory;
factory = repository.fetchContributionsWithStates(
Collections.singletonList(Contribution.STATE_FAILED));
LivePagedListBuilder livePagedListBuilder = new LivePagedListBuilder(factory,
pagedListConfig);
failedContributionList = livePagedListBuilder.build();
}
/**
* Fetches the contributions with the state "IN_PROGRESS", "QUEUED", "PAUSED" and "FAILED" and
* then it populates the `failedAndPendingContributionList`.
**/
void getFailedAndPendingContributions() {
final PagedList.Config pagedListConfig =
(new PagedList.Config.Builder())
.setPrefetchDistance(50)
.setPageSize(10).build();
Factory<Integer, Contribution> factory;
factory = repository.fetchContributionsWithStates(
Arrays.asList(Contribution.STATE_IN_PROGRESS, Contribution.STATE_QUEUED,
Contribution.STATE_PAUSED, Contribution.STATE_FAILED));
LivePagedListBuilder livePagedListBuilder = new LivePagedListBuilder(factory,
pagedListConfig);
failedAndPendingContributionList = livePagedListBuilder.build();
}
}

View file

@ -13,6 +13,7 @@ import io.reactivex.Completable;
import io.reactivex.Single;
import java.util.Calendar;
import java.util.List;
import timber.log.Timber;
@Dao
public abstract class ContributionDao {
@ -27,6 +28,9 @@ public abstract class ContributionDao {
return Completable
.fromAction(() -> {
contribution.setDateModified(Calendar.getInstance().getTime());
if (contribution.getDateUploadStarted() == null) {
contribution.setDateUploadStarted(Calendar.getInstance().getTime());
}
saveSynchronous(contribution);
});
}
@ -44,11 +48,32 @@ public abstract class ContributionDao {
@Delete
public abstract void deleteSynchronous(Contribution contribution);
/**
* Deletes contributions with specific states from the database.
*
* @param states The states of the contributions to delete.
* @throws SQLiteException If an SQLite error occurs.
*/
@Query("DELETE FROM contribution WHERE state IN (:states)")
public abstract void deleteContributionsWithStatesSynchronous(List<Integer> states)
throws SQLiteException;
public Completable delete(final Contribution contribution) {
return Completable
.fromAction(() -> deleteSynchronous(contribution));
}
/**
* Deletes contributions with specific states from the database.
*
* @param states The states of the contributions to delete.
* @return A Completable indicating the result of the operation.
*/
public Completable deleteContributionsWithStates(List<Integer> states) {
return Completable
.fromAction(() -> deleteContributionsWithStatesSynchronous(states));
}
@Query("SELECT * from contribution WHERE media_filename=:fileName")
public abstract List<Contribution> getContributionWithTitle(String fileName);
@ -58,6 +83,26 @@ public abstract class ContributionDao {
@Query("SELECT * from contribution WHERE state IN (:states) order by media_dateUploaded DESC")
public abstract Single<List<Contribution>> getContribution(List<Integer> states);
/**
* Gets contributions with specific states in descending order by the date they were uploaded.
*
* @param states The states of the contributions to fetch.
* @return A DataSource factory for paginated contributions with the specified states.
*/
@Query("SELECT * from contribution WHERE state IN (:states) order by media_dateUploaded DESC")
public abstract DataSource.Factory<Integer, Contribution> getContributions(
List<Integer> states);
/**
* Gets contributions with specific states in ascending order by the date the upload started.
*
* @param states The states of the contributions to fetch.
* @return A DataSource factory for paginated contributions with the specified states.
*/
@Query("SELECT * from contribution WHERE state IN (:states) order by dateUploadStarted ASC")
public abstract DataSource.Factory<Integer, Contribution> getContributionsSortedByDateUploadStarted(
List<Integer> states);
@Query("SELECT COUNT(*) from contribution WHERE state in (:toUpdateStates)")
public abstract Single<Integer> getPendingUploads(int[] toUpdateStates);
@ -67,6 +112,15 @@ public abstract class ContributionDao {
@Update
public abstract void updateSynchronous(Contribution contribution);
/**
* Updates the state of contributions with specific states.
*
* @param states The current states of the contributions to update.
* @param newState The new state to set.
*/
@Query("UPDATE contribution SET state = :newState WHERE state IN (:states)")
public abstract void updateContributionsState(List<Integer> states, int newState);
public Completable update(final Contribution contribution) {
return Completable
.fromAction(() -> {
@ -74,4 +128,18 @@ public abstract class ContributionDao {
updateSynchronous(contribution);
});
}
/**
* Updates the state of contributions with specific states asynchronously.
*
* @param states The current states of the contributions to update.
* @param newState The new state to set.
* @return A Completable indicating the result of the operation.
*/
public Completable updateContributionsWithStates(List<Integer> states, int newState) {
return Completable
.fromAction(() -> {
updateContributionsState(states, newState);
});
}
}

View file

@ -48,11 +48,8 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
binding = LayoutContributionBinding.bind(parent);
binding.retryButton.setOnClickListener(v -> retryUpload());
binding.cancelButton.setOnClickListener(v -> deleteUpload());
binding.contributionImage.setOnClickListener(v -> imageClicked());
binding.wikipediaButton.setOnClickListener(v -> wikipediaButtonClicked());
binding.pauseResumeButton.setOnClickListener(v -> onPauseResumeButtonClicked());
/* Set a dialog indicating that the upload is being paused. This is needed because pausing
an upload might take a dozen seconds. */
@ -79,9 +76,6 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
binding.contributionImage.getHierarchy().setPlaceholderImage(R.drawable.image_placeholder);
binding.contributionImage.getHierarchy().setFailureImage(R.drawable.image_placeholder);
final String imageSource = chooseImageSource(contribution.getMedia().getThumbUrl(),
contribution.getLocalUri());
@ -90,79 +84,27 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
imageRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(imageSource))
.setProgressiveRenderingEnabled(true)
.build();
}
else if (URLUtil.isFileUrl(imageSource)){
imageRequest=ImageRequest.fromUri(Uri.parse(imageSource));
}
else if(imageSource != null) {
} else if (URLUtil.isFileUrl(imageSource)) {
imageRequest = ImageRequest.fromUri(Uri.parse(imageSource));
} else if (imageSource != null) {
final File file = new File(imageSource);
imageRequest = ImageRequest.fromFile(file);
}
if(imageRequest != null){
if (imageRequest != null) {
binding.contributionImage.setImageRequest(imageRequest);
}
}
binding.contributionSequenceNumber.setText(String.valueOf(position + 1));
binding.contributionSequenceNumber.setVisibility(View.VISIBLE);
binding.wikipediaButton.setVisibility(View.GONE);
switch (contribution.getState()) {
case Contribution.STATE_COMPLETED:
binding.contributionState.setVisibility(View.GONE);
binding.contributionProgress.setVisibility(View.GONE);
binding.imageOptions.setVisibility(View.GONE);
binding.contributionState.setText("");
checkIfMediaExistsOnWikipediaPage(contribution);
break;
case Contribution.STATE_QUEUED:
case Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE:
binding.contributionProgress.setVisibility(View.GONE);
binding.contributionState.setVisibility(View.VISIBLE);
binding.contributionState.setText(R.string.contribution_state_queued);
binding.imageOptions.setVisibility(View.GONE);
break;
case Contribution.STATE_IN_PROGRESS:
binding.contributionState.setVisibility(View.GONE);
binding.contributionProgress.setVisibility(View.VISIBLE);
binding.wikipediaButton.setVisibility(View.GONE);
binding.pauseResumeButton.setVisibility(View.VISIBLE);
binding.cancelButton.setVisibility(View.GONE);
binding.retryButton.setVisibility(View.GONE);
binding.imageOptions.setVisibility(View.VISIBLE);
final long total = contribution.getDataLength();
final long transferred = contribution.getTransferred();
if (transferred == 0 || transferred >= total) {
binding.contributionProgress.setIndeterminate(true);
} else {
binding.contributionProgress.setIndeterminate(false);
binding.contributionProgress.setProgress((int) (((double) transferred / (double) total) * 100));
}
break;
case Contribution.STATE_PAUSED:
binding.contributionProgress.setVisibility(View.GONE);
binding.contributionState.setVisibility(View.VISIBLE);
binding.contributionState.setText(R.string.paused);
binding.cancelButton.setVisibility(View.VISIBLE);
binding.retryButton.setVisibility(View.GONE);
binding.pauseResumeButton.setVisibility(View.VISIBLE);
binding.imageOptions.setVisibility(View.VISIBLE);
setResume();
if(pausingPopUp.isShowing()){
pausingPopUp.hide();
}
break;
case Contribution.STATE_FAILED:
binding.contributionState.setVisibility(View.VISIBLE);
binding.contributionState.setText(R.string.contribution_state_failed);
binding.contributionProgress.setVisibility(View.GONE);
binding.cancelButton.setVisibility(View.VISIBLE);
binding.retryButton.setVisibility(View.VISIBLE);
binding.pauseResumeButton.setVisibility(View.GONE);
binding.imageOptions.setVisibility(View.VISIBLE);
break;
}
binding.contributionState.setVisibility(View.GONE);
binding.contributionProgress.setVisibility(View.GONE);
binding.imageOptions.setVisibility(View.GONE);
binding.contributionState.setText("");
checkIfMediaExistsOnWikipediaPage(contribution);
}
/**
@ -196,8 +138,6 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
if (!mediaExists) {
binding.wikipediaButton.setVisibility(View.VISIBLE);
isWikipediaButtonDisplayed = true;
binding.cancelButton.setVisibility(View.GONE);
binding.retryButton.setVisibility(View.GONE);
binding.imageOptions.setVisibility(View.VISIBLE);
}
}
@ -217,20 +157,6 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
null;
}
/**
* Retry upload when it is failed
*/
public void retryUpload() {
callback.retryUpload(contribution);
}
/**
* Delete a failed upload attempt
*/
public void deleteUpload() {
callback.deleteUpload(contribution);
}
public void imageClicked() {
callback.openMediaDetail(position, isWikipediaButtonDisplayed);
}
@ -239,44 +165,6 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
callback.addImageToWikipedia(contribution);
}
/**
* Triggers a callback for pause/resume
*/
public void onPauseResumeButtonClicked() {
if (binding.pauseResumeButton.getTag().toString().equals("pause")) {
pause();
} else {
resume();
}
}
private void resume() {
callback.resumeUpload(contribution);
setPaused();
}
private void pause() {
pausingPopUp.show();
callback.pauseUpload(contribution);
setResume();
}
/**
* Update pause/resume button to show pause state
*/
private void setPaused() {
binding.pauseResumeButton.setImageResource(R.drawable.pause_icon);
binding.pauseResumeButton.setTag(parent.getContext().getString(R.string.pause));
}
/**
* Update pause/resume button to show resume state
*/
private void setResume() {
binding.pauseResumeButton.setImageResource(R.drawable.play_icon);
binding.pauseResumeButton.setTag(parent.getContext().getString(R.string.resume));
}
public ImageRequest getImageRequest() {
return imageRequest;
}

View file

@ -19,8 +19,5 @@ public class ContributionsContract {
Contribution getContributionsWithTitle(String uri);
void deleteUpload(Contribution contribution);
void saveContribution(Contribution contribution);
}
}

View file

@ -5,6 +5,7 @@ import static fr.free.nrw.commons.contributions.Contribution.STATE_FAILED;
import static fr.free.nrw.commons.contributions.Contribution.STATE_PAUSED;
import static fr.free.nrw.commons.nearby.fragments.NearbyParentFragment.WLM_URL;
import static fr.free.nrw.commons.profile.ProfileActivity.KEY_USERNAME;
import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK;
import static fr.free.nrw.commons.utils.LengthUtils.computeBearing;
import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween;
@ -12,6 +13,7 @@ import android.Manifest;
import android.Manifest.permission;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
@ -25,6 +27,7 @@ import android.view.MenuItem.OnMenuItemClickListener;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
@ -44,6 +47,8 @@ import fr.free.nrw.commons.notification.models.Notification;
import fr.free.nrw.commons.notification.NotificationController;
import fr.free.nrw.commons.profile.ProfileActivity;
import fr.free.nrw.commons.theme.BaseActivity;
import fr.free.nrw.commons.upload.UploadProgressActivity;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Map;
@ -104,6 +109,8 @@ public class ContributionsFragment
LocationServiceManager locationManager;
@Inject
NotificationController notificationController;
@Inject
ContributionController contributionController;
private CompositeDisposable compositeDisposable = new CompositeDisposable();
@ -113,10 +120,10 @@ public class ContributionsFragment
static final String MEDIA_DETAIL_PAGER_FRAGMENT_TAG = "MediaDetailFragmentTag";
private static final int MAX_RETRIES = 10;
public FragmentContributionsBinding binding;
@Inject ContributionsPresenter contributionsPresenter;
@Inject
ContributionsPresenter contributionsPresenter;
@Inject
SessionManager sessionManager;
@ -129,6 +136,12 @@ public class ContributionsFragment
public TextView notificationCount;
public TextView pendingUploadsCountTextView;
public TextView uploadsErrorTextView;
public ImageView pendingUploadsImageView;
private Campaign wlmCampaign;
String userName;
@ -147,20 +160,22 @@ public class ContributionsFragment
areAllGranted = areAllGranted && b;
}
if (areAllGranted) {
onLocationPermissionGranted();
} else {
if (shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)
&& store.getBoolean("displayLocationPermissionForCardView", true)
&& !store.getBoolean("doNotAskForLocationPermission", false)
&& (((MainActivity) getActivity()).activeFragment == ActiveFragment.CONTRIBUTIONS)) {
binding.cardViewNearby.permissionType = NearbyNotificationCardView.PermissionType.ENABLE_LOCATION_PERMISSION;
if (areAllGranted) {
onLocationPermissionGranted();
} else {
displayYouWontSeeNearbyMessage();
if (shouldShowRequestPermissionRationale(
Manifest.permission.ACCESS_FINE_LOCATION)
&& store.getBoolean("displayLocationPermissionForCardView", true)
&& !store.getBoolean("doNotAskForLocationPermission", false)
&& (((MainActivity) getActivity()).activeFragment
== ActiveFragment.CONTRIBUTIONS)) {
binding.cardViewNearby.permissionType = NearbyNotificationCardView.PermissionType.ENABLE_LOCATION_PERMISSION;
} else {
displayYouWontSeeNearbyMessage();
}
}
}
}
});
});
@NonNull
public static ContributionsFragment newInstance() {
@ -198,11 +213,10 @@ public class ContributionsFragment
checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (isChecked) {
// Do not ask for permission on activity start again
store.putBoolean("displayLocationPermissionForCardView",false);
store.putBoolean("displayLocationPermissionForCardView", false);
}
});
if (savedInstanceState != null) {
mediaDetailPagerFragment = (MediaDetailPagerFragment) getChildFragmentManager()
.findFragmentByTag(MEDIA_DETAIL_PAGER_FRAGMENT_TAG);
@ -212,9 +226,7 @@ public class ContributionsFragment
}
initFragments();
if(isUserProfile) {
binding.limitedConnectionEnabledLayout.setVisibility(View.GONE);
}else {
if (!isUserProfile) {
upDateUploadCount();
}
if (shouldShowMediaDetailsFragment) {
@ -230,7 +242,6 @@ public class ContributionsFragment
&& sessionManager.getCurrentAccount() != null && !isUserProfile) {
setUploadCount();
}
binding.limitedConnectionEnabledLayout.setOnClickListener(toggleDescriptionListener);
setHasOptionsMenu(true);
return binding.getRoot();
}
@ -258,10 +269,32 @@ public class ContributionsFragment
MenuItem notificationsMenuItem = menu.findItem(R.id.notifications);
final View notification = notificationsMenuItem.getActionView();
notificationCount = notification.findViewById(R.id.notification_count_badge);
MenuItem uploadMenuItem = menu.findItem(R.id.upload_tab);
final View uploadMenuItemActionView = uploadMenuItem.getActionView();
pendingUploadsCountTextView = uploadMenuItemActionView.findViewById(
R.id.pending_uploads_count_badge);
uploadsErrorTextView = uploadMenuItemActionView.findViewById(
R.id.uploads_error_count_badge);
pendingUploadsImageView = uploadMenuItemActionView.findViewById(
R.id.pending_uploads_image_view);
if (pendingUploadsImageView != null) {
pendingUploadsImageView.setOnClickListener(view -> {
startActivity(new Intent(getContext(), UploadProgressActivity.class));
});
}
if (pendingUploadsCountTextView != null) {
pendingUploadsCountTextView.setOnClickListener(view -> {
startActivity(new Intent(getContext(), UploadProgressActivity.class));
});
}
if (uploadsErrorTextView != null) {
uploadsErrorTextView.setOnClickListener(view -> {
startActivity(new Intent(getContext(), UploadProgressActivity.class));
});
}
notification.setOnClickListener(view -> {
NotificationActivity.startYourself(getContext(), "unread");
});
updateLimitedConnectionToggle(menu);
}
@SuppressLint("CheckResult")
@ -273,6 +306,33 @@ public class ContributionsFragment
throwable -> Timber.e(throwable, "Error occurred while loading notifications")));
}
/**
* Sets the visibility of the upload icon based on the number of failed and pending
* contributions.
*/
public void setUploadIconVisibility() {
contributionController.getFailedAndPendingContributions();
contributionController.failedAndPendingContributionList.observe(getViewLifecycleOwner(),
list -> {
updateUploadIcon(list.size());
});
}
/**
* Sets the count for the upload icon based on the number of pending and failed contributions.
*/
public void setUploadIconCount() {
contributionController.getPendingContributions();
contributionController.pendingContributionList.observe(getViewLifecycleOwner(),
list -> {
updatePendingIcon(list.size());
});
contributionController.getFailedContributions();
contributionController.failedContributionList.observe(getViewLifecycleOwner(), list -> {
updateErrorIcon(list.size());
});
}
public void scrollToTop() {
if (contributionsListFragment != null) {
contributionsListFragment.scrollToTop();
@ -289,29 +349,6 @@ public class ContributionsFragment
}
}
public void updateLimitedConnectionToggle(Menu menu) {
MenuItem checkable = menu.findItem(R.id.toggle_limited_connection_mode);
boolean isEnabled = store
.getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false);
checkable.setChecked(isEnabled);
if (binding!=null) {
binding.limitedConnectionEnabledLayout.setVisibility(isEnabled ? View.VISIBLE : View.GONE);
}
checkable.setIcon((isEnabled) ? R.drawable.ic_baseline_cloud_off_24:R.drawable.ic_baseline_cloud_queue_24);
checkable.setOnMenuItemClickListener(new OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
((MainActivity) getActivity()).toggleLimitedConnectionMode();
boolean isEnabled = store.getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false);
binding.limitedConnectionEnabledLayout.setVisibility(isEnabled ? View.VISIBLE : View.GONE);
checkable.setIcon((isEnabled) ? R.drawable.ic_baseline_cloud_off_24:R.drawable.ic_baseline_cloud_queue_24);
return false;
}
});
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
@ -355,7 +392,7 @@ public class ContributionsFragment
}
private void setupViewForMediaDetails() {
if (binding!=null) {
if (binding != null) {
binding.campaignsView.setVisibility(View.GONE);
}
}
@ -465,7 +502,7 @@ public class ContributionsFragment
contributionsPresenter.onAttachView(this);
locationManager.addLocationListener(this);
if (binding==null) {
if (binding == null) {
return;
}
@ -484,7 +521,8 @@ public class ContributionsFragment
} catch (Exception e) {
Timber.e(e);
}
if (binding.cardViewNearby.cardViewVisibilityState == NearbyNotificationCardView.CardViewVisibilityState.READY) {
if (binding.cardViewNearby.cardViewVisibilityState
== NearbyNotificationCardView.CardViewVisibilityState.READY) {
binding.cardViewNearby.setVisibility(View.VISIBLE);
}
@ -494,16 +532,19 @@ public class ContributionsFragment
}
// Notification Count and Campaigns should not be set, if it is used in User Profile
if(!isUserProfile) {
if (!isUserProfile) {
setNotificationCount();
fetchCampaigns();
setUploadIconVisibility();
setUploadIconCount();
}
}
mSensorManager.registerListener(this, mLight, SensorManager.SENSOR_DELAY_UI);
}
private void checkPermissionsAndShowNearbyCardView() {
if (PermissionUtils.hasPermission(getActivity(), new String[]{Manifest.permission.ACCESS_FINE_LOCATION})) {
if (PermissionUtils.hasPermission(getActivity(),
new String[]{Manifest.permission.ACCESS_FINE_LOCATION})) {
onLocationPermissionGranted();
} else if (shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)
&& store.getBoolean("displayLocationPermissionForCardView", true)
@ -636,14 +677,14 @@ public class ContributionsFragment
*/
private void fetchCampaigns() {
if (Utils.isMonumentsEnabled(new Date())) {
if (binding!=null) {
if (binding != null) {
binding.campaignsView.setCampaign(wlmCampaign);
binding.campaignsView.setVisibility(View.VISIBLE);
}
} else if (store.getBoolean(CampaignView.CAMPAIGNS_DEFAULT_PREFERENCE, true)) {
presenter.getCampaigns();
} else {
if (binding!=null) {
if (binding != null) {
binding.campaignsView.setVisibility(View.GONE);
}
}
@ -657,7 +698,7 @@ public class ContributionsFragment
@Override
public void showCampaigns(Campaign campaign) {
if (campaign != null && !isUserProfile) {
if (binding!=null) {
if (binding != null) {
binding.campaignsView.setCampaign(campaign);
}
}
@ -676,67 +717,6 @@ public class ContributionsFragment
}
}
/**
* Restarts the upload process for a contribution
*
* @param contribution
*/
public void restartUpload(Contribution contribution) {
contribution.setState(Contribution.STATE_QUEUED);
contributionsPresenter.saveContribution(contribution);
Timber.d("Restarting for %s", contribution.toString());
}
/**
* Retry upload when it is failed
*
* @param contribution contribution to be retried
*/
@Override
public void retryUpload(Contribution contribution) {
if (NetworkUtils.isInternetConnectionEstablished(getContext())) {
if (contribution.getState() == STATE_PAUSED
|| contribution.getState() == Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE) {
restartUpload(contribution);
} else if (contribution.getState() == STATE_FAILED) {
int retries = contribution.getRetries();
// TODO: Improve UX. Additional details: https://github.com/commons-app/apps-android-commons/pull/5257#discussion_r1304662562
/* Limit the number of retries for a failed upload
to handle cases like invalid filename as such uploads
will never be successful */
if (retries < MAX_RETRIES) {
contribution.setRetries(retries + 1);
Timber.d("Retried uploading %s %d times", contribution.getMedia().getFilename(),
retries + 1);
restartUpload(contribution);
} else {
// TODO: Show the exact reason for failure
Toast.makeText(getContext(),
R.string.retry_limit_reached, Toast.LENGTH_SHORT).show();
}
} else {
Timber.d("Skipping re-upload for non-failed %s", contribution.toString());
}
} else {
ViewUtil.showLongToast(getContext(), R.string.this_function_needs_network_connection);
}
}
/**
* Pauses the upload
*
* @param contribution
*/
@Override
public void pauseUpload(Contribution 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);
}
/**
* Notify the viewpager that number of items have changed.
*/
@ -747,6 +727,54 @@ public class ContributionsFragment
}
}
/**
* Updates the visibility and text of the pending uploads count TextView based on the given
* count.
*
* @param pendingCount The number of pending uploads.
*/
public void updatePendingIcon(int pendingCount) {
if (pendingUploadsCountTextView != null) {
if (pendingCount != 0) {
pendingUploadsCountTextView.setVisibility(View.VISIBLE);
pendingUploadsCountTextView.setText(String.valueOf(pendingCount));
} else {
pendingUploadsCountTextView.setVisibility(View.INVISIBLE);
}
}
}
/**
* Updates the visibility and text of the error uploads TextView based on the given count.
*
* @param errorCount The number of error uploads.
*/
public void updateErrorIcon(int errorCount) {
if (uploadsErrorTextView != null) {
if (errorCount != 0) {
uploadsErrorTextView.setVisibility(View.VISIBLE);
uploadsErrorTextView.setText(String.valueOf(errorCount));
} else {
uploadsErrorTextView.setVisibility(View.GONE);
}
}
}
/**
* Updates the visibility of the pending uploads ImageView based on the given count.
*
* @param count The number of pending uploads.
*/
public void updateUploadIcon(int count) {
if (pendingUploadsImageView != null) {
if (count != 0) {
pendingUploadsImageView.setVisibility(View.VISIBLE);
} else {
pendingUploadsImageView.setVisibility(View.GONE);
}
}
}
/**
* Replace whatever is in the current contributionsFragmentContainer view with
* mediaDetailPagerFragment, and preserve previous state in back stack. Called when user selects
@ -782,7 +810,8 @@ public class ContributionsFragment
public boolean backButtonClicked() {
if (mediaDetailPagerFragment != null && mediaDetailPagerFragment.isVisible()) {
if (store.getBoolean("displayNearbyCardView", true) && !isUserProfile) {
if (binding.cardViewNearby.cardViewVisibilityState == NearbyNotificationCardView.CardViewVisibilityState.READY) {
if (binding.cardViewNearby.cardViewVisibilityState
== NearbyNotificationCardView.CardViewVisibilityState.READY) {
binding.cardViewNearby.setVisibility(View.VISIBLE);
}
} else {
@ -829,6 +858,60 @@ public class ContributionsFragment
}
/**
* Restarts the upload process for a contribution
*
* @param contribution
*/
public void restartUpload(Contribution contribution) {
contribution.setDateUploadStarted(Calendar.getInstance().getTime());
if (contribution.getState() == Contribution.STATE_FAILED) {
if (contribution.getErrorInfo() == null) {
contribution.setChunkInfo(null);
contribution.setTransferred(0);
}
contributionsPresenter.checkDuplicateImageAndRestartContribution(contribution);
} else {
contribution.setState(Contribution.STATE_QUEUED);
contributionsPresenter.saveContribution(contribution);
Timber.d("Restarting for %s", contribution.toString());
}
}
/**
* Retry upload when it is failed
*
* @param contribution contribution to be retried
*/
public void retryUpload(Contribution contribution) {
if (NetworkUtils.isInternetConnectionEstablished(getContext())) {
if (contribution.getState() == STATE_PAUSED) {
restartUpload(contribution);
} else if (contribution.getState() == STATE_FAILED) {
int retries = contribution.getRetries();
// TODO: Improve UX. Additional details: https://github.com/commons-app/apps-android-commons/pull/5257#discussion_r1304662562
/* Limit the number of retries for a failed upload
to handle cases like invalid filename as such uploads
will never be successful */
if (retries < MAX_RETRIES) {
contribution.setRetries(retries + 1);
Timber.d("Retried uploading %s %d times", contribution.getMedia().getFilename(),
retries + 1);
restartUpload(contribution);
} else {
// TODO: Show the exact reason for failure
Toast.makeText(getContext(),
R.string.retry_limit_reached, Toast.LENGTH_SHORT).show();
}
} else {
Timber.d("Skipping re-upload for non-failed %s", contribution.toString());
}
} else {
ViewUtil.showLongToast(getContext(), R.string.this_function_needs_network_connection);
}
}
/**
* Reload media detail fragment once media is nominated
*
@ -844,21 +927,6 @@ public class ContributionsFragment
}
}
// click listener to toggle description that means uses can press the limited connection
// banner and description will hide. Tap again to show description.
private View.OnClickListener toggleDescriptionListener = new View.OnClickListener() {
@Override
public void onClick(View view) {
View view2 = binding.limitedConnectionDescriptionTextView;
if (view2.getVisibility() == View.GONE) {
view2.setVisibility(View.VISIBLE);
} else {
view2.setVisibility(View.GONE);
}
}
};
/**
* When the device rotates, rotate the Nearby banner's compass arrow in tandem.
*/

View file

@ -70,16 +70,8 @@ public class ContributionsListAdapter extends
public interface Callback {
void retryUpload(Contribution contribution);
void deleteUpload(Contribution contribution);
void openMediaDetail(int contribution, boolean isWikipediaPageExists);
void addImageToWikipedia(Contribution contribution);
void pauseUpload(Contribution contribution);
void resumeUpload(Contribution contribution);
}
}

View file

@ -17,7 +17,5 @@ public class ContributionsListContract {
}
public interface UserActionListener extends BasePresenter<View> {
void deleteUpload(Contribution contribution);
}
}

View file

@ -19,7 +19,7 @@ import android.view.animation.AnimationUtils;
import android.widget.LinearLayout;
import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
@ -30,11 +30,11 @@ import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver;
import androidx.recyclerview.widget.RecyclerView.ItemAnimator;
import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
import androidx.recyclerview.widget.SimpleItemAnimator;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.contributions.ContributionsListAdapter.Callback;
import fr.free.nrw.commons.databinding.FragmentContributionsListBinding;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.media.MediaClient;
@ -42,7 +42,6 @@ import fr.free.nrw.commons.profile.ProfileActivity;
import fr.free.nrw.commons.utils.DialogUtil;
import fr.free.nrw.commons.utils.SystemThemeUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import javax.inject.Inject;
@ -56,7 +55,7 @@ import fr.free.nrw.commons.wikidata.model.WikiSite;
*/
public class ContributionsListFragment extends CommonsDaggerSupportFragment implements
ContributionsListContract.View, ContributionsListAdapter.Callback,
ContributionsListContract.View, Callback,
WikipediaInstructionsDialogFragment.Callback {
private static final String RV_STATE = "rv_scroll_state";
@ -81,7 +80,6 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
private Animation rotate_forward;
private Animation rotate_backward;
private boolean isFabOpen;
@VisibleForTesting
protected RecyclerView rvContributionsList;
@ -99,7 +97,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
private String userName;
private ActivityResultLauncher<String[]> inAppCameraLocationPermissionLauncher = registerForActivityResult(
new ActivityResultContracts.RequestMultiplePermissions(),
new RequestMultiplePermissions(),
new ActivityResultCallback<Map<String, Boolean>>() {
@Override
public void onActivityResult(Map<String, Boolean> result) {
@ -151,7 +149,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
contributionsListPresenter.onAttachView(this);
binding.fabCustomGallery.setOnClickListener(v -> launchCustomSelector());
binding.fabCustomGallery.setOnLongClickListener(view -> {
ViewUtil.showShortToast(getContext(),R.string.custom_selector_title);
ViewUtil.showShortToast(getContext(), R.string.custom_selector_title);
return true;
});
@ -160,7 +158,8 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
binding.fabLayout.setVisibility(VISIBLE);
} else {
binding.tvContributionsOfUser.setVisibility(VISIBLE);
binding.tvContributionsOfUser.setText(getString(R.string.contributions_of_user, userName));
binding.tvContributionsOfUser.setText(
getString(R.string.contributions_of_user, userName));
binding.fabLayout.setVisibility(GONE);
}
@ -305,8 +304,9 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
public void onConfigurationChanged(final Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// check orientation
binding.fabLayout.setOrientation(newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE ?
LinearLayout.HORIZONTAL : LinearLayout.VERTICAL);
binding.fabLayout.setOrientation(
newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE ?
LinearLayout.HORIZONTAL : LinearLayout.VERTICAL);
rvContributionsList
.setLayoutManager(
new GridLayoutManager(getContext(), getSpanCount(newConfig.orientation)));
@ -326,7 +326,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
animateFAB(isFabOpen);
});
binding.fabCamera.setOnLongClickListener(view -> {
ViewUtil.showShortToast(getContext(),R.string.add_contribution_from_camera);
ViewUtil.showShortToast(getContext(), R.string.add_contribution_from_camera);
return true;
});
binding.fabGallery.setOnClickListener(view -> {
@ -334,7 +334,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
animateFAB(isFabOpen);
});
binding.fabGallery.setOnLongClickListener(view -> {
ViewUtil.showShortToast(getContext(),R.string.menu_from_gallery);
ViewUtil.showShortToast(getContext(), R.string.menu_from_gallery);
return true;
});
}
@ -415,30 +415,6 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
}
}
@Override
public void retryUpload(final Contribution contribution) {
if (null != callback) {//Just being safe, ideally they won't be called when detached
callback.retryUpload(contribution);
}
}
@Override
public void deleteUpload(final Contribution contribution) {
DialogUtil.showAlertDialog(getActivity(),
String.format(Locale.getDefault(),
getString(R.string.cancelling_upload)),
String.format(Locale.getDefault(),
getString(R.string.cancel_upload_dialog)),
String.format(Locale.getDefault(), getString(R.string.yes)), String.format(Locale.getDefault(), getString(R.string.no)),
() -> {
ViewUtil.showShortToast(getContext(), R.string.cancelling_upload);
contributionsListPresenter.deleteUpload(contribution);
CommonsApplication.cancelledUploads.add(contribution.getPageId());
}, () -> {
// Do nothing
});
}
@Override
public void openMediaDetail(final int position, boolean isWikipediaButtonDisplayed) {
if (null != callback) {//Just being safe, ideally they won't be called when detached
@ -463,28 +439,6 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
});
}
/**
* Pauses the current upload
*
* @param contribution
*/
@Override
public void pauseUpload(Contribution contribution) {
ViewUtil.showShortToast(getContext(), R.string.pausing_upload);
callback.pauseUpload(contribution);
}
/**
* Resumes the current upload
*
* @param contribution
*/
@Override
public void resumeUpload(Contribution contribution) {
ViewUtil.showShortToast(getContext(), R.string.resuming_upload);
callback.retryUpload(contribution);
}
/**
* Display confirmation dialog with instructions when the user tries to add image to wikipedia
*
@ -536,13 +490,10 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
void notifyDataSetChanged();
void retryUpload(Contribution contribution);
void showDetail(int position, boolean isWikipediaButtonDisplayed);
void pauseUpload(Contribution contribution);
// Notify the viewpager that number of items have changed.
void viewPagerNotifyDataSetChanged();
}
}

View file

@ -10,6 +10,8 @@ import fr.free.nrw.commons.contributions.ContributionsListContract.UserActionLis
import fr.free.nrw.commons.di.CommonsApplicationModule;
import io.reactivex.Scheduler;
import io.reactivex.disposables.CompositeDisposable;
import java.util.Arrays;
import java.util.Collections;
import javax.inject.Inject;
import javax.inject.Named;
@ -36,7 +38,7 @@ public class ContributionsListPresenter implements UserActionListener {
this.contributionBoundaryCallback = contributionBoundaryCallback;
this.repository = repository;
this.ioThreadScheduler = ioThreadScheduler;
this.contributionsRemoteDataSource=contributionsRemoteDataSource;
this.contributionsRemoteDataSource = contributionsRemoteDataSource;
compositeDisposable = new CompositeDisposable();
}
@ -71,10 +73,12 @@ public class ContributionsListPresenter implements UserActionListener {
} else {
contributionBoundaryCallback.setUserName(userName);
shouldSetBoundaryCallback = true;
factory = repository.fetchContributions();
factory = repository.fetchContributionsWithStates(
Collections.singletonList(Contribution.STATE_COMPLETED));
}
LivePagedListBuilder livePagedListBuilder = new LivePagedListBuilder(factory, pagedListConfig);
LivePagedListBuilder livePagedListBuilder = new LivePagedListBuilder(factory,
pagedListConfig);
if (shouldSetBoundaryCallback) {
livePagedListBuilder.setBoundaryCallback(contributionBoundaryCallback);
}
@ -89,15 +93,4 @@ public class ContributionsListPresenter implements UserActionListener {
contributionBoundaryCallback.dispose();
}
/**
* Delete a failed contribution from the local db
*/
@Override
public void deleteUpload(final Contribution contribution) {
compositeDisposable.add(repository
.deleteContributionFromDB(contribution)
.subscribeOn(ioThreadScheduler)
.subscribe());
}
}

View file

@ -1,16 +1,14 @@
package fr.free.nrw.commons.contributions;
import androidx.paging.DataSource.Factory;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import io.reactivex.Completable;
import io.reactivex.Single;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import io.reactivex.Single;
/**
* The LocalDataSource class for Contributions
*/
@ -21,8 +19,8 @@ class ContributionsLocalDataSource {
@Inject
public ContributionsLocalDataSource(
@Named("default_preferences") final JsonKvStore defaultKVStore,
final ContributionDao contributionDao) {
@Named("default_preferences") final JsonKvStore defaultKVStore,
final ContributionDao contributionDao) {
this.defaultKVStore = defaultKVStore;
this.contributionDao = contributionDao;
}
@ -38,17 +36,19 @@ class ContributionsLocalDataSource {
* Fetch default number of contributions to be show, based on user preferences
*/
public long getLong(final String key) {
return defaultKVStore.getLong(key);
return defaultKVStore.getLong(key);
}
/**
* Get contribution object from cursor
*
* @param uri
* @return
*/
public Contribution getContributionWithFileName(final String uri) {
final List<Contribution> contributionWithUri = contributionDao.getContributionWithTitle(uri);
if(!contributionWithUri.isEmpty()){
final List<Contribution> contributionWithUri = contributionDao.getContributionWithTitle(
uri);
if (!contributionWithUri.isEmpty()) {
return contributionWithUri.get(0);
}
return null;
@ -56,6 +56,7 @@ class ContributionsLocalDataSource {
/**
* Remove a contribution from the contributions table
*
* @param contribution
* @return
*/
@ -63,15 +64,48 @@ class ContributionsLocalDataSource {
return contributionDao.delete(contribution);
}
/**
* Deletes contributions with specific states.
*
* @param states The states of the contributions to delete.
* @return A Completable indicating the result of the operation.
*/
public Completable deleteContributionsWithStates(List<Integer> states) {
return contributionDao.deleteContributionsWithStates(states);
}
public Factory<Integer, Contribution> getContributions() {
return contributionDao.fetchContributions();
}
/**
* Fetches contributions with specific states.
*
* @param states The states of the contributions to fetch.
* @return A DataSource factory for paginated contributions with the specified states.
*/
public Factory<Integer, Contribution> getContributionsWithStates(List<Integer> states) {
return contributionDao.getContributions(states);
}
/**
* Fetches contributions with specific states sorted by the date the upload started.
*
* @param states The states of the contributions to fetch.
* @return A DataSource factory for paginated contributions with the specified states sorted by
* date upload started.
*/
public Factory<Integer, Contribution> getContributionsWithStatesSortedByDateUploadStarted(
List<Integer> states) {
return contributionDao.getContributionsSortedByDateUploadStarted(states);
}
public Single<List<Long>> saveContributions(final List<Contribution> contributions) {
final List<Contribution> contributionList = new ArrayList<>();
for(final Contribution contribution: contributions) {
final Contribution oldContribution = contributionDao.getContribution(contribution.getPageId());
if(oldContribution != null) {
for (final Contribution contribution : contributions) {
final Contribution oldContribution = contributionDao.getContribution(
contribution.getPageId());
if (oldContribution != null) {
contribution.setWikidataPlace(oldContribution.getWikidataPlace());
}
contributionList.add(contribution);
@ -84,10 +118,14 @@ class ContributionsLocalDataSource {
}
public void set(final String key, final long value) {
defaultKVStore.putLong(key,value);
defaultKVStore.putLong(key, value);
}
public Completable updateContribution(final Contribution contribution) {
return contributionDao.update(contribution);
}
public Completable updateContributionsWithStates(List<Integer> states, int newState) {
return contributionDao.updateContributionsWithStates(states, newState);
}
}

View file

@ -1,21 +1,26 @@
package fr.free.nrw.commons.contributions;
import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK;
import androidx.work.ExistingWorkPolicy;
import fr.free.nrw.commons.MediaDataExtractor;
import fr.free.nrw.commons.contributions.ContributionsContract.UserActionListener;
import fr.free.nrw.commons.di.CommonsApplicationModule;
import fr.free.nrw.commons.repository.UploadRepository;
import fr.free.nrw.commons.upload.worker.WorkRequestHelper;
import io.reactivex.Scheduler;
import io.reactivex.disposables.CompositeDisposable;
import javax.inject.Inject;
import javax.inject.Named;
import timber.log.Timber;
/**
* The presenter class for Contributions
*/
public class ContributionsPresenter implements UserActionListener {
private final ContributionsRepository repository;
private final ContributionsRepository contributionsRepository;
private final UploadRepository uploadRepository;
private final Scheduler ioThreadScheduler;
private CompositeDisposable compositeDisposable;
private ContributionsContract.View view;
@ -25,15 +30,17 @@ public class ContributionsPresenter implements UserActionListener {
@Inject
ContributionsPresenter(ContributionsRepository repository,
UploadRepository uploadRepository,
@Named(CommonsApplicationModule.IO_THREAD) Scheduler ioThreadScheduler) {
this.repository = repository;
this.ioThreadScheduler=ioThreadScheduler;
this.contributionsRepository = repository;
this.uploadRepository = uploadRepository;
this.ioThreadScheduler = ioThreadScheduler;
}
@Override
public void onAttachView(ContributionsContract.View view) {
this.view = view;
compositeDisposable=new CompositeDisposable();
compositeDisposable = new CompositeDisposable();
}
@Override
@ -44,19 +51,30 @@ public class ContributionsPresenter implements UserActionListener {
@Override
public Contribution getContributionsWithTitle(String title) {
return repository.getContributionWithFileName(title);
return contributionsRepository.getContributionWithFileName(title);
}
/**
* Delete a failed contribution from the local db
* @param contribution
* Checks if a contribution is a duplicate and restarts the contribution process if it is not.
*
* @param contribution The contribution to check and potentially restart.
*/
@Override
public void deleteUpload(Contribution contribution) {
compositeDisposable.add(repository
.deleteContributionFromDB(contribution)
public void checkDuplicateImageAndRestartContribution(Contribution contribution) {
compositeDisposable.add(uploadRepository
.checkDuplicateImage(contribution.getLocalUriPath().getPath())
.subscribeOn(ioThreadScheduler)
.subscribe());
.subscribe(imageCheckResult -> {
if (imageCheckResult == IMAGE_OK) {
contribution.setState(Contribution.STATE_QUEUED);
saveContribution(contribution);
} else {
Timber.e("Contribution already exists");
compositeDisposable.add(contributionsRepository
.deleteContributionFromDB(contribution)
.subscribeOn(ioThreadScheduler)
.subscribe());
}
}));
}
/**
@ -65,9 +83,8 @@ public class ContributionsPresenter implements UserActionListener {
*
* @param contribution
*/
@Override
public void saveContribution(Contribution contribution) {
compositeDisposable.add(repository
compositeDisposable.add(contributionsRepository
.save(contribution)
.subscribeOn(ioThreadScheduler)
.subscribe(() -> WorkRequestHelper.Companion.makeOneTimeWorkRequest(

View file

@ -29,6 +29,7 @@ public class ContributionsRepository {
/**
* Deletes a failed upload from DB
*
* @param contribution
* @return
*/
@ -36,8 +37,19 @@ public class ContributionsRepository {
return localDataSource.deleteContribution(contribution);
}
/**
* Deletes contributions from the database with specific states.
*
* @param states The states of the contributions to delete.
* @return A Completable indicating the result of the operation.
*/
public Completable deleteContributionsFromDBWithStates(List<Integer> states) {
return localDataSource.deleteContributionsWithStates(states);
}
/**
* Get contribution object with title
*
* @param fileName
* @return
*/
@ -49,19 +61,52 @@ public class ContributionsRepository {
return localDataSource.getContributions();
}
/**
* Fetches contributions with specific states.
*
* @param states The states of the contributions to fetch.
* @return A DataSource factory for paginated contributions with the specified states.
*/
public Factory<Integer, Contribution> fetchContributionsWithStates(List<Integer> states) {
return localDataSource.getContributionsWithStates(states);
}
/**
* Fetches contributions with specific states sorted by the date the upload started.
*
* @param states The states of the contributions to fetch.
* @return A DataSource factory for paginated contributions with the specified states sorted by
* date upload started.
*/
public Factory<Integer, Contribution> fetchContributionsWithStatesSortedByDateUploadStarted(
List<Integer> states) {
return localDataSource.getContributionsWithStatesSortedByDateUploadStarted(states);
}
public Single<List<Long>> save(List<Contribution> contributions) {
return localDataSource.saveContributions(contributions);
}
public Completable save(Contribution contributions){
public Completable save(Contribution contributions) {
return localDataSource.saveContributions(contributions);
}
public void set(String key, long value) {
localDataSource.set(key,value);
localDataSource.set(key, value);
}
public Completable updateContribution(Contribution contribution) {
return localDataSource.updateContribution(contribution);
}
/**
* Updates the state of contributions with specific states.
*
* @param states The current states of the contributions to update.
* @param newState The new state to set.
* @return A Completable indicating the result of the operation.
*/
public Completable updateContributionsWithStates(List<Integer> states, int newState) {
return localDataSource.updateContributionsWithStates(states, newState);
}
}

View file

@ -41,18 +41,21 @@ import fr.free.nrw.commons.notification.NotificationController;
import fr.free.nrw.commons.quiz.QuizChecker;
import fr.free.nrw.commons.settings.SettingsFragment;
import fr.free.nrw.commons.theme.BaseActivity;
import fr.free.nrw.commons.upload.UploadActivity;
import fr.free.nrw.commons.upload.UploadProgressActivity;
import fr.free.nrw.commons.upload.worker.WorkRequestHelper;
import fr.free.nrw.commons.utils.PermissionUtils;
import fr.free.nrw.commons.utils.ViewUtilWrapper;
import io.reactivex.Completable;
import io.reactivex.schedulers.Schedulers;
import java.util.Calendar;
import java.util.Collections;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import timber.log.Timber;
public class MainActivity extends BaseActivity
public class MainActivity extends BaseActivity
implements FragmentManager.OnBackStackChangedListener {
@Inject
@ -144,16 +147,16 @@ public class MainActivity extends BaseActivity
applicationKvStore.putBoolean("hasAlreadyLaunchedBigMultiupload", false);
applicationKvStore.putBoolean("hasAlreadyLaunchedCategoriesDialog", false);
}
if(savedInstanceState == null){
if (savedInstanceState == null) {
//starting a fresh fragment.
// Open Last opened screen if it is Contributions or Nearby, otherwise Contributions
if(applicationKvStore.getBoolean("last_opened_nearby")){
if (applicationKvStore.getBoolean("last_opened_nearby")) {
setTitle(getString(R.string.nearby_fragment));
showNearby();
loadFragment(NearbyParentFragment.newInstance(),false);
}else{
loadFragment(NearbyParentFragment.newInstance(), false);
} else {
setTitle(getString(R.string.contributions_fragment));
loadFragment(ContributionsFragment.newInstance(),false);
loadFragment(ContributionsFragment.newInstance(), false);
}
}
setUpPager();
@ -165,7 +168,8 @@ public class MainActivity extends BaseActivity
if (VERSION.SDK_INT >= VERSION_CODES.Q) {
PermissionUtils.checkPermissionsAndPerformAction(
this,
() -> {},
() -> {
},
R.string.media_location_permission_denied,
R.string.add_location_manually,
permission.ACCESS_MEDIA_LOCATION);
@ -179,32 +183,33 @@ public class MainActivity extends BaseActivity
}
private void setUpPager() {
binding.fragmentMainNavTabLayout.setOnNavigationItemSelectedListener(navListener = (item) -> {
if (!item.getTitle().equals(getString(R.string.more))) {
// do not change title for more fragment
setTitle(item.getTitle());
}
// set last_opened_nearby true if item is nearby screen else set false
applicationKvStore.putBoolean("last_opened_nearby",
item.getTitle().equals(getString(R.string.nearby_fragment)));
final Fragment fragment = NavTab.of(item.getOrder()).newInstance();
return loadFragment(fragment, true);
});
binding.fragmentMainNavTabLayout.setOnNavigationItemSelectedListener(
navListener = (item) -> {
if (!item.getTitle().equals(getString(R.string.more))) {
// do not change title for more fragment
setTitle(item.getTitle());
}
// set last_opened_nearby true if item is nearby screen else set false
applicationKvStore.putBoolean("last_opened_nearby",
item.getTitle().equals(getString(R.string.nearby_fragment)));
final Fragment fragment = NavTab.of(item.getOrder()).newInstance();
return loadFragment(fragment, true);
});
}
private void setUpLoggedOutPager() {
loadFragment(ExploreFragment.newInstance(),false);
loadFragment(ExploreFragment.newInstance(), false);
binding.fragmentMainNavTabLayout.setOnNavigationItemSelectedListener(item -> {
if (!item.getTitle().equals(getString(R.string.more))) {
// do not change title for more fragment
setTitle(item.getTitle());
}
Fragment fragment = NavTabLoggedOut.of(item.getOrder()).newInstance();
return loadFragment(fragment,true);
return loadFragment(fragment, true);
});
}
private boolean loadFragment(Fragment fragment,boolean showBottom ) {
private boolean loadFragment(Fragment fragment, boolean showBottom) {
//showBottom so that we do not show the bottom tray again when constructing
//from the saved instance state.
if (fragment instanceof ContributionsFragment) {
@ -234,7 +239,8 @@ public class MainActivity extends BaseActivity
bookmarkFragment = (BookmarkFragment) fragment;
activeFragment = ActiveFragment.BOOKMARK;
} else if (fragment == null && showBottom) {
if (applicationKvStore.getBoolean("login_skipped") == true) { // If logged out, more sheet is different
if (applicationKvStore.getBoolean("login_skipped")
== true) { // If logged out, more sheet is different
MoreBottomSheetLoggedOutFragment bottomSheet = new MoreBottomSheetLoggedOutFragment();
bottomSheet.show(getSupportFragmentManager(),
"MoreBottomSheetLoggedOut");
@ -264,28 +270,30 @@ public class MainActivity extends BaseActivity
}
/**
* Adds number of uploads next to tab text "Contributions" then it will look like
* "Contributions (NUMBER)"
* Adds number of uploads next to tab text "Contributions" then it will look like "Contributions
* (NUMBER)"
*
* @param uploadCount
*/
public void setNumOfUploads(int uploadCount) {
if (activeFragment == ActiveFragment.CONTRIBUTIONS) {
setTitle(getResources().getString(R.string.contributions_fragment) +" "+ (
setTitle(getResources().getString(R.string.contributions_fragment) + " " + (
!(uploadCount == 0) ?
getResources()
.getQuantityString(R.plurals.contributions_subtitle,
uploadCount, uploadCount):getString(R.string.contributions_subtitle_zero)));
getResources()
.getQuantityString(R.plurals.contributions_subtitle,
uploadCount, uploadCount)
: getString(R.string.contributions_subtitle_zero)));
}
}
/**
* Resume the uploads that got stuck because of the app being killed
* or the device being rebooted.
*
* Resume the uploads that got stuck because of the app being killed or the device being
* rebooted.
* <p>
* When the app is terminated or the device is restarted, contributions remain in the
* 'STATE_IN_PROGRESS' state. This status persists and doesn't change during these events.
* So, retrieving contributions labeled as 'STATE_IN_PROGRESS'
* from the database will provide the list of uploads that appear as stuck on opening the app again
* 'STATE_IN_PROGRESS' state. This status persists and doesn't change during these events. So,
* retrieving contributions labeled as 'STATE_IN_PROGRESS' from the database will provide the
* list of uploads that appear as stuck on opening the app again
*/
@SuppressLint("CheckResult")
private void checkAndResumeStuckUploads() {
@ -294,9 +302,10 @@ public class MainActivity extends BaseActivity
.subscribeOn(Schedulers.io())
.blockingGet();
Timber.d("Resuming " + stuckUploads.size() + " uploads...");
if(!stuckUploads.isEmpty()) {
for(Contribution contribution: stuckUploads) {
if (!stuckUploads.isEmpty()) {
for (Contribution contribution : stuckUploads) {
contribution.setState(Contribution.STATE_QUEUED);
contribution.setDateUploadStarted(Calendar.getInstance().getTime());
Completable.fromAction(() -> contributionDao.saveSynchronous(contribution))
.subscribeOn(Schedulers.io())
.subscribe();
@ -323,24 +332,24 @@ public class MainActivity extends BaseActivity
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
String activeFragmentName = savedInstanceState.getString("activeFragment");
if(activeFragmentName != null) {
if (activeFragmentName != null) {
restoreActiveFragment(activeFragmentName);
}
}
private void restoreActiveFragment(@NonNull String fragmentName) {
if(fragmentName.equals(ActiveFragment.CONTRIBUTIONS.name())) {
if (fragmentName.equals(ActiveFragment.CONTRIBUTIONS.name())) {
setTitle(getString(R.string.contributions_fragment));
loadFragment(ContributionsFragment.newInstance(),false);
}else if(fragmentName.equals(ActiveFragment.NEARBY.name())) {
loadFragment(ContributionsFragment.newInstance(), false);
} else if (fragmentName.equals(ActiveFragment.NEARBY.name())) {
setTitle(getString(R.string.nearby_fragment));
loadFragment(NearbyParentFragment.newInstance(),false);
}else if(fragmentName.equals(ActiveFragment.EXPLORE.name())) {
loadFragment(NearbyParentFragment.newInstance(), false);
} else if (fragmentName.equals(ActiveFragment.EXPLORE.name())) {
setTitle(getString(R.string.navigation_item_explore));
loadFragment(ExploreFragment.newInstance(),false);
}else if(fragmentName.equals(ActiveFragment.BOOKMARK.name())) {
loadFragment(ExploreFragment.newInstance(), false);
} else if (fragmentName.equals(ActiveFragment.BOOKMARK.name())) {
setTitle(getString(R.string.bookmarks));
loadFragment(BookmarkFragment.newInstance(),false);
loadFragment(BookmarkFragment.newInstance(), false);
}
}
@ -356,8 +365,9 @@ public class MainActivity extends BaseActivity
// Means that nearby fragment is visible
/* If function nearbyParentFragment.backButtonClick() returns false, it means that the bottomsheet is
not expanded. So if the back button is pressed, then go back to the Contributions tab */
if(!nearbyParentFragment.backButtonClicked()){
getSupportFragmentManager().beginTransaction().remove(nearbyParentFragment).commit();
if (!nearbyParentFragment.backButtonClicked()) {
getSupportFragmentManager().beginTransaction().remove(nearbyParentFragment)
.commit();
setSelectedItemId(NavTab.CONTRIBUTIONS.code());
}
} else if (exploreFragment != null && activeFragment == ActiveFragment.EXPLORE) {
@ -382,18 +392,6 @@ public class MainActivity extends BaseActivity
//initBackButton();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.notifications:
// Starts notification activity on click to notification icon
NotificationActivity.startYourself(this, "unread");
return true;
default:
return super.onOptionsItemSelected(item);
}
}
/**
* Retry all failed uploads as soon as the user returns to the app
*/
@ -403,41 +401,45 @@ public class MainActivity extends BaseActivity
getContribution(Collections.singletonList(Contribution.STATE_FAILED))
.subscribeOn(Schedulers.io())
.subscribe(failedUploads -> {
for (Contribution contribution: failedUploads) {
for (Contribution contribution : failedUploads) {
contributionsFragment.retryUpload(contribution);
}
});
}
public void toggleLimitedConnectionMode() {
defaultKvStore.putBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED,
!defaultKvStore
.getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false));
if (defaultKvStore
.getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false)) {
viewUtilWrapper
.showShortToast(getBaseContext(), getString(R.string.limited_connection_enabled));
} else {
WorkRequestHelper.Companion.makeOneTimeWorkRequest(getApplicationContext(),
ExistingWorkPolicy.APPEND_OR_REPLACE);
viewUtilWrapper
.showShortToast(getBaseContext(), getString(R.string.limited_connection_disabled));
/**
* Handles item selection in the options menu. This method is called when a user interacts with
* the options menu in the Top Bar.
*/
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.upload_tab:
startActivity(new Intent(this, UploadProgressActivity.class));
return true;
case R.id.notifications:
// Starts notification activity on click to notification icon
NotificationActivity.startYourself(this, "unread");
return true;
default:
return super.onOptionsItemSelected(item);
}
}
public void centerMapToPlace(Place place) {
setSelectedItemId(NavTab.NEARBY.code());
nearbyParentFragment.setNearbyParentFragmentInstanceReadyCallback(new NearbyParentFragmentInstanceReadyCallback() {
@Override
public void onReady() {
nearbyParentFragment.centerMapToPlace(place);
}
});
nearbyParentFragment.setNearbyParentFragmentInstanceReadyCallback(
new NearbyParentFragmentInstanceReadyCallback() {
@Override
public void onReady() {
nearbyParentFragment.centerMapToPlace(place);
}
});
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
Timber.d(data!=null?data.toString():"onActivityResult data is null");
Timber.d(data != null ? data.toString() : "onActivityResult data is null");
super.onActivityResult(requestCode, resultCode, data);
controller.handleActivityResult(this, requestCode, resultCode, data);
}
@ -482,14 +484,15 @@ public class MainActivity extends BaseActivity
/**
* Load default language in onCreate from SharedPreferences
*/
private void loadLocale(){
final SharedPreferences preferences = getSharedPreferences("Settings", Activity.MODE_PRIVATE);
private void loadLocale() {
final SharedPreferences preferences = getSharedPreferences("Settings",
Activity.MODE_PRIVATE);
final String language = preferences.getString("language", "");
final SettingsFragment settingsFragment = new SettingsFragment();
settingsFragment.setLocale(this, language);
}
public NavTabLayout.OnNavigationItemSelectedListener getNavListener(){
public NavTabLayout.OnNavigationItemSelectedListener getNavListener() {
return navListener;
}
}