mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-27 21:03:54 +01:00
When opening the app, in the background (without any UI impact such as loading spinwheel) we check whether the server's last image is the app's last image.
If not, refresh. New "refresh" button having the same effect as swipe down.
This commit is contained in:
parent
9f9938a0b7
commit
288f9aae3f
8 changed files with 189 additions and 123 deletions
|
|
@ -59,6 +59,12 @@ class ContributionBoundaryCallback
|
|||
}
|
||||
fetchContributions(onRefreshFinish)
|
||||
}
|
||||
/**
|
||||
* Public method to fetch contributions, which internally calls the private fetchContributions().
|
||||
*/
|
||||
fun fetchContributionsPublic(onRefreshFinish: () -> Unit = {}) {
|
||||
fetchContributions(onRefreshFinish)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches contributions using the MediaWiki API
|
||||
|
|
|
|||
|
|
@ -2,9 +2,6 @@ package fr.free.nrw.commons.contributions;
|
|||
|
||||
import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
|
@ -13,6 +10,7 @@ import androidx.paging.DataSource.Factory;
|
|||
import androidx.paging.LivePagedListBuilder;
|
||||
import androidx.paging.PagedList;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
import fr.free.nrw.commons.auth.SessionManager;
|
||||
import fr.free.nrw.commons.contributions.ContributionsListContract.UserActionListener;
|
||||
import io.reactivex.Scheduler;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
|
|
@ -22,6 +20,7 @@ import java.util.List;
|
|||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import kotlin.Unit;
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* The presenter class for Contributions
|
||||
|
|
@ -33,7 +32,11 @@ public class ContributionsListPresenter implements UserActionListener {
|
|||
private final Scheduler ioThreadScheduler;
|
||||
|
||||
private final CompositeDisposable compositeDisposable;
|
||||
private final ContributionsRemoteDataSource contributionsRemoteDataSource;
|
||||
@Inject
|
||||
ContributionsRemoteDataSource contributionsRemoteDataSource;
|
||||
|
||||
@Inject
|
||||
SessionManager sessionManager;
|
||||
|
||||
LiveData<PagedList<Contribution>> contributionList;
|
||||
|
||||
|
|
@ -41,11 +44,6 @@ public class ContributionsListPresenter implements UserActionListener {
|
|||
|
||||
private List<Contribution> existingContributions = new ArrayList<>();
|
||||
|
||||
// Timer for polling new contributions
|
||||
private Handler pollingHandler;
|
||||
private Runnable pollingRunnable;
|
||||
private long pollingInterval = 24 * 60 * 60 * 1000L; // Poll every day
|
||||
|
||||
@Inject
|
||||
ContributionsListPresenter(
|
||||
final ContributionBoundaryCallback contributionBoundaryCallback,
|
||||
|
|
@ -107,9 +105,7 @@ public class ContributionsListPresenter implements UserActionListener {
|
|||
existingContributions.addAll(pagedList);
|
||||
liveData.setValue(existingContributions); // Update liveData with the latest list
|
||||
}
|
||||
});
|
||||
// Start polling for new contributions
|
||||
startPollingForNewContributions();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -117,7 +113,6 @@ public class ContributionsListPresenter implements UserActionListener {
|
|||
compositeDisposable.clear();
|
||||
contributionsRemoteDataSource.dispose();
|
||||
contributionBoundaryCallback.dispose();
|
||||
stopPollingForNewContributions();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -134,65 +129,42 @@ public class ContributionsListPresenter implements UserActionListener {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start polling for new contributions every 24 hour.
|
||||
*/
|
||||
private void startPollingForNewContributions() {
|
||||
if (pollingHandler != null) {
|
||||
stopPollingForNewContributions();
|
||||
}
|
||||
|
||||
pollingHandler = new Handler(Looper.getMainLooper());
|
||||
pollingRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
checkForNewContributions();
|
||||
pollingHandler.postDelayed(this, pollingInterval); // Repeat after the interval
|
||||
}
|
||||
};
|
||||
pollingHandler.post(pollingRunnable); // Start polling immediately
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the polling task when the view is detached or the activity is paused.
|
||||
*/
|
||||
private void stopPollingForNewContributions() {
|
||||
if (pollingHandler != null && pollingRunnable != null) {
|
||||
pollingHandler.removeCallbacks(pollingRunnable);
|
||||
pollingHandler = null;
|
||||
pollingRunnable = null;
|
||||
}
|
||||
}
|
||||
private String lastKnownIdentifier = null; // Declare and initialize
|
||||
|
||||
/**
|
||||
* Check for new contributions by comparing the latest contribution identifier.
|
||||
*/
|
||||
private void checkForNewContributions() {
|
||||
void checkForNewContributions() {
|
||||
|
||||
// Set the username before fetching contributions
|
||||
contributionsRemoteDataSource.setUserName(sessionManager.getUserName());
|
||||
|
||||
contributionsRemoteDataSource.fetchLatestContributionIdentifier(latestIdentifier -> {
|
||||
if (latestIdentifier != null && !latestIdentifier.equals(lastKnownIdentifier)) {
|
||||
lastKnownIdentifier = latestIdentifier;
|
||||
fetchAllContributions(); // Fetch the full list of contributions
|
||||
}
|
||||
return Unit.INSTANCE; // Explicitly return Unit for Kotlin compatibility
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch new contributions from the server and append them to the existing list.
|
||||
*/
|
||||
private void fetchAllContributions() {
|
||||
contributionsRemoteDataSource.fetchAllContributions(new ContributionsRemoteDataSource.LoadCallback<Contribution>() {
|
||||
@Override
|
||||
public void onResult(List<Contribution> newContributions) {
|
||||
if (newContributions != null && !newContributions.isEmpty()) {
|
||||
existingContributions.clear();
|
||||
existingContributions.addAll(newContributions);
|
||||
liveData.postValue(existingContributions); // Update liveData with the new list
|
||||
void fetchAllContributions() {
|
||||
contributionsRemoteDataSource.fetchContributions(
|
||||
new ContributionsRemoteDataSource.LoadCallback<>() {
|
||||
|
||||
@Override
|
||||
public void onResult(List<Contribution> newContributions) {
|
||||
if (newContributions != null && !newContributions.isEmpty()) {
|
||||
existingContributions.clear();
|
||||
existingContributions.addAll(newContributions);
|
||||
liveData.postValue(
|
||||
existingContributions); // Update liveData with the new list
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import androidx.paging.ItemKeyedDataSource
|
|||
import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.IO_THREAD
|
||||
import fr.free.nrw.commons.media.MediaClient
|
||||
import io.reactivex.Scheduler
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
|
@ -13,65 +14,65 @@ import javax.inject.Named
|
|||
* Data-Source which acts as mediator for contributions-data from the API
|
||||
*/
|
||||
class ContributionsRemoteDataSource
|
||||
@Inject
|
||||
constructor(
|
||||
private val mediaClient: MediaClient,
|
||||
@param:Named(IO_THREAD) private val ioThreadScheduler: Scheduler,
|
||||
) : ItemKeyedDataSource<Int, Contribution>() {
|
||||
private val compositeDisposable: CompositeDisposable = CompositeDisposable()
|
||||
var userName: String? = null
|
||||
@Inject
|
||||
constructor(
|
||||
private val mediaClient: MediaClient,
|
||||
@param:Named(IO_THREAD) private val ioThreadScheduler: Scheduler,
|
||||
) : ItemKeyedDataSource<Int, Contribution>() {
|
||||
private val compositeDisposable: CompositeDisposable = CompositeDisposable()
|
||||
var userName: String? = null
|
||||
|
||||
override fun loadInitial(
|
||||
params: LoadInitialParams<Int>,
|
||||
callback: LoadInitialCallback<Contribution>,
|
||||
) {
|
||||
fetchAllContributions(callback)
|
||||
override fun loadInitial(
|
||||
params: LoadInitialParams<Int>,
|
||||
callback: LoadInitialCallback<Contribution>,
|
||||
) {
|
||||
fetchContributions(callback)
|
||||
}
|
||||
|
||||
override fun loadAfter(
|
||||
params: LoadParams<Int>,
|
||||
callback: LoadCallback<Contribution>,
|
||||
) {
|
||||
fetchContributions(callback)
|
||||
}
|
||||
|
||||
override fun loadBefore(
|
||||
params: LoadParams<Int>,
|
||||
callback: LoadCallback<Contribution>,
|
||||
) {
|
||||
}
|
||||
|
||||
override fun getKey(item: Contribution): Int = item.pageId.hashCode()
|
||||
|
||||
/**
|
||||
* Fetches contributions using the MediaWiki API
|
||||
*/
|
||||
fun fetchContributions(callback: LoadCallback<Contribution>) {
|
||||
if (userName.isNullOrEmpty()) {
|
||||
Timber.e("Failed to fetch contributions: userName is null or empty")
|
||||
return
|
||||
}
|
||||
Timber.d("Fetching contributions for user: $userName")
|
||||
|
||||
override fun loadAfter(
|
||||
params: LoadParams<Int>,
|
||||
callback: LoadCallback<Contribution>,
|
||||
) {
|
||||
fetchAllContributions(callback)
|
||||
}
|
||||
compositeDisposable.add(
|
||||
mediaClient
|
||||
.getMediaListForUser(userName!!)
|
||||
.map { mediaList ->
|
||||
mediaList.map {
|
||||
Contribution(media = it, state = Contribution.STATE_COMPLETED)
|
||||
}
|
||||
}.subscribeOn(ioThreadScheduler)
|
||||
.subscribe({
|
||||
callback.onResult(it)
|
||||
}) { error: Throwable ->
|
||||
Timber.e(
|
||||
"Failed to fetch contributions: %s",
|
||||
error.message,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun loadBefore(
|
||||
params: LoadParams<Int>,
|
||||
callback: LoadCallback<Contribution>,
|
||||
) {
|
||||
}
|
||||
|
||||
override fun getKey(item: Contribution): Int = item.pageId.hashCode()
|
||||
|
||||
/**
|
||||
* Fetches contributions using the MediaWiki API
|
||||
*/
|
||||
fun fetchAllContributions(callback: LoadCallback<Contribution>) {
|
||||
if (userName.isNullOrEmpty()) {
|
||||
Timber.e("Failed to fetch contributions: userName is null or empty")
|
||||
return
|
||||
}
|
||||
Timber.d("Fetching contributions for user: $userName")
|
||||
|
||||
compositeDisposable.add(
|
||||
mediaClient
|
||||
.getMediaListForUser(userName!!)
|
||||
.map { mediaList ->
|
||||
mediaList.map {
|
||||
Contribution(media = it, state = Contribution.STATE_COMPLETED)
|
||||
}
|
||||
}.subscribeOn(ioThreadScheduler)
|
||||
.subscribe({ contributions ->
|
||||
// Pass the contributions to the callback
|
||||
callback.onResult(contributions)
|
||||
}) { error: Throwable ->
|
||||
Timber.e(
|
||||
"Failed to fetch contributions: %s",
|
||||
error.message,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
/**
|
||||
* Fetches the latest contribution identifier only
|
||||
*/
|
||||
|
|
@ -97,7 +98,7 @@ class ContributionsRemoteDataSource
|
|||
)
|
||||
}
|
||||
|
||||
fun dispose() {
|
||||
compositeDisposable.dispose()
|
||||
}
|
||||
fun dispose() {
|
||||
compositeDisposable.dispose()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
package fr.free.nrw.commons.contributions;
|
||||
|
||||
import static android.mediautil.MediaUtil.getContext;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
|
|
@ -7,13 +9,19 @@ import android.content.Intent;
|
|||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.AnimationUtils;
|
||||
import android.widget.ImageView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentManager.OnBackStackChangedListener;
|
||||
import androidx.work.ExistingWorkPolicy;
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView.OnNavigationItemSelectedListener;
|
||||
import fr.free.nrw.commons.databinding.MainBinding;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.WelcomeActivity;
|
||||
|
|
@ -22,6 +30,7 @@ import fr.free.nrw.commons.bookmarks.BookmarkFragment;
|
|||
import fr.free.nrw.commons.explore.ExploreFragment;
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
import fr.free.nrw.commons.location.LocationServiceManager;
|
||||
import fr.free.nrw.commons.media.MediaClient;
|
||||
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
|
||||
import fr.free.nrw.commons.navtab.MoreBottomSheetFragment;
|
||||
import fr.free.nrw.commons.navtab.MoreBottomSheetLoggedOutFragment;
|
||||
|
|
@ -32,6 +41,7 @@ import fr.free.nrw.commons.nearby.Place;
|
|||
import fr.free.nrw.commons.nearby.fragments.NearbyParentFragment;
|
||||
import fr.free.nrw.commons.nearby.fragments.NearbyParentFragment.NearbyParentFragmentInstanceReadyCallback;
|
||||
import fr.free.nrw.commons.notification.NotificationActivity;
|
||||
import fr.free.nrw.commons.notification.NotificationActivity.Companion;
|
||||
import fr.free.nrw.commons.notification.NotificationController;
|
||||
import fr.free.nrw.commons.quiz.QuizChecker;
|
||||
import fr.free.nrw.commons.settings.SettingsFragment;
|
||||
|
|
@ -41,16 +51,20 @@ 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.Scheduler;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
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 kotlin.Unit;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class MainActivity extends BaseActivity
|
||||
implements FragmentManager.OnBackStackChangedListener {
|
||||
implements OnBackStackChangedListener {
|
||||
|
||||
@Inject
|
||||
SessionManager sessionManager;
|
||||
|
|
@ -59,13 +73,18 @@ public class MainActivity extends BaseActivity
|
|||
@Inject
|
||||
ContributionDao contributionDao;
|
||||
|
||||
@Inject
|
||||
ContributionsListPresenter contributionsListPresenter;
|
||||
@Inject
|
||||
ContributionsRemoteDataSource dataSource;
|
||||
|
||||
private ContributionsFragment contributionsFragment;
|
||||
private NearbyParentFragment nearbyParentFragment;
|
||||
private ExploreFragment exploreFragment;
|
||||
private BookmarkFragment bookmarkFragment;
|
||||
public ActiveFragment activeFragment;
|
||||
private MediaDetailPagerFragment mediaDetailPagerFragment;
|
||||
private NavTabLayout.OnNavigationItemSelectedListener navListener;
|
||||
private OnNavigationItemSelectedListener navListener;
|
||||
|
||||
@Inject
|
||||
public LocationServiceManager locationManager;
|
||||
|
|
@ -413,14 +432,47 @@ public class MainActivity extends BaseActivity
|
|||
startActivity(new Intent(this, UploadProgressActivity.class));
|
||||
return true;
|
||||
case R.id.notifications:
|
||||
// Starts notification activity on click to notification icon
|
||||
NotificationActivity.Companion.startYourself(this, "unread");
|
||||
return true;
|
||||
case R.id.menu_refresh:
|
||||
// Refresh button handled in onCreateOptionsMenu through action layout
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
menu.clear(); // Clear the old menu to prevent duplication
|
||||
MenuInflater inflater = getMenuInflater();
|
||||
inflater.inflate(R.menu.contribution_activity_notification_menu, menu);
|
||||
|
||||
// Handle refresh button click
|
||||
MenuItem refreshItem = menu.findItem(R.id.menu_refresh);
|
||||
if (refreshItem != null) {
|
||||
View actionView = refreshItem.getActionView();
|
||||
if (actionView != null) {
|
||||
ImageView refreshIcon = actionView.findViewById(R.id.refresh_icon);
|
||||
if (refreshIcon != null) {
|
||||
refreshIcon.setOnClickListener(v -> {
|
||||
// Clear previous animation and start new one
|
||||
v.clearAnimation();
|
||||
Animation rotateAnimation = AnimationUtils.loadAnimation(this, R.anim.rotate);
|
||||
v.startAnimation(rotateAnimation);
|
||||
|
||||
// Trigger refresh logic
|
||||
contributionsListPresenter.checkForNewContributions();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public void centerMapToPlace(Place place) {
|
||||
setSelectedItemId(NavTab.NEARBY.code());
|
||||
nearbyParentFragment.setNearbyParentFragmentInstanceReadyCallback(
|
||||
|
|
@ -435,14 +487,16 @@ public class MainActivity extends BaseActivity
|
|||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
|
||||
if ((applicationKvStore.getBoolean("firstrun", true)) &&
|
||||
(!applicationKvStore.getBoolean("login_skipped"))) {
|
||||
// Check if it's the first run and the user hasn't skipped login
|
||||
if (applicationKvStore.getBoolean("firstrun", true) &&
|
||||
!applicationKvStore.getBoolean("login_skipped")) {
|
||||
defaultKvStore.putBoolean("inAppCameraFirstRun", true);
|
||||
WelcomeActivity.startYourself(this);
|
||||
}
|
||||
|
||||
retryAllFailedUploads();
|
||||
// Background check for new contributions
|
||||
contributionsListPresenter.checkForNewContributions();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -480,7 +534,7 @@ public class MainActivity extends BaseActivity
|
|||
settingsFragment.setLocale(this, language);
|
||||
}
|
||||
|
||||
public NavTabLayout.OnNavigationItemSelectedListener getNavListener() {
|
||||
public OnNavigationItemSelectedListener getNavListener() {
|
||||
return navListener;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
9
app/src/main/res/anim/rotate.xml
Normal file
9
app/src/main/res/anim/rotate.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:fromDegrees="0"
|
||||
android:toDegrees="360"
|
||||
android:duration="300"
|
||||
android:pivotX="50%"
|
||||
android:pivotY="50%" />
|
||||
</set>
|
||||
19
app/src/main/res/layout/refresh_button_icon.xml
Normal file
19
app/src/main/res/layout/refresh_button_icon.xml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@android:color/transparent"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="center">
|
||||
|
||||
<!-- Refresh Icon with margin from the right -->
|
||||
<ImageView
|
||||
android:id="@+id/refresh_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:contentDescription="@string/refresh"
|
||||
android:layout_marginEnd="@dimen/activity_margin_horizontal"
|
||||
app:srcCompat="@android:drawable/ic_menu_rotate" />
|
||||
</RelativeLayout>
|
||||
|
|
@ -1,5 +1,10 @@
|
|||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/menu_refresh"
|
||||
android:title="Refresh"
|
||||
app:actionLayout="@layout/refresh_button_icon"
|
||||
app:showAsAction="ifRoom|withText" />
|
||||
<item
|
||||
android:id="@+id/upload_tab"
|
||||
android:title="Upload"
|
||||
|
|
@ -7,8 +12,7 @@
|
|||
app:showAsAction="ifRoom|withText" />
|
||||
<item
|
||||
android:id="@+id/notifications"
|
||||
android:menuCategory="secondary"
|
||||
android:title="@string/notifications"
|
||||
android:title="Notifications"
|
||||
app:actionLayout="@layout/notification_icon"
|
||||
app:showAsAction="ifRoom|withText" />
|
||||
</menu>
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
<string name="nearby_filter_search">Search View</string>
|
||||
<string name="nearby_filter_state">Place State</string>
|
||||
<string name="appwidget_img">Pic of the Day</string>
|
||||
<string name="refresh">Refresh</string>
|
||||
|
||||
<!--Other strings-->
|
||||
<plurals name="uploads_pending_notification_indicator">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue