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