From 288f9aae3ff7e1d4cb494bc3b76c7def3f9fe6a4 Mon Sep 17 00:00:00 2001 From: Sujal-Gupta-SG Date: Thu, 23 Jan 2025 21:16:32 +0530 Subject: [PATCH] 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. --- .../ContributionBoundaryCallback.kt | 6 + .../ContributionsListPresenter.java | 84 +++++-------- .../ContributionsRemoteDataSource.kt | 117 +++++++++--------- .../commons/contributions/MainActivity.java | 68 ++++++++-- app/src/main/res/anim/rotate.xml | 9 ++ .../main/res/layout/refresh_button_icon.xml | 19 +++ ...ontribution_activity_notification_menu.xml | 8 +- app/src/main/res/values/strings.xml | 1 + 8 files changed, 189 insertions(+), 123 deletions(-) create mode 100644 app/src/main/res/anim/rotate.xml create mode 100644 app/src/main/res/layout/refresh_button_icon.xml diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionBoundaryCallback.kt b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionBoundaryCallback.kt index b5075a21e..4b1deef11 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionBoundaryCallback.kt +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionBoundaryCallback.kt @@ -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 diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListPresenter.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListPresenter.java index c867511ab..4ce5a7bbf 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListPresenter.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListPresenter.java @@ -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> contributionList; @@ -41,11 +44,6 @@ public class ContributionsListPresenter implements UserActionListener { private List 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() { - @Override - public void onResult(List 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 newContributions) { + if (newContributions != null && !newContributions.isEmpty()) { + existingContributions.clear(); + existingContributions.addAll(newContributions); + liveData.postValue( + existingContributions); // Update liveData with the new list + } } - } - }); + }); } -} +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsRemoteDataSource.kt b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsRemoteDataSource.kt index a62030e90..c8e9ea001 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsRemoteDataSource.kt +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsRemoteDataSource.kt @@ -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() { - private val compositeDisposable: CompositeDisposable = CompositeDisposable() - var userName: String? = null +@Inject +constructor( + private val mediaClient: MediaClient, + @param:Named(IO_THREAD) private val ioThreadScheduler: Scheduler, +) : ItemKeyedDataSource() { + private val compositeDisposable: CompositeDisposable = CompositeDisposable() + var userName: String? = null - override fun loadInitial( - params: LoadInitialParams, - callback: LoadInitialCallback, - ) { - fetchAllContributions(callback) + override fun loadInitial( + params: LoadInitialParams, + callback: LoadInitialCallback, + ) { + fetchContributions(callback) + } + + override fun loadAfter( + params: LoadParams, + callback: LoadCallback, + ) { + fetchContributions(callback) + } + + override fun loadBefore( + params: LoadParams, + callback: LoadCallback, + ) { + } + + override fun getKey(item: Contribution): Int = item.pageId.hashCode() + + /** + * Fetches contributions using the MediaWiki API + */ + fun fetchContributions(callback: LoadCallback) { + 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, - callback: LoadCallback, - ) { - 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, - callback: LoadCallback, - ) { - } - - override fun getKey(item: Contribution): Int = item.pageId.hashCode() - - /** - * Fetches contributions using the MediaWiki API - */ - fun fetchAllContributions(callback: LoadCallback) { - 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() } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java index 03027f287..5037d7928 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java @@ -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; } } diff --git a/app/src/main/res/anim/rotate.xml b/app/src/main/res/anim/rotate.xml new file mode 100644 index 000000000..3d0f77138 --- /dev/null +++ b/app/src/main/res/anim/rotate.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/refresh_button_icon.xml b/app/src/main/res/layout/refresh_button_icon.xml new file mode 100644 index 000000000..7a781fecd --- /dev/null +++ b/app/src/main/res/layout/refresh_button_icon.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/app/src/main/res/menu/contribution_activity_notification_menu.xml b/app/src/main/res/menu/contribution_activity_notification_menu.xml index 5ecf919d0..49e45e134 100644 --- a/app/src/main/res/menu/contribution_activity_notification_menu.xml +++ b/app/src/main/res/menu/contribution_activity_notification_menu.xml @@ -1,5 +1,10 @@ + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d2bde98ab..c3bc93f7b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -22,6 +22,7 @@ Search View Place State Pic of the Day + Refresh