|
12 | 12 | import android.view.MenuItem;
|
13 | 13 | import android.view.MotionEvent;
|
14 | 14 | import android.view.View;
|
| 15 | +import androidx.core.util.Pair; |
15 | 16 | import androidx.fragment.app.Fragment;
|
16 | 17 | import androidx.fragment.app.FragmentManager;
|
17 | 18 | import fr.free.nrw.commons.Media;
|
@@ -62,9 +63,16 @@ public class ReviewActivity extends BaseActivity {
|
62 | 63 |
|
63 | 64 | private List<Media> cachedMedia = new ArrayList<>();
|
64 | 65 |
|
| 66 | + /** Constants for managing media cache in the review activity */ |
| 67 | + // Name of SharedPreferences file for storing review activity preferences |
65 | 68 | private static final String PREF_NAME = "ReviewActivityPrefs";
|
| 69 | + // Key for storing the timestamp of last cache update |
66 | 70 | private static final String LAST_CACHE_TIME_KEY = "lastCacheTime";
|
| 71 | + |
| 72 | + // Maximum number of media files to store in cache |
67 | 73 | private static final int CACHE_SIZE = 5;
|
| 74 | + // Cache expiration time in milliseconds (24 hours) |
| 75 | + |
68 | 76 | private static final long CACHE_EXPIRY_TIME = 24 * 60 * 60 * 1000;
|
69 | 77 |
|
70 | 78 | @Override
|
@@ -113,10 +121,21 @@ protected void onCreate(Bundle savedInstanceState) {
|
113 | 121 | Drawable d[]=binding.skipImage.getCompoundDrawablesRelative();
|
114 | 122 | d[2].setColorFilter(getApplicationContext().getResources().getColor(R.color.button_blue), PorterDuff.Mode.SRC_IN);
|
115 | 123 |
|
| 124 | + /** |
| 125 | + * Restores the previous state of the activity or initializes a new review session. |
| 126 | + * Checks if there's a saved media state from a previous session (e.g., before screen rotation). |
| 127 | + * If found, restores the last viewed image and its detail view. |
| 128 | + * Otherwise, starts a new random image review session. |
| 129 | + * |
| 130 | + * @param savedInstanceState Bundle containing the activity's previously saved state, if any |
| 131 | + */ |
116 | 132 | if (savedInstanceState != null && savedInstanceState.getParcelable(SAVED_MEDIA) != null) {
|
| 133 | + // Restore the previously viewed image if state exists |
117 | 134 | updateImage(savedInstanceState.getParcelable(SAVED_MEDIA));
|
| 135 | + // Restore media detail view (handles configuration changes like screen rotation) |
118 | 136 | setUpMediaDetailOnOrientation();
|
119 | 137 | } else {
|
| 138 | + // Start fresh review session with a random image |
120 | 139 | runRandomizer();
|
121 | 140 | }
|
122 | 141 |
|
@@ -145,54 +164,173 @@ public boolean onSupportNavigateUp() {
|
145 | 164 | return true;
|
146 | 165 | }
|
147 | 166 |
|
| 167 | + /** |
| 168 | + * Initiates the process of loading a random media file for review. |
| 169 | + * This method: |
| 170 | + * - Resets the UI state |
| 171 | + * - Shows loading indicator |
| 172 | + * - Manages media cache |
| 173 | + * - Either loads from cache or fetches new media |
| 174 | + * |
| 175 | + * The method is annotated with @SuppressLint("CheckResult") as the Observable |
| 176 | + * subscription is handled through CompositeDisposable in the implementation. |
| 177 | + * |
| 178 | + * @return true indicating successful initiation of the randomization process |
| 179 | + */ |
148 | 180 | @SuppressLint("CheckResult")
|
149 | 181 | public boolean runRandomizer() {
|
| 182 | + // Reset flag for tracking presence of non-hidden categories |
150 | 183 | hasNonHiddenCategories = false;
|
| 184 | + // Display loading indicator while fetching media |
151 | 185 | binding.pbReviewImage.setVisibility(View.VISIBLE);
|
| 186 | + // Reset view pager to first page |
152 | 187 | binding.viewPagerReview.setCurrentItem(0);
|
153 | 188 |
|
| 189 | + // Check cache status and determine source of next media |
154 | 190 | if (cachedMedia.isEmpty() || isCacheExpired()) {
|
| 191 | + // Fetch and cache new media if cache is empty or expired |
155 | 192 | fetchAndCacheMedia();
|
156 | 193 | } else {
|
| 194 | + // Use next media file from existing cache |
157 | 195 | processNextCachedMedia();
|
158 | 196 | }
|
159 | 197 | return true;
|
160 | 198 | }
|
161 | 199 |
|
| 200 | + /** |
| 201 | + * Batch checks whether multiple files from the cache are used in wikis. |
| 202 | + * This is a more efficient way to process multiple files compared to checking them one by one. |
| 203 | + * |
| 204 | + * @param mediaList List of Media objects to check for usage |
| 205 | + */ |
| 206 | + /** |
| 207 | + * Batch checks whether multiple files from the cache are used in wikis. |
| 208 | + * This is a more efficient way to process multiple files compared to checking them one by one. |
| 209 | + * |
| 210 | + * @param mediaList List of Media objects to check for usage |
| 211 | + */ |
| 212 | + private void batchCheckFilesUsage(List<Media> mediaList) { |
| 213 | + // Extract filenames from media objects |
| 214 | + List<String> filenames = new ArrayList<>(); |
| 215 | + for (Media media : mediaList) { |
| 216 | + if (media.getFilename() != null) { |
| 217 | + filenames.add(media.getFilename()); |
| 218 | + } |
| 219 | + } |
| 220 | + |
| 221 | + compositeDisposable.add( |
| 222 | + reviewHelper.checkFileUsageBatch(filenames) |
| 223 | + .subscribeOn(Schedulers.io()) |
| 224 | + .observeOn(AndroidSchedulers.mainThread()) |
| 225 | + .toList() |
| 226 | + .subscribe(results -> { |
| 227 | + // Process each result |
| 228 | + for (kotlin.Pair<String, Boolean> result : results) { |
| 229 | + String filename = result.getFirst(); |
| 230 | + Boolean isUsed = result.getSecond(); |
| 231 | + |
| 232 | + // Find corresponding media object |
| 233 | + for (Media media : mediaList) { |
| 234 | + if (filename.equals(media.getFilename())) { |
| 235 | + if (!isUsed) { |
| 236 | + // If file is not used, proceed with category check |
| 237 | + findNonHiddenCategories(media); |
| 238 | + } |
| 239 | + break; |
| 240 | + } |
| 241 | + } |
| 242 | + } |
| 243 | + }, this::handleError)); |
| 244 | + } |
| 245 | + |
| 246 | + |
| 247 | + /** |
| 248 | + * Fetches and caches new media files for review. |
| 249 | + * Uses RxJava to: |
| 250 | + * - Generate a range of indices for the desired cache size |
| 251 | + * - Fetch random media files asynchronously |
| 252 | + * - Handle thread scheduling between IO and UI operations |
| 253 | + * - Store the fetched media in cache |
| 254 | + * |
| 255 | + * The operation is added to compositeDisposable for proper lifecycle management. |
| 256 | + */ |
162 | 257 | private void fetchAndCacheMedia() {
|
163 |
| - compositeDisposable.add(Observable.range(0, CACHE_SIZE) |
164 |
| - .flatMap(i -> reviewHelper.getRandomMedia().toObservable()) |
165 |
| - .subscribeOn(Schedulers.io()) |
166 |
| - .observeOn(AndroidSchedulers.mainThread()) |
167 |
| - .toList() |
168 |
| - .subscribe(mediaList -> { |
169 |
| - cachedMedia.clear(); |
170 |
| - cachedMedia.addAll(mediaList); |
171 |
| - updateLastCacheTime(); |
172 |
| - processNextCachedMedia(); |
173 |
| - }, this::handleError)); |
| 258 | + compositeDisposable.add( |
| 259 | + Observable.range(0, CACHE_SIZE) |
| 260 | + .flatMap(i -> reviewHelper.getRandomMedia().toObservable()) |
| 261 | + .subscribeOn(Schedulers.io()) |
| 262 | + .observeOn(AndroidSchedulers.mainThread()) |
| 263 | + .toList() |
| 264 | + .subscribe(mediaList -> { |
| 265 | + // Clear existing cache |
| 266 | + cachedMedia.clear(); |
| 267 | + |
| 268 | + // Start batch check process |
| 269 | + batchCheckFilesUsage(mediaList); |
| 270 | + |
| 271 | + // Update cache with new media |
| 272 | + cachedMedia.addAll(mediaList); |
| 273 | + updateLastCacheTime(); |
| 274 | + |
| 275 | + // Process first media item if available |
| 276 | + if (!cachedMedia.isEmpty()) { |
| 277 | + processNextCachedMedia(); |
| 278 | + } |
| 279 | + }, this::handleError)); |
174 | 280 | }
|
175 | 281 |
|
| 282 | + /** |
| 283 | + * Processes the next media file from the cache. |
| 284 | + * If cache is not empty, removes and processes the first media file. |
| 285 | + * If cache is empty, triggers a new fetch operation. |
| 286 | + * |
| 287 | + * This method ensures continuous flow of media files for review |
| 288 | + * while maintaining the cache mechanism. |
| 289 | + */ |
176 | 290 | private void processNextCachedMedia() {
|
177 | 291 | if (!cachedMedia.isEmpty()) {
|
| 292 | + // Remove and get the first media from cache |
178 | 293 | Media media = cachedMedia.remove(0);
|
| 294 | + |
179 | 295 | checkWhetherFileIsUsedInWikis(media);
|
180 | 296 | } else {
|
| 297 | + // Refill cache if empty |
181 | 298 | fetchAndCacheMedia();
|
182 | 299 | }
|
183 | 300 | }
|
184 | 301 |
|
| 302 | + /** |
| 303 | + * Checks if the current cache has expired. |
| 304 | + * Cache expiration is determined by comparing the last cache time |
| 305 | + * with the current time against the configured expiry duration. |
| 306 | + * |
| 307 | + * @return true if cache has expired, false otherwise |
| 308 | + */ |
185 | 309 | private boolean isCacheExpired() {
|
| 310 | + // Get shared preferences instance |
186 | 311 | SharedPreferences prefs = getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
|
| 312 | + |
187 | 313 | long lastCacheTime = prefs.getLong(LAST_CACHE_TIME_KEY, 0);
|
| 314 | + |
188 | 315 | long currentTime = System.currentTimeMillis();
|
| 316 | + |
189 | 317 | return (currentTime - lastCacheTime) > CACHE_EXPIRY_TIME;
|
190 | 318 | }
|
191 | 319 |
|
| 320 | + |
| 321 | + /** |
| 322 | + * Updates the timestamp of the last cache operation. |
| 323 | + * Stores the current time in SharedPreferences to track |
| 324 | + * cache freshness for future operations. |
| 325 | + */ |
192 | 326 | private void updateLastCacheTime() {
|
| 327 | + |
193 | 328 | SharedPreferences prefs = getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
|
| 329 | + |
194 | 330 | SharedPreferences.Editor editor = prefs.edit();
|
| 331 | + // Store current timestamp as last cache time |
195 | 332 | editor.putLong(LAST_CACHE_TIME_KEY, System.currentTimeMillis());
|
| 333 | + // Apply changes asynchronously |
196 | 334 | editor.apply();
|
197 | 335 | }
|
198 | 336 |
|
@@ -305,10 +443,20 @@ public void showReviewImageInfo() {
|
305 | 443 | null,
|
306 | 444 | null);
|
307 | 445 | }
|
| 446 | + /** |
| 447 | + * Handles errors that occur during media processing operations. |
| 448 | + * This is a generic error handler that: |
| 449 | + * - Hides the loading indicator |
| 450 | + * - Shows a user-friendly error message via Snackbar |
| 451 | + * |
| 452 | + * Used as error callback for RxJava operations and other async tasks. |
| 453 | + * |
| 454 | + * @param error The Throwable that was caught during operation |
| 455 | + */ |
308 | 456 | private void handleError(Throwable error) {
|
309 | 457 | binding.pbReviewImage.setVisibility(View.GONE);
|
| 458 | + // Show error message to user via Snackbar |
310 | 459 | ViewUtil.showShortSnackbar(binding.drawerLayout, R.string.error_review);
|
311 |
| - |
312 | 460 | }
|
313 | 461 |
|
314 | 462 |
|
|
0 commit comments