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:
Sujal-Gupta-SG 2025-01-23 21:16:32 +05:30
parent 9f9938a0b7
commit 288f9aae3f
8 changed files with 189 additions and 123 deletions

View file

@ -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

View file

@ -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,
@ -108,8 +106,6 @@ public class ContributionsListPresenter implements UserActionListener {
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,62 +129,39 @@ 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>() {
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
liveData.postValue(
existingContributions); // Update liveData with the new list
}
}
});

View file

@ -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,11 +14,11 @@ import javax.inject.Named
* Data-Source which acts as mediator for contributions-data from the API
*/
class ContributionsRemoteDataSource
@Inject
constructor(
@Inject
constructor(
private val mediaClient: MediaClient,
@param:Named(IO_THREAD) private val ioThreadScheduler: Scheduler,
) : ItemKeyedDataSource<Int, Contribution>() {
) : ItemKeyedDataSource<Int, Contribution>() {
private val compositeDisposable: CompositeDisposable = CompositeDisposable()
var userName: String? = null
@ -25,14 +26,14 @@ class ContributionsRemoteDataSource
params: LoadInitialParams<Int>,
callback: LoadInitialCallback<Contribution>,
) {
fetchAllContributions(callback)
fetchContributions(callback)
}
override fun loadAfter(
params: LoadParams<Int>,
callback: LoadCallback<Contribution>,
) {
fetchAllContributions(callback)
fetchContributions(callback)
}
override fun loadBefore(
@ -46,7 +47,7 @@ class ContributionsRemoteDataSource
/**
* Fetches contributions using the MediaWiki API
*/
fun fetchAllContributions(callback: LoadCallback<Contribution>) {
fun fetchContributions(callback: LoadCallback<Contribution>) {
if (userName.isNullOrEmpty()) {
Timber.e("Failed to fetch contributions: userName is null or empty")
return
@ -61,9 +62,8 @@ class ContributionsRemoteDataSource
Contribution(media = it, state = Contribution.STATE_COMPLETED)
}
}.subscribeOn(ioThreadScheduler)
.subscribe({ contributions ->
// Pass the contributions to the callback
callback.onResult(contributions)
.subscribe({
callback.onResult(it)
}) { error: Throwable ->
Timber.e(
"Failed to fetch contributions: %s",
@ -72,6 +72,7 @@ class ContributionsRemoteDataSource
},
)
}
/**
* Fetches the latest contribution identifier only
*/
@ -100,4 +101,4 @@ class ContributionsRemoteDataSource
fun dispose() {
compositeDisposable.dispose()
}
}
}

View file

@ -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;
}
}

View 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>

View 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>

View file

@ -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>

View file

@ -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">