mirror of
				https://github.com/commons-app/apps-android-commons.git
				synced 2025-10-31 06:43:56 +01:00 
			
		
		
		
	Migrated contributions folder Files from java to kotlin (#6176)
* Rename .java to .kt * Migrated ContributionController * Rename .java to .kt * Migrated ContributionDao * Rename .java to .kt * Migrated ContributionsContract,ContributionFragment,ContributionListAdapter,ContributionsListContract from java to Kotlin * Rename .java to .kt * converted/Migrated * converted/Migrated * Rename .java to .kt * Migrated ContributionController * Rename .java to .kt * Migrated ContributionDao * Rename .java to .kt * Migrated ContributionsContract,ContributionFragment,ContributionListAdapter,ContributionsListContract from java to Kotlin * Rename .java to .kt * Show placeholder and display depiction section when no depictions are available (#6163) (#6165) * corrected * corrected * Update MediaDetailFragment.kt Spelling correction * Migrated AboutActivity from Java to Kotlin (#6158) * Rename Constants to Follow Kotlin Naming Conventions >This PR refactors constant names in the project to adhere to Kotlin's UPPERCASE_SNAKE_CASE naming convention, improving code readability and maintaining consistency across the codebase. >Renamed the following constants in LoginActivity: >saveProgressDialog → SAVE_PROGRESS_DIALOG >saveErrorMessage → SAVE_ERROR_MESSAGE >saveUsername → SAVE_USERNAME >savePassword → SAVE_PASSWORD >Updated all references to these constants throughout the project. * Update Project_Default.xml * Refactor variable names to adhere to naming conventions Renamed variables to use camel case: -UPLOAD_COUNT_THRESHOLD → uploadCountThreshold -REVERT_PERCENTAGE_FOR_MESSAGE → revertPercentageForMessage -REVERT_SHARED_PREFERENCE → revertSharedPreference -UPLOAD_SHARED_PREFERENCE → uploadSharedPreference Renamed variables with uppercase initials to lowercase for alignment with Kotlin conventions: -Latitude → latitude -Longitude → longitude -Accuracy → accuracy Refactored the following variable names: -NUMBER_OF_QUESTIONS → numberOfQuestions -MULTIPLIER_TO_GET_PERCENTAGE → multiplierToGetPercentage * Refactor Dialog View Initialization with Null-Safe Calls This PR refactors the dialog setup code in CustomSelectorActivity to improve safety and readability by replacing explicit casts with null-safe generic calls for findViewById. >Replaced explicit casting (as Button and as TextView) with the generic findViewById<T>() method for improved type safety. >Added null-safety (?.) to avoid potential crashes if a view is not found in the dialog layout. why changed:- >Prevents runtime crashes caused by NullPointerException when a view is missing in the layout. * Refactor Unit Test: Replace Unsafe Casting with Type-Safe Mocking for findViewById >PR refactors the unit test for NearbyParentFragment by replacing unsafe casting in the findViewById mocking statements with type-safe >Ensured all findViewById mocks now use a consistent, type-safe format (findViewById<View>(...)) to reduce verbosity and potential casting errors. >Verified the functionality of prepareViewsForSheetPosition remains unchanged, ensuring no regression in test behavior. * Update NearbyParentFragmentUnitTest.kt * Refactor: Rename Constants to Follow CamelCase Naming Convention >Updated all constant variable names to follow the camelCase naming convention, removing underscores in the middle or end. >Ensured variable names remain descriptive and align with code readability best practices. * Replace private val with const val for URL constants in QuizController * Renaming the constant to use UPPER_SNAKE_CASE * Renaming the constant to use UPPER_SNAKE_CASE * Update Done * **Refactor: Convert `minimumThresholdForSwipe` to a compile-time constant** * Convert AboutActivity from Java to Kotlin This PR converts the AboutActivity class from Java to Kotlin >Testing: >Verified all functionalities of the AboutActivity, including toolbar setup, intent launches, and dialog interactions, to ensure behavior remains consistent post-conversion. >Successfully ran unit tests for AboutActivity to confirm the correctness of methods and logic. * Thank you for the suggestion! Since these methods all take a single View parameter, replacing them with method references is a great way to simplify the code and improve readability. I'll updated the code accordingly. Added a TODO in the code as a reminder to refactor this in the future. --------- Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com> * Localisation updates from https://translatewiki.net. * Feat: Make it smoother to switch between nearby and explore maps (#6164) * Nearby: Add 'Show in Explore' 3-dots menu item * MainActivity: Add methods to pass extras between Nearby and Explore * MainActivity: Extend loadFragment() to support passing fragment arguments * Nearby: Add ability to navigate to Explore fragment on 'Show in Explore' click * Explore: Read fragment arguments for Nearby map data and update Explore map if present * Explore: Add 'Show in Nearby' 3-dots menu item. Only visible when Map tab is selected * Explore: On 'Show in Nearby' click, navigate to Nearby fragment, passing map data as fragment args * Nearby: Read fragment arguments for Explore map data and update Nearby map if present * MainActivity: Fix memory leaks when navigating between bottom nav destinations * Explore: Fix crashes caused by unattached map fragment * Refactor code to pass unit tests * Explore: Format javadocs --------- Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com> * Localisation updates from https://translatewiki.net. * enhance spammy category filter (#6167) Signed-off-by: parneet-guraya <gurayaparneet@gmail.com> * Localisation updates from https://translatewiki.net. * correction * correction * correction * GitHub workflow to build betaDebug (#6174) * [Bug fix] Check if duplicate exist using both original and modified file's checksum (#6169) * check original file's SHA too along with modified one Signed-off-by: parneet-guraya <gurayaparneet@gmail.com> * fix tests Signed-off-by: parneet-guraya <gurayaparneet@gmail.com> --------- Signed-off-by: parneet-guraya <gurayaparneet@gmail.com> * Add multiline input for caption and description (#6173) * allow multiple lines for description/caption * make caption multiline too --------- Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com> * correction --------- Signed-off-by: parneet-guraya <gurayaparneet@gmail.com> Co-authored-by: Akshay Komar <146421342+Akshaykomar890@users.noreply.github.com> Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com> Co-authored-by: translatewiki.net <l10n-bot@translatewiki.net> Co-authored-by: Ifeoluwa Andrew Omole <iomole3@gmail.com> Co-authored-by: Parneet Singh <111801812+parneet-guraya@users.noreply.github.com> Co-authored-by: Matija Nalis <mnalis-git@voyager.hr>
This commit is contained in:
		
							parent
							
								
									1e77b1457a
								
							
						
					
					
						commit
						12cadd0186
					
				
					 49 changed files with 3630 additions and 3529 deletions
				
			
		|  | @ -1,405 +0,0 @@ | |||
| package fr.free.nrw.commons.contributions; | ||||
| 
 | ||||
| import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT; | ||||
| 
 | ||||
| import android.Manifest.permission; | ||||
| import android.app.Activity; | ||||
| import android.content.Context; | ||||
| import android.content.Intent; | ||||
| import android.widget.Toast; | ||||
| import androidx.activity.result.ActivityResult; | ||||
| import androidx.activity.result.ActivityResultLauncher; | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.lifecycle.LiveData; | ||||
| import androidx.paging.DataSource.Factory; | ||||
| import androidx.paging.LivePagedListBuilder; | ||||
| import androidx.paging.PagedList; | ||||
| import fr.free.nrw.commons.R; | ||||
| import fr.free.nrw.commons.filepicker.DefaultCallback; | ||||
| import fr.free.nrw.commons.filepicker.FilePicker; | ||||
| import fr.free.nrw.commons.filepicker.FilePicker.ImageSource; | ||||
| import fr.free.nrw.commons.filepicker.UploadableFile; | ||||
| import fr.free.nrw.commons.kvstore.JsonKvStore; | ||||
| import fr.free.nrw.commons.location.LatLng; | ||||
| import fr.free.nrw.commons.location.LocationPermissionsHelper; | ||||
| import fr.free.nrw.commons.location.LocationPermissionsHelper.LocationPermissionCallback; | ||||
| import fr.free.nrw.commons.location.LocationServiceManager; | ||||
| import fr.free.nrw.commons.nearby.Place; | ||||
| import fr.free.nrw.commons.upload.UploadActivity; | ||||
| import fr.free.nrw.commons.utils.DialogUtil; | ||||
| import fr.free.nrw.commons.utils.PermissionUtils; | ||||
| import fr.free.nrw.commons.utils.ViewUtil; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Arrays; | ||||
| import java.util.Collections; | ||||
| import java.util.List; | ||||
| import javax.inject.Inject; | ||||
| import javax.inject.Named; | ||||
| import javax.inject.Singleton; | ||||
| 
 | ||||
| @Singleton | ||||
| public class ContributionController { | ||||
| 
 | ||||
|     public static final String ACTION_INTERNAL_UPLOADS = "internalImageUploads"; | ||||
|     private final JsonKvStore defaultKvStore; | ||||
|     private LatLng locationBeforeImageCapture; | ||||
|     private boolean isInAppCameraUpload; | ||||
|     public LocationPermissionCallback locationPermissionCallback; | ||||
|     private LocationPermissionsHelper locationPermissionsHelper; | ||||
|     // Temporarily disabled, see issue [https://github.com/commons-app/apps-android-commons/issues/5847] | ||||
|     // LiveData<PagedList<Contribution>> failedAndPendingContributionList; | ||||
|     LiveData<PagedList<Contribution>> pendingContributionList; | ||||
|     LiveData<PagedList<Contribution>> failedContributionList; | ||||
| 
 | ||||
|     @Inject | ||||
|     LocationServiceManager locationManager; | ||||
| 
 | ||||
|     @Inject | ||||
|     ContributionsRepository repository; | ||||
| 
 | ||||
|     @Inject | ||||
|     public ContributionController(@Named("default_preferences") JsonKvStore defaultKvStore) { | ||||
|         this.defaultKvStore = defaultKvStore; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check for permissions and initiate camera click | ||||
|      */ | ||||
|     public void initiateCameraPick(Activity activity, | ||||
|         ActivityResultLauncher<String[]> inAppCameraLocationPermissionLauncher, | ||||
|         ActivityResultLauncher<Intent> resultLauncher) { | ||||
|         boolean useExtStorage = defaultKvStore.getBoolean("useExternalStorage", true); | ||||
|         if (!useExtStorage) { | ||||
|             initiateCameraUpload(activity, resultLauncher); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         PermissionUtils.checkPermissionsAndPerformAction(activity, | ||||
|             () -> { | ||||
|                 if (defaultKvStore.getBoolean("inAppCameraFirstRun")) { | ||||
|                     defaultKvStore.putBoolean("inAppCameraFirstRun", false); | ||||
|                     askUserToAllowLocationAccess(activity, inAppCameraLocationPermissionLauncher, resultLauncher); | ||||
|                 } else if (defaultKvStore.getBoolean("inAppCameraLocationPref")) { | ||||
|                     createDialogsAndHandleLocationPermissions(activity, | ||||
|                         inAppCameraLocationPermissionLauncher, resultLauncher); | ||||
|                 } else { | ||||
|                     initiateCameraUpload(activity, resultLauncher); | ||||
|                 } | ||||
|             }, | ||||
|             R.string.storage_permission_title, | ||||
|             R.string.write_storage_permission_rationale, | ||||
|             PermissionUtils.getPERMISSIONS_STORAGE()); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Asks users to provide location access | ||||
|      * | ||||
|      * @param activity | ||||
|      */ | ||||
|     private void createDialogsAndHandleLocationPermissions(Activity activity, | ||||
|         ActivityResultLauncher<String[]> inAppCameraLocationPermissionLauncher, | ||||
|         ActivityResultLauncher<Intent> resultLauncher) { | ||||
|         locationPermissionCallback = new LocationPermissionCallback() { | ||||
|             @Override | ||||
|             public void onLocationPermissionDenied(String toastMessage) { | ||||
|                 Toast.makeText( | ||||
|                     activity, | ||||
|                     toastMessage, | ||||
|                     Toast.LENGTH_LONG | ||||
|                 ).show(); | ||||
|                 initiateCameraUpload(activity, resultLauncher); | ||||
|             } | ||||
| 
 | ||||
|             @Override | ||||
|             public void onLocationPermissionGranted() { | ||||
|                 if (!locationPermissionsHelper.isLocationAccessToAppsTurnedOn()) { | ||||
|                     showLocationOffDialog(activity, R.string.in_app_camera_needs_location, | ||||
|                         R.string.in_app_camera_location_unavailable, resultLauncher); | ||||
|                 } else { | ||||
|                     initiateCameraUpload(activity, resultLauncher); | ||||
|                 } | ||||
|             } | ||||
|         }; | ||||
| 
 | ||||
|         locationPermissionsHelper = new LocationPermissionsHelper( | ||||
|             activity, locationManager, locationPermissionCallback); | ||||
|         if (inAppCameraLocationPermissionLauncher != null) { | ||||
|             inAppCameraLocationPermissionLauncher.launch( | ||||
|                 new String[]{permission.ACCESS_FINE_LOCATION}); | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Shows a dialog alerting the user about location services being off and asking them to turn it | ||||
|      * on | ||||
|      * TODO: Add a seperate callback in LocationPermissionsHelper for this. | ||||
|      *      Ref: https://github.com/commons-app/apps-android-commons/pull/5494/files#r1510553114 | ||||
|      * | ||||
|      * @param activity           Activity reference | ||||
|      * @param dialogTextResource Resource id of text to be shown in dialog | ||||
|      * @param toastTextResource  Resource id of text to be shown in toast | ||||
|      * @param resultLauncher | ||||
|      */ | ||||
|     private void showLocationOffDialog(Activity activity, int dialogTextResource, | ||||
|         int toastTextResource, ActivityResultLauncher<Intent> resultLauncher) { | ||||
|         DialogUtil | ||||
|             .showAlertDialog(activity, | ||||
|                 activity.getString(R.string.ask_to_turn_location_on), | ||||
|                 activity.getString(dialogTextResource), | ||||
|                 activity.getString(R.string.title_app_shortcut_setting), | ||||
|                 activity.getString(R.string.cancel), | ||||
|                 () -> locationPermissionsHelper.openLocationSettings(activity), | ||||
|                 () -> { | ||||
|                     Toast.makeText(activity, activity.getString(toastTextResource), | ||||
|                         Toast.LENGTH_LONG).show(); | ||||
|                     initiateCameraUpload(activity, resultLauncher); | ||||
|                 } | ||||
|             ); | ||||
|     } | ||||
| 
 | ||||
|     public void handleShowRationaleFlowCameraLocation(Activity activity, | ||||
|         ActivityResultLauncher<String[]> inAppCameraLocationPermissionLauncher, | ||||
|         ActivityResultLauncher<Intent> resultLauncher) { | ||||
|         DialogUtil.showAlertDialog(activity, activity.getString(R.string.location_permission_title), | ||||
|             activity.getString(R.string.in_app_camera_location_permission_rationale), | ||||
|             activity.getString(android.R.string.ok), | ||||
|             activity.getString(android.R.string.cancel), | ||||
|             () -> { | ||||
|                 createDialogsAndHandleLocationPermissions(activity, | ||||
|                     inAppCameraLocationPermissionLauncher, resultLauncher); | ||||
|             }, | ||||
|             () -> locationPermissionCallback.onLocationPermissionDenied( | ||||
|                 activity.getString(R.string.in_app_camera_location_permission_denied)), | ||||
|             null | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Suggest user to attach location information with pictures. If the user selects "Yes", then: | ||||
|      * <p> | ||||
|      * Location is taken from the EXIF if the default camera application does not redact location | ||||
|      * tags. | ||||
|      * <p> | ||||
|      * Otherwise, if the EXIF metadata does not have location information, then location captured by | ||||
|      * the app is used | ||||
|      * | ||||
|      * @param activity | ||||
|      */ | ||||
|     private void askUserToAllowLocationAccess(Activity activity, | ||||
|         ActivityResultLauncher<String[]> inAppCameraLocationPermissionLauncher, | ||||
|         ActivityResultLauncher<Intent> resultLauncher) { | ||||
|         DialogUtil.showAlertDialog(activity, | ||||
|             activity.getString(R.string.in_app_camera_location_permission_title), | ||||
|             activity.getString(R.string.in_app_camera_location_access_explanation), | ||||
|             activity.getString(R.string.option_allow), | ||||
|             activity.getString(R.string.option_dismiss), | ||||
|             () -> { | ||||
|                 defaultKvStore.putBoolean("inAppCameraLocationPref", true); | ||||
|                 createDialogsAndHandleLocationPermissions(activity, | ||||
|                     inAppCameraLocationPermissionLauncher, resultLauncher); | ||||
|             }, | ||||
|             () -> { | ||||
|                 ViewUtil.showLongToast(activity, R.string.in_app_camera_location_permission_denied); | ||||
|                 defaultKvStore.putBoolean("inAppCameraLocationPref", false); | ||||
|                 initiateCameraUpload(activity, resultLauncher); | ||||
|             }, | ||||
|             null | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Initiate gallery picker | ||||
|      */ | ||||
|     public void initiateGalleryPick(final Activity activity, ActivityResultLauncher<Intent> resultLauncher, final boolean allowMultipleUploads) { | ||||
|         initiateGalleryUpload(activity, resultLauncher, allowMultipleUploads); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Initiate gallery picker with permission | ||||
|      */ | ||||
|     public void initiateCustomGalleryPickWithPermission(final Activity activity, ActivityResultLauncher<Intent> resultLauncher) { | ||||
|         setPickerConfiguration(activity, true); | ||||
| 
 | ||||
|         PermissionUtils.checkPermissionsAndPerformAction(activity, | ||||
|             () -> FilePicker.openCustomSelector(activity, resultLauncher, 0), | ||||
|             R.string.storage_permission_title, | ||||
|             R.string.write_storage_permission_rationale, | ||||
|             PermissionUtils.getPERMISSIONS_STORAGE()); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * Open chooser for gallery uploads | ||||
|      */ | ||||
|     private void initiateGalleryUpload(final Activity activity, ActivityResultLauncher<Intent> resultLauncher, | ||||
|         final boolean allowMultipleUploads) { | ||||
|         setPickerConfiguration(activity, allowMultipleUploads); | ||||
|         FilePicker.openGallery(activity, resultLauncher, 0, isDocumentPhotoPickerPreferred()); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Sets configuration for file picker | ||||
|      */ | ||||
|     private void setPickerConfiguration(Activity activity, | ||||
|         boolean allowMultipleUploads) { | ||||
|         boolean copyToExternalStorage = defaultKvStore.getBoolean("useExternalStorage", true); | ||||
|         FilePicker.configuration(activity) | ||||
|             .setCopyTakenPhotosToPublicGalleryAppFolder(copyToExternalStorage) | ||||
|             .setAllowMultiplePickInGallery(allowMultipleUploads); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Initiate camera upload by opening camera | ||||
|      */ | ||||
|     private void initiateCameraUpload(Activity activity, ActivityResultLauncher<Intent> resultLauncher) { | ||||
|         setPickerConfiguration(activity, false); | ||||
|         if (defaultKvStore.getBoolean("inAppCameraLocationPref", false)) { | ||||
|             locationBeforeImageCapture = locationManager.getLastLocation(); | ||||
|         } | ||||
|         isInAppCameraUpload = true; | ||||
|         FilePicker.openCameraForImage(activity, resultLauncher, 0); | ||||
|     } | ||||
| 
 | ||||
|     private boolean isDocumentPhotoPickerPreferred(){ | ||||
|         return defaultKvStore.getBoolean( | ||||
|             "openDocumentPhotoPickerPref", true); | ||||
|     } | ||||
| 
 | ||||
|     public void onPictureReturnedFromGallery(ActivityResult result, Activity activity, FilePicker.Callbacks callbacks){ | ||||
| 
 | ||||
|         if(isDocumentPhotoPickerPreferred()){ | ||||
|             FilePicker.onPictureReturnedFromDocuments(result, activity, callbacks); | ||||
|         } else { | ||||
|             FilePicker.onPictureReturnedFromGallery(result, activity, callbacks); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public void onPictureReturnedFromCustomSelector(ActivityResult result, Activity activity, @NonNull FilePicker.Callbacks callbacks) { | ||||
|         FilePicker.onPictureReturnedFromCustomSelector(result, activity, callbacks); | ||||
|     } | ||||
| 
 | ||||
|     public void onPictureReturnedFromCamera(ActivityResult result, Activity activity, @NonNull FilePicker.Callbacks callbacks) { | ||||
|         FilePicker.onPictureReturnedFromCamera(result, activity, callbacks); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Attaches callback for file picker. | ||||
|      */ | ||||
|     public void handleActivityResultWithCallback(Activity activity, FilePicker.HandleActivityResult handleActivityResult) { | ||||
| 
 | ||||
|         handleActivityResult.onHandleActivityResult(new DefaultCallback() { | ||||
| 
 | ||||
|                 @Override | ||||
|                 public void onCanceled(final ImageSource source, final int type) { | ||||
|                     super.onCanceled(source, type); | ||||
|                     defaultKvStore.remove(PLACE_OBJECT); | ||||
|                 } | ||||
| 
 | ||||
|                 @Override | ||||
|                 public void onImagePickerError(Exception e, FilePicker.ImageSource source, | ||||
|                     int type) { | ||||
|                     ViewUtil.showShortToast(activity, R.string.error_occurred_in_picking_images); | ||||
|                 } | ||||
| 
 | ||||
|                 @Override | ||||
|                 public void onImagesPicked(@NonNull List<UploadableFile> imagesFiles, | ||||
|                     FilePicker.ImageSource source, int type) { | ||||
|                     Intent intent = handleImagesPicked(activity, imagesFiles); | ||||
|                     activity.startActivity(intent); | ||||
|                 } | ||||
|             }); | ||||
|     } | ||||
| 
 | ||||
|     public List<UploadableFile> handleExternalImagesPicked(Activity activity, | ||||
|         Intent data) { | ||||
|         return FilePicker.handleExternalImagesPicked(data, activity); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns intent to be passed to upload activity Attaches place object for nearby uploads and | ||||
|      * location before image capture if in-app camera is used | ||||
|      */ | ||||
|     private Intent handleImagesPicked(Context context, | ||||
|         List<UploadableFile> imagesFiles) { | ||||
|         Intent shareIntent = new Intent(context, UploadActivity.class); | ||||
|         shareIntent.setAction(ACTION_INTERNAL_UPLOADS); | ||||
|         shareIntent | ||||
|             .putParcelableArrayListExtra(UploadActivity.EXTRA_FILES, new ArrayList<>(imagesFiles)); | ||||
|         Place place = defaultKvStore.getJson(PLACE_OBJECT, Place.class); | ||||
| 
 | ||||
|         if (place != null) { | ||||
|             shareIntent.putExtra(PLACE_OBJECT, place); | ||||
|         } | ||||
| 
 | ||||
|         if (locationBeforeImageCapture != null) { | ||||
|             shareIntent.putExtra( | ||||
|                 UploadActivity.LOCATION_BEFORE_IMAGE_CAPTURE, | ||||
|                 locationBeforeImageCapture); | ||||
|         } | ||||
| 
 | ||||
|         shareIntent.putExtra( | ||||
|             UploadActivity.IN_APP_CAMERA_UPLOAD, | ||||
|             isInAppCameraUpload | ||||
|         ); | ||||
|         isInAppCameraUpload = false;    // reset the flag for next use | ||||
|         return shareIntent; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetches the contributions with the state "IN_PROGRESS", "QUEUED" and "PAUSED" and then it | ||||
|      * populates the `pendingContributionList`. | ||||
|      **/ | ||||
|     void getPendingContributions() { | ||||
|         final PagedList.Config pagedListConfig = | ||||
|             (new PagedList.Config.Builder()) | ||||
|                 .setPrefetchDistance(50) | ||||
|                 .setPageSize(10).build(); | ||||
|         Factory<Integer, Contribution> factory; | ||||
|         factory = repository.fetchContributionsWithStates( | ||||
|             Arrays.asList(Contribution.STATE_IN_PROGRESS, Contribution.STATE_QUEUED, | ||||
|                 Contribution.STATE_PAUSED)); | ||||
| 
 | ||||
|         LivePagedListBuilder livePagedListBuilder = new LivePagedListBuilder(factory, | ||||
|             pagedListConfig); | ||||
|         pendingContributionList = livePagedListBuilder.build(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetches the contributions with the state "FAILED" and populates the | ||||
|      * `failedContributionList`. | ||||
|      **/ | ||||
|     void getFailedContributions() { | ||||
|         final PagedList.Config pagedListConfig = | ||||
|             (new PagedList.Config.Builder()) | ||||
|                 .setPrefetchDistance(50) | ||||
|                 .setPageSize(10).build(); | ||||
|         Factory<Integer, Contribution> factory; | ||||
|         factory = repository.fetchContributionsWithStates( | ||||
|             Collections.singletonList(Contribution.STATE_FAILED)); | ||||
| 
 | ||||
|         LivePagedListBuilder livePagedListBuilder = new LivePagedListBuilder(factory, | ||||
|             pagedListConfig); | ||||
|         failedContributionList = livePagedListBuilder.build(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Temporarily disabled, see issue [https://github.com/commons-app/apps-android-commons/issues/5847] | ||||
|      * Fetches the contributions with the state "IN_PROGRESS", "QUEUED", "PAUSED" and "FAILED" and | ||||
|      * then it populates the `failedAndPendingContributionList`. | ||||
|      **/ | ||||
| //    void getFailedAndPendingContributions() { | ||||
| //        final PagedList.Config pagedListConfig = | ||||
| //            (new PagedList.Config.Builder()) | ||||
| //                .setPrefetchDistance(50) | ||||
| //                .setPageSize(10).build(); | ||||
| //        Factory<Integer, Contribution> factory; | ||||
| //        factory = repository.fetchContributionsWithStates( | ||||
| //            Arrays.asList(Contribution.STATE_IN_PROGRESS, Contribution.STATE_QUEUED, | ||||
| //                Contribution.STATE_PAUSED, Contribution.STATE_FAILED)); | ||||
| // | ||||
| //        LivePagedListBuilder livePagedListBuilder = new LivePagedListBuilder(factory, | ||||
| //            pagedListConfig); | ||||
| //        failedAndPendingContributionList = livePagedListBuilder.build(); | ||||
| //    } | ||||
| } | ||||
|  | @ -0,0 +1,474 @@ | |||
| package fr.free.nrw.commons.contributions | ||||
| 
 | ||||
| import android.Manifest.permission | ||||
| import android.app.Activity | ||||
| import android.content.Context | ||||
| import android.content.Intent | ||||
| import android.widget.Toast | ||||
| import androidx.activity.result.ActivityResult | ||||
| import androidx.activity.result.ActivityResultLauncher | ||||
| import androidx.lifecycle.LiveData | ||||
| import androidx.paging.LivePagedListBuilder | ||||
| import androidx.paging.PagedList | ||||
| import fr.free.nrw.commons.R | ||||
| import fr.free.nrw.commons.filepicker.DefaultCallback | ||||
| import fr.free.nrw.commons.filepicker.FilePicker | ||||
| import fr.free.nrw.commons.filepicker.FilePicker.HandleActivityResult | ||||
| import fr.free.nrw.commons.filepicker.FilePicker.configuration | ||||
| import fr.free.nrw.commons.filepicker.FilePicker.handleExternalImagesPicked | ||||
| import fr.free.nrw.commons.filepicker.FilePicker.onPictureReturnedFromDocuments | ||||
| import fr.free.nrw.commons.filepicker.FilePicker.openCameraForImage | ||||
| import fr.free.nrw.commons.filepicker.FilePicker.openCustomSelector | ||||
| import fr.free.nrw.commons.filepicker.FilePicker.openGallery | ||||
| import fr.free.nrw.commons.filepicker.UploadableFile | ||||
| import fr.free.nrw.commons.kvstore.JsonKvStore | ||||
| import fr.free.nrw.commons.location.LatLng | ||||
| import fr.free.nrw.commons.location.LocationPermissionsHelper | ||||
| import fr.free.nrw.commons.location.LocationPermissionsHelper.LocationPermissionCallback | ||||
| import fr.free.nrw.commons.location.LocationServiceManager | ||||
| import fr.free.nrw.commons.nearby.Place | ||||
| import fr.free.nrw.commons.upload.UploadActivity | ||||
| import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog | ||||
| import fr.free.nrw.commons.utils.PermissionUtils.PERMISSIONS_STORAGE | ||||
| import fr.free.nrw.commons.utils.PermissionUtils.checkPermissionsAndPerformAction | ||||
| import fr.free.nrw.commons.utils.ViewUtil.showLongToast | ||||
| import fr.free.nrw.commons.utils.ViewUtil.showShortToast | ||||
| import fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT | ||||
| import java.util.Arrays | ||||
| import javax.inject.Inject | ||||
| import javax.inject.Named | ||||
| import javax.inject.Singleton | ||||
| 
 | ||||
| @Singleton | ||||
| class ContributionController @Inject constructor(@param:Named("default_preferences") private val defaultKvStore: JsonKvStore) { | ||||
|     private var locationBeforeImageCapture: LatLng? = null | ||||
|     private var isInAppCameraUpload = false | ||||
|     @JvmField | ||||
|     var locationPermissionCallback: LocationPermissionCallback? = null | ||||
|     private var locationPermissionsHelper: LocationPermissionsHelper? = null | ||||
| 
 | ||||
|     // Temporarily disabled, see issue [https://github.com/commons-app/apps-android-commons/issues/5847] | ||||
|     // LiveData<PagedList<Contribution>> failedAndPendingContributionList; | ||||
|     @JvmField | ||||
|     var pendingContributionList: LiveData<PagedList<Contribution>>? = null | ||||
|     @JvmField | ||||
|     var failedContributionList: LiveData<PagedList<Contribution>>? = null | ||||
| 
 | ||||
|     @JvmField | ||||
|     @Inject | ||||
|     var locationManager: LocationServiceManager? = null | ||||
| 
 | ||||
|     @JvmField | ||||
|     @Inject | ||||
|     var repository: ContributionsRepository? = null | ||||
| 
 | ||||
|     /** | ||||
|      * Check for permissions and initiate camera click | ||||
|      */ | ||||
|     fun initiateCameraPick( | ||||
|         activity: Activity, | ||||
|         inAppCameraLocationPermissionLauncher: ActivityResultLauncher<Array<String>>, | ||||
|         resultLauncher: ActivityResultLauncher<Intent> | ||||
|     ) { | ||||
|         val useExtStorage = defaultKvStore.getBoolean("useExternalStorage", true) | ||||
|         if (!useExtStorage) { | ||||
|             initiateCameraUpload(activity, resultLauncher) | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
|         checkPermissionsAndPerformAction( | ||||
|             activity, | ||||
|             { | ||||
|                 if (defaultKvStore.getBoolean("inAppCameraFirstRun")) { | ||||
|                     defaultKvStore.putBoolean("inAppCameraFirstRun", false) | ||||
|                     askUserToAllowLocationAccess( | ||||
|                         activity, | ||||
|                         inAppCameraLocationPermissionLauncher, | ||||
|                         resultLauncher | ||||
|                     ) | ||||
|                 } else if (defaultKvStore.getBoolean("inAppCameraLocationPref")) { | ||||
|                     createDialogsAndHandleLocationPermissions( | ||||
|                         activity, | ||||
|                         inAppCameraLocationPermissionLauncher, resultLauncher | ||||
|                     ) | ||||
|                 } else { | ||||
|                     initiateCameraUpload(activity, resultLauncher) | ||||
|                 } | ||||
|             }, | ||||
|             R.string.storage_permission_title, | ||||
|             R.string.write_storage_permission_rationale, | ||||
|             *PERMISSIONS_STORAGE | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Asks users to provide location access | ||||
|      * | ||||
|      * @param activity | ||||
|      */ | ||||
|     private fun createDialogsAndHandleLocationPermissions( | ||||
|         activity: Activity, | ||||
|         inAppCameraLocationPermissionLauncher: ActivityResultLauncher<Array<String>>?, | ||||
|         resultLauncher: ActivityResultLauncher<Intent> | ||||
|     ) { | ||||
|         locationPermissionCallback = object : LocationPermissionCallback { | ||||
|             override fun onLocationPermissionDenied(toastMessage: String) { | ||||
|                 Toast.makeText( | ||||
|                     activity, | ||||
|                     toastMessage, | ||||
|                     Toast.LENGTH_LONG | ||||
|                 ).show() | ||||
|                 initiateCameraUpload(activity, resultLauncher) | ||||
|             } | ||||
| 
 | ||||
|             override fun onLocationPermissionGranted() { | ||||
|                 if (!locationPermissionsHelper!!.isLocationAccessToAppsTurnedOn()) { | ||||
|                     showLocationOffDialog( | ||||
|                         activity, R.string.in_app_camera_needs_location, | ||||
|                         R.string.in_app_camera_location_unavailable, resultLauncher | ||||
|                     ) | ||||
|                 } else { | ||||
|                     initiateCameraUpload(activity, resultLauncher) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         locationPermissionsHelper = LocationPermissionsHelper( | ||||
|             activity, locationManager!!, locationPermissionCallback | ||||
|         ) | ||||
|         inAppCameraLocationPermissionLauncher?.launch( | ||||
|             arrayOf(permission.ACCESS_FINE_LOCATION) | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Shows a dialog alerting the user about location services being off and asking them to turn it | ||||
|      * on | ||||
|      * TODO: Add a seperate callback in LocationPermissionsHelper for this. | ||||
|      * Ref: https://github.com/commons-app/apps-android-commons/pull/5494/files#r1510553114 | ||||
|      * | ||||
|      * @param activity           Activity reference | ||||
|      * @param dialogTextResource Resource id of text to be shown in dialog | ||||
|      * @param toastTextResource  Resource id of text to be shown in toast | ||||
|      * @param resultLauncher | ||||
|      */ | ||||
|     private fun showLocationOffDialog( | ||||
|         activity: Activity, dialogTextResource: Int, | ||||
|         toastTextResource: Int, resultLauncher: ActivityResultLauncher<Intent> | ||||
|     ) { | ||||
|         showAlertDialog(activity, | ||||
|             activity.getString(R.string.ask_to_turn_location_on), | ||||
|             activity.getString(dialogTextResource), | ||||
|             activity.getString(R.string.title_app_shortcut_setting), | ||||
|             activity.getString(R.string.cancel), | ||||
|             { locationPermissionsHelper!!.openLocationSettings(activity) }, | ||||
|             { | ||||
|                 Toast.makeText( | ||||
|                     activity, activity.getString(toastTextResource), | ||||
|                     Toast.LENGTH_LONG | ||||
|                 ).show() | ||||
|                 initiateCameraUpload(activity, resultLauncher) | ||||
|             } | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     fun handleShowRationaleFlowCameraLocation( | ||||
|         activity: Activity, | ||||
|         inAppCameraLocationPermissionLauncher: ActivityResultLauncher<Array<String>>?, | ||||
|         resultLauncher: ActivityResultLauncher<Intent> | ||||
|     ) { | ||||
|         showAlertDialog( | ||||
|             activity, activity.getString(R.string.location_permission_title), | ||||
|             activity.getString(R.string.in_app_camera_location_permission_rationale), | ||||
|             activity.getString(android.R.string.ok), | ||||
|             activity.getString(android.R.string.cancel), | ||||
|             { | ||||
|                 createDialogsAndHandleLocationPermissions( | ||||
|                     activity, | ||||
|                     inAppCameraLocationPermissionLauncher, resultLauncher | ||||
|                 ) | ||||
|             }, | ||||
|             { | ||||
|                 locationPermissionCallback!!.onLocationPermissionDenied( | ||||
|                     activity.getString(R.string.in_app_camera_location_permission_denied) | ||||
|                 ) | ||||
|             }, | ||||
|             null | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Suggest user to attach location information with pictures. If the user selects "Yes", then: | ||||
|      * | ||||
|      * | ||||
|      * Location is taken from the EXIF if the default camera application does not redact location | ||||
|      * tags. | ||||
|      * | ||||
|      * | ||||
|      * Otherwise, if the EXIF metadata does not have location information, then location captured by | ||||
|      * the app is used | ||||
|      * | ||||
|      * @param activity | ||||
|      */ | ||||
|     private fun askUserToAllowLocationAccess( | ||||
|         activity: Activity, | ||||
|         inAppCameraLocationPermissionLauncher: ActivityResultLauncher<Array<String>>, | ||||
|         resultLauncher: ActivityResultLauncher<Intent> | ||||
|     ) { | ||||
|         showAlertDialog( | ||||
|             activity, | ||||
|             activity.getString(R.string.in_app_camera_location_permission_title), | ||||
|             activity.getString(R.string.in_app_camera_location_access_explanation), | ||||
|             activity.getString(R.string.option_allow), | ||||
|             activity.getString(R.string.option_dismiss), | ||||
|             { | ||||
|                 defaultKvStore.putBoolean("inAppCameraLocationPref", true) | ||||
|                 createDialogsAndHandleLocationPermissions( | ||||
|                     activity, | ||||
|                     inAppCameraLocationPermissionLauncher, resultLauncher | ||||
|                 ) | ||||
|             }, | ||||
|             { | ||||
|                 showLongToast(activity, R.string.in_app_camera_location_permission_denied) | ||||
|                 defaultKvStore.putBoolean("inAppCameraLocationPref", false) | ||||
|                 initiateCameraUpload(activity, resultLauncher) | ||||
|             }, | ||||
|             null | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Initiate gallery picker | ||||
|      */ | ||||
|     fun initiateGalleryPick( | ||||
|         activity: Activity, | ||||
|         resultLauncher: ActivityResultLauncher<Intent>, | ||||
|         allowMultipleUploads: Boolean | ||||
|     ) { | ||||
|         initiateGalleryUpload(activity, resultLauncher, allowMultipleUploads) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Initiate gallery picker with permission | ||||
|      */ | ||||
|     fun initiateCustomGalleryPickWithPermission( | ||||
|         activity: Activity, | ||||
|         resultLauncher: ActivityResultLauncher<Intent> | ||||
|     ) { | ||||
|         setPickerConfiguration(activity, true) | ||||
| 
 | ||||
|         checkPermissionsAndPerformAction( | ||||
|             activity, | ||||
|             { openCustomSelector(activity, resultLauncher, 0) }, | ||||
|             R.string.storage_permission_title, | ||||
|             R.string.write_storage_permission_rationale, | ||||
|             *PERMISSIONS_STORAGE | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * Open chooser for gallery uploads | ||||
|      */ | ||||
|     private fun initiateGalleryUpload( | ||||
|         activity: Activity, resultLauncher: ActivityResultLauncher<Intent>, | ||||
|         allowMultipleUploads: Boolean | ||||
|     ) { | ||||
|         setPickerConfiguration(activity, allowMultipleUploads) | ||||
|         openGallery(activity, resultLauncher, 0, isDocumentPhotoPickerPreferred) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Sets configuration for file picker | ||||
|      */ | ||||
|     private fun setPickerConfiguration( | ||||
|         activity: Activity, | ||||
|         allowMultipleUploads: Boolean | ||||
|     ) { | ||||
|         val copyToExternalStorage = defaultKvStore.getBoolean("useExternalStorage", true) | ||||
|         configuration(activity) | ||||
|             .setCopyTakenPhotosToPublicGalleryAppFolder(copyToExternalStorage) | ||||
|             .setAllowMultiplePickInGallery(allowMultipleUploads) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Initiate camera upload by opening camera | ||||
|      */ | ||||
|     private fun initiateCameraUpload( | ||||
|         activity: Activity, | ||||
|         resultLauncher: ActivityResultLauncher<Intent> | ||||
|     ) { | ||||
|         setPickerConfiguration(activity, false) | ||||
|         if (defaultKvStore.getBoolean("inAppCameraLocationPref", false)) { | ||||
|             locationBeforeImageCapture = locationManager!!.getLastLocation() | ||||
|         } | ||||
|         isInAppCameraUpload = true | ||||
|         openCameraForImage(activity, resultLauncher, 0) | ||||
|     } | ||||
| 
 | ||||
|     private val isDocumentPhotoPickerPreferred: Boolean | ||||
|         get() = defaultKvStore.getBoolean( | ||||
|             "openDocumentPhotoPickerPref", true | ||||
|         ) | ||||
| 
 | ||||
|     fun onPictureReturnedFromGallery( | ||||
|         result: ActivityResult, | ||||
|         activity: Activity, | ||||
|         callbacks: FilePicker.Callbacks | ||||
|     ) { | ||||
|         if (isDocumentPhotoPickerPreferred) { | ||||
|             onPictureReturnedFromDocuments(result, activity, callbacks) | ||||
|         } else { | ||||
|             FilePicker.onPictureReturnedFromGallery(result, activity, callbacks) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fun onPictureReturnedFromCustomSelector( | ||||
|         result: ActivityResult, | ||||
|         activity: Activity, | ||||
|         callbacks: FilePicker.Callbacks | ||||
|     ) { | ||||
|         FilePicker.onPictureReturnedFromCustomSelector(result, activity, callbacks) | ||||
|     } | ||||
| 
 | ||||
|     fun onPictureReturnedFromCamera( | ||||
|         result: ActivityResult, | ||||
|         activity: Activity, | ||||
|         callbacks: FilePicker.Callbacks | ||||
|     ) { | ||||
|         FilePicker.onPictureReturnedFromCamera(result, activity, callbacks) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Attaches callback for file picker. | ||||
|      */ | ||||
|     fun handleActivityResultWithCallback( | ||||
|         activity: Activity, | ||||
|         handleActivityResult: HandleActivityResult | ||||
|     ) { | ||||
|         handleActivityResult.onHandleActivityResult(object : DefaultCallback() { | ||||
|             override fun onCanceled(source: FilePicker.ImageSource, type: Int) { | ||||
|                 super.onCanceled(source, type) | ||||
|                 defaultKvStore.remove(PLACE_OBJECT) | ||||
|             } | ||||
| 
 | ||||
|             override fun onImagePickerError( | ||||
|                 e: Exception, source: FilePicker.ImageSource, | ||||
|                 type: Int | ||||
|             ) { | ||||
|                 showShortToast(activity, R.string.error_occurred_in_picking_images) | ||||
|             } | ||||
| 
 | ||||
|             override fun onImagesPicked( | ||||
|                 imagesFiles: List<UploadableFile>, | ||||
|                 source: FilePicker.ImageSource, type: Int | ||||
|             ) { | ||||
|                 val intent = handleImagesPicked(activity, imagesFiles) | ||||
|                 activity.startActivity(intent) | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     fun handleExternalImagesPicked( | ||||
|         activity: Activity, | ||||
|         data: Intent? | ||||
|     ): List<UploadableFile> { | ||||
|         return handleExternalImagesPicked(data, activity) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns intent to be passed to upload activity Attaches place object for nearby uploads and | ||||
|      * location before image capture if in-app camera is used | ||||
|      */ | ||||
|     private fun handleImagesPicked( | ||||
|         context: Context, | ||||
|         imagesFiles: List<UploadableFile> | ||||
|     ): Intent { | ||||
|         val shareIntent = Intent(context, UploadActivity::class.java) | ||||
|         shareIntent.setAction(ACTION_INTERNAL_UPLOADS) | ||||
|         shareIntent | ||||
|             .putParcelableArrayListExtra(UploadActivity.EXTRA_FILES, ArrayList(imagesFiles)) | ||||
|         val place = defaultKvStore.getJson<Place>(PLACE_OBJECT, Place::class.java) | ||||
| 
 | ||||
|         if (place != null) { | ||||
|             shareIntent.putExtra(PLACE_OBJECT, place) | ||||
|         } | ||||
| 
 | ||||
|         if (locationBeforeImageCapture != null) { | ||||
|             shareIntent.putExtra( | ||||
|                 UploadActivity.LOCATION_BEFORE_IMAGE_CAPTURE, | ||||
|                 locationBeforeImageCapture | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         shareIntent.putExtra( | ||||
|             UploadActivity.IN_APP_CAMERA_UPLOAD, | ||||
|             isInAppCameraUpload | ||||
|         ) | ||||
|         isInAppCameraUpload = false // reset the flag for next use | ||||
|         return shareIntent | ||||
|     } | ||||
| 
 | ||||
|     val pendingContributions: Unit | ||||
|         /** | ||||
|          * Fetches the contributions with the state "IN_PROGRESS", "QUEUED" and "PAUSED" and then it | ||||
|          * populates the `pendingContributionList`. | ||||
|          */ | ||||
|         get() { | ||||
|             val pagedListConfig = | ||||
|                 (PagedList.Config.Builder()) | ||||
|                     .setPrefetchDistance(50) | ||||
|                     .setPageSize(10).build() | ||||
|             val factory = repository!!.fetchContributionsWithStates( | ||||
|                 Arrays.asList( | ||||
|                     Contribution.STATE_IN_PROGRESS, Contribution.STATE_QUEUED, | ||||
|                     Contribution.STATE_PAUSED | ||||
|                 ) | ||||
|             ) | ||||
| 
 | ||||
|             val livePagedListBuilder = LivePagedListBuilder(factory, pagedListConfig) | ||||
|             pendingContributionList = livePagedListBuilder.build() | ||||
|         } | ||||
| 
 | ||||
|     val failedContributions: Unit | ||||
|         /** | ||||
|          * Fetches the contributions with the state "FAILED" and populates the | ||||
|          * `failedContributionList`. | ||||
|          */ | ||||
|         get() { | ||||
|             val pagedListConfig = | ||||
|                 (PagedList.Config.Builder()) | ||||
|                     .setPrefetchDistance(50) | ||||
|                     .setPageSize(10).build() | ||||
|             val factory = repository!!.fetchContributionsWithStates( | ||||
|                 listOf(Contribution.STATE_FAILED) | ||||
|             ) | ||||
| 
 | ||||
|             val livePagedListBuilder = LivePagedListBuilder(factory, pagedListConfig) | ||||
|             failedContributionList = livePagedListBuilder.build() | ||||
|         } | ||||
| 
 | ||||
|     /** | ||||
|      * Temporarily disabled, see issue [https://github.com/commons-app/apps-android-commons/issues/5847] | ||||
|      * Fetches the contributions with the state "IN_PROGRESS", "QUEUED", "PAUSED" and "FAILED" and | ||||
|      * then it populates the `failedAndPendingContributionList`. | ||||
|      */ | ||||
|     //    void getFailedAndPendingContributions() { | ||||
|     //        final PagedList.Config pagedListConfig = | ||||
|     //            (new PagedList.Config.Builder()) | ||||
|     //                .setPrefetchDistance(50) | ||||
|     //                .setPageSize(10).build(); | ||||
|     //        Factory<Integer, Contribution> factory; | ||||
|     //        factory = repository.fetchContributionsWithStates( | ||||
|     //            Arrays.asList(Contribution.STATE_IN_PROGRESS, Contribution.STATE_QUEUED, | ||||
|     //                Contribution.STATE_PAUSED, Contribution.STATE_FAILED)); | ||||
|     // | ||||
|     //        LivePagedListBuilder livePagedListBuilder = new LivePagedListBuilder(factory, | ||||
|     //            pagedListConfig); | ||||
|     //        failedAndPendingContributionList = livePagedListBuilder.build(); | ||||
|     //    } | ||||
| 
 | ||||
|     companion object { | ||||
|         const val ACTION_INTERNAL_UPLOADS: String = "internalImageUploads" | ||||
|     } | ||||
| } | ||||
|  | @ -1,145 +0,0 @@ | |||
| package fr.free.nrw.commons.contributions; | ||||
| 
 | ||||
| import android.database.sqlite.SQLiteException; | ||||
| import androidx.paging.DataSource; | ||||
| import androidx.room.Dao; | ||||
| import androidx.room.Delete; | ||||
| import androidx.room.Insert; | ||||
| import androidx.room.OnConflictStrategy; | ||||
| import androidx.room.Query; | ||||
| import androidx.room.Transaction; | ||||
| import androidx.room.Update; | ||||
| import io.reactivex.Completable; | ||||
| import io.reactivex.Single; | ||||
| import java.util.Calendar; | ||||
| import java.util.List; | ||||
| import timber.log.Timber; | ||||
| 
 | ||||
| @Dao | ||||
| public abstract class ContributionDao { | ||||
| 
 | ||||
|     @Query("SELECT * FROM contribution order by media_dateUploaded DESC") | ||||
|     abstract DataSource.Factory<Integer, Contribution> fetchContributions(); | ||||
| 
 | ||||
|     @Insert(onConflict = OnConflictStrategy.REPLACE) | ||||
|     public abstract void saveSynchronous(Contribution contribution); | ||||
| 
 | ||||
|     public Completable save(final Contribution contribution) { | ||||
|         return Completable | ||||
|             .fromAction(() -> { | ||||
|                 contribution.setDateModified(Calendar.getInstance().getTime()); | ||||
|                 if (contribution.getDateUploadStarted() == null) { | ||||
|                     contribution.setDateUploadStarted(Calendar.getInstance().getTime()); | ||||
|                 } | ||||
|                 saveSynchronous(contribution); | ||||
|             }); | ||||
|     } | ||||
| 
 | ||||
|     @Transaction | ||||
|     public void deleteAndSaveContribution(final Contribution oldContribution, | ||||
|         final Contribution newContribution) { | ||||
|         deleteSynchronous(oldContribution); | ||||
|         saveSynchronous(newContribution); | ||||
|     } | ||||
| 
 | ||||
|     @Insert(onConflict = OnConflictStrategy.REPLACE) | ||||
|     public abstract Single<List<Long>> save(List<Contribution> contribution); | ||||
| 
 | ||||
|     @Delete | ||||
|     public abstract void deleteSynchronous(Contribution contribution); | ||||
| 
 | ||||
|     /** | ||||
|      * Deletes contributions with specific states from the database. | ||||
|      * | ||||
|      * @param states The states of the contributions to delete. | ||||
|      * @throws SQLiteException If an SQLite error occurs. | ||||
|      */ | ||||
|     @Query("DELETE FROM contribution WHERE state IN (:states)") | ||||
|     public abstract void deleteContributionsWithStatesSynchronous(List<Integer> states) | ||||
|         throws SQLiteException; | ||||
| 
 | ||||
|     public Completable delete(final Contribution contribution) { | ||||
|         return Completable | ||||
|             .fromAction(() -> deleteSynchronous(contribution)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Deletes contributions with specific states from the database. | ||||
|      * | ||||
|      * @param states The states of the contributions to delete. | ||||
|      * @return A Completable indicating the result of the operation. | ||||
|      */ | ||||
|     public Completable deleteContributionsWithStates(List<Integer> states) { | ||||
|         return Completable | ||||
|             .fromAction(() -> deleteContributionsWithStatesSynchronous(states)); | ||||
|     } | ||||
| 
 | ||||
|     @Query("SELECT * from contribution WHERE media_filename=:fileName") | ||||
|     public abstract List<Contribution> getContributionWithTitle(String fileName); | ||||
| 
 | ||||
|     @Query("SELECT * from contribution WHERE pageId=:pageId") | ||||
|     public abstract Contribution getContribution(String pageId); | ||||
| 
 | ||||
|     @Query("SELECT * from contribution WHERE state IN (:states) order by media_dateUploaded DESC") | ||||
|     public abstract Single<List<Contribution>> getContribution(List<Integer> states); | ||||
| 
 | ||||
|     /** | ||||
|      * Gets contributions with specific states in descending order by the date they were uploaded. | ||||
|      * | ||||
|      * @param states The states of the contributions to fetch. | ||||
|      * @return A DataSource factory for paginated contributions with the specified states. | ||||
|      */ | ||||
|     @Query("SELECT * from contribution WHERE state IN (:states) order by media_dateUploaded DESC") | ||||
|     public abstract DataSource.Factory<Integer, Contribution> getContributions( | ||||
|         List<Integer> states); | ||||
| 
 | ||||
|     /** | ||||
|      * Gets contributions with specific states in ascending order by the date the upload started. | ||||
|      * | ||||
|      * @param states The states of the contributions to fetch. | ||||
|      * @return A DataSource factory for paginated contributions with the specified states. | ||||
|      */ | ||||
|     @Query("SELECT * from contribution WHERE state IN (:states) order by dateUploadStarted ASC") | ||||
|     public abstract DataSource.Factory<Integer, Contribution> getContributionsSortedByDateUploadStarted( | ||||
|         List<Integer> states); | ||||
| 
 | ||||
|     @Query("SELECT COUNT(*) from contribution WHERE state in (:toUpdateStates)") | ||||
|     public abstract Single<Integer> getPendingUploads(int[] toUpdateStates); | ||||
| 
 | ||||
|     @Query("Delete FROM contribution") | ||||
|     public abstract void deleteAll() throws SQLiteException; | ||||
| 
 | ||||
|     @Update | ||||
|     public abstract void updateSynchronous(Contribution contribution); | ||||
| 
 | ||||
|     /** | ||||
|      * Updates the state of contributions with specific states. | ||||
|      * | ||||
|      * @param states   The current states of the contributions to update. | ||||
|      * @param newState The new state to set. | ||||
|      */ | ||||
|     @Query("UPDATE contribution SET state = :newState WHERE state IN (:states)") | ||||
|     public abstract void updateContributionsState(List<Integer> states, int newState); | ||||
| 
 | ||||
|     public Completable update(final Contribution contribution) { | ||||
|         return Completable | ||||
|             .fromAction(() -> { | ||||
|                 contribution.setDateModified(Calendar.getInstance().getTime()); | ||||
|                 updateSynchronous(contribution); | ||||
|             }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Updates the state of contributions with specific states asynchronously. | ||||
|      * | ||||
|      * @param states   The current states of the contributions to update. | ||||
|      * @param newState The new state to set. | ||||
|      * @return A Completable indicating the result of the operation. | ||||
|      */ | ||||
|     public Completable updateContributionsWithStates(List<Integer> states, int newState) { | ||||
|         return Completable | ||||
|             .fromAction(() -> { | ||||
|                 updateContributionsState(states, newState); | ||||
|             }); | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,148 @@ | |||
| package fr.free.nrw.commons.contributions | ||||
| 
 | ||||
| import android.database.sqlite.SQLiteException | ||||
| import androidx.paging.DataSource | ||||
| import androidx.room.Dao | ||||
| import androidx.room.Delete | ||||
| import androidx.room.Insert | ||||
| import androidx.room.OnConflictStrategy | ||||
| import androidx.room.Query | ||||
| import androidx.room.Transaction | ||||
| import androidx.room.Update | ||||
| import io.reactivex.Completable | ||||
| import io.reactivex.Single | ||||
| import java.util.Calendar | ||||
| 
 | ||||
| @Dao | ||||
| abstract class ContributionDao { | ||||
|     @Query("SELECT * FROM contribution order by media_dateUploaded DESC") | ||||
|     abstract fun fetchContributions(): DataSource.Factory<Int, Contribution> | ||||
| 
 | ||||
|     @Insert(onConflict = OnConflictStrategy.REPLACE) | ||||
|     abstract fun saveSynchronous(contribution: Contribution) | ||||
| 
 | ||||
|     fun save(contribution: Contribution): Completable { | ||||
|         return Completable | ||||
|             .fromAction { | ||||
|                 contribution.dateModified = Calendar.getInstance().time | ||||
|                 if (contribution.dateUploadStarted == null) { | ||||
|                     contribution.dateUploadStarted = Calendar.getInstance().time | ||||
|                 } | ||||
|                 saveSynchronous(contribution) | ||||
|             } | ||||
|     } | ||||
| 
 | ||||
|     @Transaction | ||||
|     open fun deleteAndSaveContribution( | ||||
|         oldContribution: Contribution, | ||||
|         newContribution: Contribution | ||||
|     ) { | ||||
|         deleteSynchronous(oldContribution) | ||||
|         saveSynchronous(newContribution) | ||||
|     } | ||||
| 
 | ||||
|     @Insert(onConflict = OnConflictStrategy.REPLACE) | ||||
|     abstract fun save(contribution: List<Contribution>): Single<List<Long>> | ||||
| 
 | ||||
|     @Delete | ||||
|     abstract fun deleteSynchronous(contribution: Contribution) | ||||
| 
 | ||||
|     /** | ||||
|      * Deletes contributions with specific states from the database. | ||||
|      * | ||||
|      * @param states The states of the contributions to delete. | ||||
|      * @throws SQLiteException If an SQLite error occurs. | ||||
|      */ | ||||
|     @Query("DELETE FROM contribution WHERE state IN (:states)") | ||||
|     @Throws(SQLiteException::class) | ||||
|     abstract fun deleteContributionsWithStatesSynchronous(states: List<Int>) | ||||
| 
 | ||||
|     fun delete(contribution: Contribution): Completable { | ||||
|         return Completable | ||||
|             .fromAction { deleteSynchronous(contribution) } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Deletes contributions with specific states from the database. | ||||
|      * | ||||
|      * @param states The states of the contributions to delete. | ||||
|      * @return A Completable indicating the result of the operation. | ||||
|      */ | ||||
|     fun deleteContributionsWithStates(states: List<Int>): Completable { | ||||
|         return Completable | ||||
|             .fromAction { deleteContributionsWithStatesSynchronous(states) } | ||||
|     } | ||||
| 
 | ||||
|     @Query("SELECT * from contribution WHERE media_filename=:fileName") | ||||
|     abstract fun getContributionWithTitle(fileName: String): List<Contribution> | ||||
| 
 | ||||
|     @Query("SELECT * from contribution WHERE pageId=:pageId") | ||||
|     abstract fun getContribution(pageId: String): Contribution | ||||
| 
 | ||||
|     @Query("SELECT * from contribution WHERE state IN (:states) order by media_dateUploaded DESC") | ||||
|     abstract fun getContribution(states: List<Int>): Single<List<Contribution>> | ||||
| 
 | ||||
|     /** | ||||
|      * Gets contributions with specific states in descending order by the date they were uploaded. | ||||
|      * | ||||
|      * @param states The states of the contributions to fetch. | ||||
|      * @return A DataSource factory for paginated contributions with the specified states. | ||||
|      */ | ||||
|     @Query("SELECT * from contribution WHERE state IN (:states) order by media_dateUploaded DESC") | ||||
|     abstract fun getContributions( | ||||
|         states: List<Int> | ||||
|     ): DataSource.Factory<Int, Contribution> | ||||
| 
 | ||||
|     /** | ||||
|      * Gets contributions with specific states in ascending order by the date the upload started. | ||||
|      * | ||||
|      * @param states The states of the contributions to fetch. | ||||
|      * @return A DataSource factory for paginated contributions with the specified states. | ||||
|      */ | ||||
|     @Query("SELECT * from contribution WHERE state IN (:states) order by dateUploadStarted ASC") | ||||
|     abstract fun getContributionsSortedByDateUploadStarted( | ||||
|         states: List<Int> | ||||
|     ): DataSource.Factory<Int, Contribution> | ||||
| 
 | ||||
|     @Query("SELECT COUNT(*) from contribution WHERE state in (:toUpdateStates)") | ||||
|     abstract fun getPendingUploads(toUpdateStates: IntArray): Single<Int> | ||||
| 
 | ||||
|     @Query("Delete FROM contribution") | ||||
|     @Throws(SQLiteException::class) | ||||
|     abstract fun deleteAll() | ||||
| 
 | ||||
|     @Update | ||||
|     abstract fun updateSynchronous(contribution: Contribution) | ||||
| 
 | ||||
|     /** | ||||
|      * Updates the state of contributions with specific states. | ||||
|      * | ||||
|      * @param states   The current states of the contributions to update. | ||||
|      * @param newState The new state to set. | ||||
|      */ | ||||
|     @Query("UPDATE contribution SET state = :newState WHERE state IN (:states)") | ||||
|     abstract fun updateContributionsState(states: List<Int>, newState: Int) | ||||
| 
 | ||||
|     fun update(contribution: Contribution): Completable { | ||||
|         return Completable.fromAction { | ||||
|             contribution.dateModified = Calendar.getInstance().time | ||||
|             updateSynchronous(contribution) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * Updates the state of contributions with specific states asynchronously. | ||||
|      * | ||||
|      * @param states   The current states of the contributions to update. | ||||
|      * @param newState The new state to set. | ||||
|      * @return A Completable indicating the result of the operation. | ||||
|      */ | ||||
|     fun updateContributionsWithStates(states: List<Int>, newState: Int): Completable { | ||||
|         return Completable | ||||
|             .fromAction { | ||||
|                 updateContributionsState(states, newState) | ||||
|             } | ||||
|     } | ||||
| } | ||||
|  | @ -1,171 +0,0 @@ | |||
| package fr.free.nrw.commons.contributions; | ||||
| 
 | ||||
| import android.net.Uri; | ||||
| import android.text.TextUtils; | ||||
| import android.view.View; | ||||
| import android.webkit.URLUtil; | ||||
| import android.widget.ImageButton; | ||||
| import android.widget.ProgressBar; | ||||
| import android.widget.RelativeLayout; | ||||
| import android.widget.TextView; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.appcompat.app.AlertDialog; | ||||
| import androidx.appcompat.app.AlertDialog.Builder; | ||||
| import androidx.recyclerview.widget.RecyclerView; | ||||
| import com.facebook.drawee.view.SimpleDraweeView; | ||||
| import com.facebook.imagepipeline.request.ImageRequest; | ||||
| import com.facebook.imagepipeline.request.ImageRequestBuilder; | ||||
| import fr.free.nrw.commons.R; | ||||
| import fr.free.nrw.commons.contributions.ContributionsListAdapter.Callback; | ||||
| import fr.free.nrw.commons.databinding.LayoutContributionBinding; | ||||
| import fr.free.nrw.commons.media.MediaClient; | ||||
| import io.reactivex.android.schedulers.AndroidSchedulers; | ||||
| import io.reactivex.disposables.CompositeDisposable; | ||||
| import io.reactivex.schedulers.Schedulers; | ||||
| import java.io.File; | ||||
| 
 | ||||
| public class ContributionViewHolder extends RecyclerView.ViewHolder { | ||||
| 
 | ||||
|     private final Callback callback; | ||||
| 
 | ||||
|     LayoutContributionBinding binding; | ||||
| 
 | ||||
|     private int position; | ||||
|     private Contribution contribution; | ||||
|     private final CompositeDisposable compositeDisposable = new CompositeDisposable(); | ||||
|     private final MediaClient mediaClient; | ||||
|     private boolean isWikipediaButtonDisplayed; | ||||
|     private AlertDialog pausingPopUp; | ||||
|     private View parent; | ||||
|     private ImageRequest imageRequest; | ||||
| 
 | ||||
|     ContributionViewHolder(final View parent, final Callback callback, | ||||
|         final MediaClient mediaClient) { | ||||
|         super(parent); | ||||
|         this.parent = parent; | ||||
|         this.mediaClient = mediaClient; | ||||
|         this.callback = callback; | ||||
| 
 | ||||
|         binding = LayoutContributionBinding.bind(parent); | ||||
| 
 | ||||
|         binding.contributionImage.setOnClickListener(v -> imageClicked()); | ||||
|         binding.wikipediaButton.setOnClickListener(v -> wikipediaButtonClicked()); | ||||
| 
 | ||||
|         /* Set a dialog indicating that the upload is being paused. This is needed because pausing | ||||
|         an upload might take a dozen seconds. */ | ||||
|         AlertDialog.Builder builder = new Builder(parent.getContext()); | ||||
|         builder.setCancelable(false); | ||||
|         builder.setView(R.layout.progress_dialog); | ||||
|         pausingPopUp = builder.create(); | ||||
|     } | ||||
| 
 | ||||
|     public void init(final int position, final Contribution contribution) { | ||||
| 
 | ||||
|         //handling crashes when the contribution is null. | ||||
|         if (null == contribution) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this.contribution = contribution; | ||||
|         this.position = position; | ||||
|         binding.contributionTitle.setText(contribution.getMedia().getMostRelevantCaption()); | ||||
|         binding.authorView.setText(contribution.getMedia().getAuthor()); | ||||
| 
 | ||||
|         //Removes flicker of loading image. | ||||
|         binding.contributionImage.getHierarchy().setFadeDuration(0); | ||||
| 
 | ||||
|         binding.contributionImage.getHierarchy().setPlaceholderImage(R.drawable.image_placeholder); | ||||
|         binding.contributionImage.getHierarchy().setFailureImage(R.drawable.image_placeholder); | ||||
| 
 | ||||
|         final String imageSource = chooseImageSource(contribution.getMedia().getThumbUrl(), | ||||
|             contribution.getLocalUri()); | ||||
|         if (!TextUtils.isEmpty(imageSource)) { | ||||
|             if (URLUtil.isHttpsUrl(imageSource)) { | ||||
|                 imageRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(imageSource)) | ||||
|                     .setProgressiveRenderingEnabled(true) | ||||
|                     .build(); | ||||
|             } else if (URLUtil.isFileUrl(imageSource)) { | ||||
|                 imageRequest = ImageRequest.fromUri(Uri.parse(imageSource)); | ||||
|             } else if (imageSource != null) { | ||||
|                 final File file = new File(imageSource); | ||||
|                 imageRequest = ImageRequest.fromFile(file); | ||||
|             } | ||||
| 
 | ||||
|             if (imageRequest != null) { | ||||
|                 binding.contributionImage.setImageRequest(imageRequest); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         binding.contributionSequenceNumber.setText(String.valueOf(position + 1)); | ||||
|         binding.contributionSequenceNumber.setVisibility(View.VISIBLE); | ||||
|         binding.wikipediaButton.setVisibility(View.GONE); | ||||
|         binding.contributionState.setVisibility(View.GONE); | ||||
|         binding.contributionProgress.setVisibility(View.GONE); | ||||
|         binding.imageOptions.setVisibility(View.GONE); | ||||
|         binding.contributionState.setText(""); | ||||
|         checkIfMediaExistsOnWikipediaPage(contribution); | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Checks if a media exists on the corresponding Wikipedia article Currently the check is made | ||||
|      * for the device's current language Wikipedia | ||||
|      * | ||||
|      * @param contribution | ||||
|      */ | ||||
|     private void checkIfMediaExistsOnWikipediaPage(final Contribution contribution) { | ||||
|         if (contribution.getWikidataPlace() == null | ||||
|             || contribution.getWikidataPlace().getWikipediaArticle() == null) { | ||||
|             return; | ||||
|         } | ||||
|         final String wikipediaArticle = contribution.getWikidataPlace().getWikipediaPageTitle(); | ||||
|         compositeDisposable.add(mediaClient.doesPageContainMedia(wikipediaArticle) | ||||
|             .subscribeOn(Schedulers.io()) | ||||
|             .observeOn(AndroidSchedulers.mainThread()) | ||||
|             .subscribe(mediaExists -> { | ||||
|                 displayWikipediaButton(mediaExists); | ||||
|             })); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Handle action buttons visibility if the corresponding wikipedia page doesn't contain any | ||||
|      * media. This method needs to control the state of just the scenario where media does not | ||||
|      * exists as other scenarios are already handled in the init method. | ||||
|      * | ||||
|      * @param mediaExists | ||||
|      */ | ||||
|     private void displayWikipediaButton(Boolean mediaExists) { | ||||
|         if (!mediaExists) { | ||||
|             binding.wikipediaButton.setVisibility(View.VISIBLE); | ||||
|             isWikipediaButtonDisplayed = true; | ||||
|             binding.imageOptions.setVisibility(View.VISIBLE); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns the image source for the image view, first preference is given to thumbUrl if that is | ||||
|      * null, moves to local uri and if both are null return null | ||||
|      * | ||||
|      * @param thumbUrl | ||||
|      * @param localUri | ||||
|      * @return | ||||
|      */ | ||||
|     @Nullable | ||||
|     private String chooseImageSource(final String thumbUrl, final Uri localUri) { | ||||
|         return !TextUtils.isEmpty(thumbUrl) ? thumbUrl : | ||||
|             localUri != null ? localUri.toString() : | ||||
|                 null; | ||||
|     } | ||||
| 
 | ||||
|     public void imageClicked() { | ||||
|         callback.openMediaDetail(position, isWikipediaButtonDisplayed); | ||||
|     } | ||||
| 
 | ||||
|     public void wikipediaButtonClicked() { | ||||
|         callback.addImageToWikipedia(contribution); | ||||
|     } | ||||
| 
 | ||||
|     public ImageRequest getImageRequest() { | ||||
|         return imageRequest; | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,152 @@ | |||
| package fr.free.nrw.commons.contributions | ||||
| 
 | ||||
| import android.net.Uri | ||||
| import android.text.TextUtils | ||||
| import android.view.View | ||||
| import android.webkit.URLUtil | ||||
| import androidx.appcompat.app.AlertDialog | ||||
| import androidx.recyclerview.widget.RecyclerView | ||||
| import com.facebook.imagepipeline.request.ImageRequest | ||||
| import com.facebook.imagepipeline.request.ImageRequestBuilder | ||||
| import fr.free.nrw.commons.R | ||||
| import fr.free.nrw.commons.databinding.LayoutContributionBinding | ||||
| import fr.free.nrw.commons.media.MediaClient | ||||
| import io.reactivex.android.schedulers.AndroidSchedulers | ||||
| import io.reactivex.disposables.CompositeDisposable | ||||
| import io.reactivex.schedulers.Schedulers | ||||
| import java.io.File | ||||
| 
 | ||||
| class ContributionViewHolder internal constructor( | ||||
|     private val parent: View, private val callback: ContributionsListAdapter.Callback, | ||||
|     private val mediaClient: MediaClient | ||||
| ) : RecyclerView.ViewHolder(parent) { | ||||
|     var binding: LayoutContributionBinding = LayoutContributionBinding.bind(parent) | ||||
| 
 | ||||
|     private var position = 0 | ||||
|     private var contribution: Contribution? = null | ||||
|     private val compositeDisposable = CompositeDisposable() | ||||
|     private var isWikipediaButtonDisplayed = false | ||||
|     private val pausingPopUp: AlertDialog | ||||
|     var imageRequest: ImageRequest? = null | ||||
|         private set | ||||
| 
 | ||||
|     init { | ||||
|         binding.contributionImage.setOnClickListener { v: View? -> imageClicked() } | ||||
|         binding.wikipediaButton.setOnClickListener { v: View? -> wikipediaButtonClicked() } | ||||
| 
 | ||||
|         /* Set a dialog indicating that the upload is being paused. This is needed because pausing | ||||
| an upload might take a dozen seconds. */ | ||||
|         val builder = AlertDialog.Builder( | ||||
|             parent.context | ||||
|         ) | ||||
|         builder.setCancelable(false) | ||||
|         builder.setView(R.layout.progress_dialog) | ||||
|         pausingPopUp = builder.create() | ||||
|     } | ||||
| 
 | ||||
|     fun init(position: Int, contribution: Contribution?) { | ||||
|         //handling crashes when the contribution is null. | ||||
| 
 | ||||
|         if (null == contribution) { | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
|         this.contribution = contribution | ||||
|         this.position = position | ||||
|         binding.contributionTitle.text = contribution.media.mostRelevantCaption | ||||
|         binding.authorView.text = contribution.media.author | ||||
| 
 | ||||
|         //Removes flicker of loading image. | ||||
|         binding.contributionImage.hierarchy.fadeDuration = 0 | ||||
| 
 | ||||
|         binding.contributionImage.hierarchy.setPlaceholderImage(R.drawable.image_placeholder) | ||||
|         binding.contributionImage.hierarchy.setFailureImage(R.drawable.image_placeholder) | ||||
| 
 | ||||
|         val imageSource = chooseImageSource( | ||||
|             contribution.media.thumbUrl, | ||||
|             contribution.localUri | ||||
|         ) | ||||
|         if (!TextUtils.isEmpty(imageSource)) { | ||||
|             if (URLUtil.isHttpsUrl(imageSource)) { | ||||
|                 imageRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(imageSource)) | ||||
|                     .setProgressiveRenderingEnabled(true) | ||||
|                     .build() | ||||
|             } else if (URLUtil.isFileUrl(imageSource)) { | ||||
|                 imageRequest = ImageRequest.fromUri(Uri.parse(imageSource)) | ||||
|             } else if (imageSource != null) { | ||||
|                 val file = File(imageSource) | ||||
|                 imageRequest = ImageRequest.fromFile(file) | ||||
|             } | ||||
| 
 | ||||
|             if (imageRequest != null) { | ||||
|                 binding.contributionImage.setImageRequest(imageRequest) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         binding.contributionSequenceNumber.text = (position + 1).toString() | ||||
|         binding.contributionSequenceNumber.visibility = View.VISIBLE | ||||
|         binding.wikipediaButton.visibility = View.GONE | ||||
|         binding.contributionState.visibility = View.GONE | ||||
|         binding.contributionProgress.visibility = View.GONE | ||||
|         binding.imageOptions.visibility = View.GONE | ||||
|         binding.contributionState.text = "" | ||||
|         checkIfMediaExistsOnWikipediaPage(contribution) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Checks if a media exists on the corresponding Wikipedia article Currently the check is made | ||||
|      * for the device's current language Wikipedia | ||||
|      * | ||||
|      * @param contribution | ||||
|      */ | ||||
|     private fun checkIfMediaExistsOnWikipediaPage(contribution: Contribution) { | ||||
|         if (contribution.wikidataPlace == null | ||||
|             || contribution.wikidataPlace!!.wikipediaArticle == null | ||||
|         ) { | ||||
|             return | ||||
|         } | ||||
|         val wikipediaArticle = contribution.wikidataPlace!!.getWikipediaPageTitle() | ||||
|         compositeDisposable.add( | ||||
|             mediaClient.doesPageContainMedia(wikipediaArticle) | ||||
|                 .subscribeOn(Schedulers.io()) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe { mediaExists: Boolean -> | ||||
|                     displayWikipediaButton(mediaExists) | ||||
|                 }) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Handle action buttons visibility if the corresponding wikipedia page doesn't contain any | ||||
|      * media. This method needs to control the state of just the scenario where media does not | ||||
|      * exists as other scenarios are already handled in the init method. | ||||
|      * | ||||
|      * @param mediaExists | ||||
|      */ | ||||
|     private fun displayWikipediaButton(mediaExists: Boolean) { | ||||
|         if (!mediaExists) { | ||||
|             binding.wikipediaButton.visibility = View.VISIBLE | ||||
|             isWikipediaButtonDisplayed = true | ||||
|             binding.imageOptions.visibility = View.VISIBLE | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns the image source for the image view, first preference is given to thumbUrl if that is | ||||
|      * null, moves to local uri and if both are null return null | ||||
|      * | ||||
|      * @param thumbUrl | ||||
|      * @param localUri | ||||
|      * @return | ||||
|      */ | ||||
|     private fun chooseImageSource(thumbUrl: String?, localUri: Uri?): String? { | ||||
|         return if (!TextUtils.isEmpty(thumbUrl)) thumbUrl else localUri?.toString() | ||||
|     } | ||||
| 
 | ||||
|     fun imageClicked() { | ||||
|         callback.openMediaDetail(position, isWikipediaButtonDisplayed) | ||||
|     } | ||||
| 
 | ||||
|     fun wikipediaButtonClicked() { | ||||
|         callback.addImageToWikipedia(contribution) | ||||
|     } | ||||
| } | ||||
|  | @ -1,23 +0,0 @@ | |||
| package fr.free.nrw.commons.contributions; | ||||
| 
 | ||||
| import android.content.Context; | ||||
| import fr.free.nrw.commons.BasePresenter; | ||||
| 
 | ||||
| /** | ||||
|  * The contract for Contributions View & Presenter | ||||
|  */ | ||||
| public class ContributionsContract { | ||||
| 
 | ||||
|     public interface View { | ||||
| 
 | ||||
|         void showMessage(String localizedMessage); | ||||
| 
 | ||||
|         Context getContext(); | ||||
|     } | ||||
| 
 | ||||
|     public interface UserActionListener extends BasePresenter<ContributionsContract.View> { | ||||
| 
 | ||||
|         Contribution getContributionsWithTitle(String uri); | ||||
| 
 | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,19 @@ | |||
| package fr.free.nrw.commons.contributions | ||||
| 
 | ||||
| import android.content.Context | ||||
| import fr.free.nrw.commons.BasePresenter | ||||
| 
 | ||||
| /** | ||||
|  * The contract for Contributions View & Presenter | ||||
|  */ | ||||
| interface ContributionsContract { | ||||
| 
 | ||||
|     interface View { | ||||
|         fun showMessage(localizedMessage: String) | ||||
|         fun getContext(): Context | ||||
|     } | ||||
| 
 | ||||
|     interface UserActionListener : BasePresenter<View> { | ||||
|         fun getContributionsWithTitle(uri: String): Contribution | ||||
|     } | ||||
| } | ||||
|  | @ -1,940 +0,0 @@ | |||
| package fr.free.nrw.commons.contributions; | ||||
| 
 | ||||
| import static android.content.Context.SENSOR_SERVICE; | ||||
| import static fr.free.nrw.commons.contributions.Contribution.STATE_FAILED; | ||||
| import static fr.free.nrw.commons.contributions.Contribution.STATE_PAUSED; | ||||
| import static fr.free.nrw.commons.nearby.fragments.NearbyParentFragment.WLM_URL; | ||||
| import static fr.free.nrw.commons.profile.ProfileActivity.KEY_USERNAME; | ||||
| import static fr.free.nrw.commons.utils.LengthUtils.computeBearing; | ||||
| import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween; | ||||
| 
 | ||||
| import android.Manifest; | ||||
| import android.Manifest.permission; | ||||
| import android.annotation.SuppressLint; | ||||
| import android.content.Context; | ||||
| import android.content.Intent; | ||||
| import android.hardware.Sensor; | ||||
| import android.hardware.SensorEvent; | ||||
| import android.hardware.SensorEventListener; | ||||
| import android.hardware.SensorManager; | ||||
| import android.os.Bundle; | ||||
| import android.view.LayoutInflater; | ||||
| import android.view.Menu; | ||||
| import android.view.MenuInflater; | ||||
| import android.view.MenuItem; | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| import android.widget.CheckBox; | ||||
| import android.widget.ImageView; | ||||
| import android.widget.TextView; | ||||
| import android.widget.Toast; | ||||
| import androidx.activity.result.ActivityResultCallback; | ||||
| import androidx.activity.result.ActivityResultLauncher; | ||||
| import androidx.activity.result.contract.ActivityResultContracts; | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.fragment.app.Fragment; | ||||
| import androidx.fragment.app.FragmentManager.OnBackStackChangedListener; | ||||
| import androidx.fragment.app.FragmentTransaction; | ||||
| import fr.free.nrw.commons.Utils; | ||||
| import fr.free.nrw.commons.auth.SessionManager; | ||||
| import fr.free.nrw.commons.databinding.FragmentContributionsBinding; | ||||
| import fr.free.nrw.commons.notification.models.Notification; | ||||
| import fr.free.nrw.commons.notification.NotificationController; | ||||
| import fr.free.nrw.commons.profile.ProfileActivity; | ||||
| import fr.free.nrw.commons.theme.BaseActivity; | ||||
| import fr.free.nrw.commons.upload.UploadProgressActivity; | ||||
| import java.util.Calendar; | ||||
| import java.util.Date; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
| import javax.inject.Inject; | ||||
| import javax.inject.Named; | ||||
| import androidx.work.WorkManager; | ||||
| import fr.free.nrw.commons.Media; | ||||
| import fr.free.nrw.commons.R; | ||||
| import fr.free.nrw.commons.campaigns.models.Campaign; | ||||
| import fr.free.nrw.commons.campaigns.CampaignView; | ||||
| import fr.free.nrw.commons.campaigns.CampaignsPresenter; | ||||
| import fr.free.nrw.commons.campaigns.ICampaignsView; | ||||
| import fr.free.nrw.commons.contributions.ContributionsListFragment.Callback; | ||||
| import fr.free.nrw.commons.contributions.MainActivity.ActiveFragment; | ||||
| import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; | ||||
| import fr.free.nrw.commons.kvstore.JsonKvStore; | ||||
| import fr.free.nrw.commons.location.LatLng; | ||||
| import fr.free.nrw.commons.location.LocationServiceManager; | ||||
| import fr.free.nrw.commons.location.LocationUpdateListener; | ||||
| import fr.free.nrw.commons.media.MediaDetailPagerFragment; | ||||
| import fr.free.nrw.commons.media.MediaDetailPagerFragment.MediaDetailProvider; | ||||
| import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; | ||||
| import fr.free.nrw.commons.nearby.NearbyController; | ||||
| import fr.free.nrw.commons.nearby.NearbyNotificationCardView; | ||||
| import fr.free.nrw.commons.nearby.Place; | ||||
| import fr.free.nrw.commons.notification.NotificationActivity; | ||||
| import fr.free.nrw.commons.upload.worker.UploadWorker; | ||||
| import fr.free.nrw.commons.utils.ConfigUtils; | ||||
| import fr.free.nrw.commons.utils.DialogUtil; | ||||
| import fr.free.nrw.commons.utils.NetworkUtils; | ||||
| import fr.free.nrw.commons.utils.PermissionUtils; | ||||
| import fr.free.nrw.commons.utils.ViewUtil; | ||||
| import io.reactivex.Observable; | ||||
| import io.reactivex.android.schedulers.AndroidSchedulers; | ||||
| import io.reactivex.disposables.CompositeDisposable; | ||||
| import io.reactivex.schedulers.Schedulers; | ||||
| import timber.log.Timber; | ||||
| 
 | ||||
| public class ContributionsFragment | ||||
|     extends CommonsDaggerSupportFragment | ||||
|     implements | ||||
|     OnBackStackChangedListener, | ||||
|     LocationUpdateListener, | ||||
|     MediaDetailProvider, | ||||
|     SensorEventListener, | ||||
|     ICampaignsView, ContributionsContract.View, Callback { | ||||
| 
 | ||||
|     @Inject | ||||
|     @Named("default_preferences") | ||||
|     JsonKvStore store; | ||||
|     @Inject | ||||
|     NearbyController nearbyController; | ||||
|     @Inject | ||||
|     OkHttpJsonApiClient okHttpJsonApiClient; | ||||
|     @Inject | ||||
|     CampaignsPresenter presenter; | ||||
|     @Inject | ||||
|     LocationServiceManager locationManager; | ||||
|     @Inject | ||||
|     NotificationController notificationController; | ||||
|     @Inject | ||||
|     ContributionController contributionController; | ||||
| 
 | ||||
|     private CompositeDisposable compositeDisposable = new CompositeDisposable(); | ||||
| 
 | ||||
|     private ContributionsListFragment contributionsListFragment; | ||||
|     private static final String CONTRIBUTION_LIST_FRAGMENT_TAG = "ContributionListFragmentTag"; | ||||
|     private MediaDetailPagerFragment mediaDetailPagerFragment; | ||||
|     static final String MEDIA_DETAIL_PAGER_FRAGMENT_TAG = "MediaDetailFragmentTag"; | ||||
|     private static final int MAX_RETRIES = 10; | ||||
| 
 | ||||
|     public FragmentContributionsBinding binding; | ||||
| 
 | ||||
|     @Inject | ||||
|     ContributionsPresenter contributionsPresenter; | ||||
| 
 | ||||
|     @Inject | ||||
|     SessionManager sessionManager; | ||||
| 
 | ||||
|     private LatLng currentLatLng; | ||||
| 
 | ||||
|     private boolean isFragmentAttachedBefore = false; | ||||
|     private View checkBoxView; | ||||
|     private CheckBox checkBox; | ||||
| 
 | ||||
|     public TextView notificationCount; | ||||
| 
 | ||||
|     public TextView pendingUploadsCountTextView; | ||||
| 
 | ||||
|     public TextView uploadsErrorTextView; | ||||
| 
 | ||||
|     public ImageView pendingUploadsImageView; | ||||
| 
 | ||||
|     private Campaign wlmCampaign; | ||||
| 
 | ||||
|     String userName; | ||||
|     private boolean isUserProfile; | ||||
| 
 | ||||
|     private SensorManager mSensorManager; | ||||
|     private Sensor mLight; | ||||
|     private float direction; | ||||
|     private ActivityResultLauncher<String[]> nearbyLocationPermissionLauncher = registerForActivityResult( | ||||
|         new ActivityResultContracts.RequestMultiplePermissions(), | ||||
|         new ActivityResultCallback<Map<String, Boolean>>() { | ||||
|             @Override | ||||
|             public void onActivityResult(Map<String, Boolean> result) { | ||||
|                 boolean areAllGranted = true; | ||||
|                 for (final boolean b : result.values()) { | ||||
|                     areAllGranted = areAllGranted && b; | ||||
|                 } | ||||
| 
 | ||||
|                 if (areAllGranted) { | ||||
|                     onLocationPermissionGranted(); | ||||
|                 } else { | ||||
|                     if (shouldShowRequestPermissionRationale( | ||||
|                         Manifest.permission.ACCESS_FINE_LOCATION) | ||||
|                         && store.getBoolean("displayLocationPermissionForCardView", true) | ||||
|                         && !store.getBoolean("doNotAskForLocationPermission", false) | ||||
|                         && (((MainActivity) getActivity()).activeFragment | ||||
|                         == ActiveFragment.CONTRIBUTIONS)) { | ||||
|                         binding.cardViewNearby.permissionType = NearbyNotificationCardView.PermissionType.ENABLE_LOCATION_PERMISSION; | ||||
|                     } else { | ||||
|                         displayYouWontSeeNearbyMessage(); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|     @NonNull | ||||
|     public static ContributionsFragment newInstance() { | ||||
|         ContributionsFragment fragment = new ContributionsFragment(); | ||||
|         fragment.setRetainInstance(true); | ||||
|         return fragment; | ||||
|     } | ||||
| 
 | ||||
|     private boolean shouldShowMediaDetailsFragment; | ||||
| 
 | ||||
|     @Override | ||||
|     public void onCreate(@Nullable Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         if (getArguments() != null && getArguments().getString(KEY_USERNAME) != null) { | ||||
|             userName = getArguments().getString(KEY_USERNAME); | ||||
|             isUserProfile = true; | ||||
|         } | ||||
|         mSensorManager = (SensorManager) getActivity().getSystemService(SENSOR_SERVICE); | ||||
|         mLight = mSensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION); | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     @Override | ||||
|     public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, | ||||
|         @Nullable Bundle savedInstanceState) { | ||||
| 
 | ||||
|         binding = FragmentContributionsBinding.inflate(inflater, container, false); | ||||
| 
 | ||||
|         initWLMCampaign(); | ||||
|         presenter.onAttachView(this); | ||||
|         contributionsPresenter.onAttachView(this); | ||||
|         binding.campaignsView.setVisibility(View.GONE); | ||||
|         checkBoxView = View.inflate(getActivity(), R.layout.nearby_permission_dialog, null); | ||||
|         checkBox = (CheckBox) checkBoxView.findViewById(R.id.never_ask_again); | ||||
|         checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> { | ||||
|             if (isChecked) { | ||||
|                 // Do not ask for permission on activity start again | ||||
|                 store.putBoolean("displayLocationPermissionForCardView", false); | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         if (savedInstanceState != null) { | ||||
|             mediaDetailPagerFragment = (MediaDetailPagerFragment) getChildFragmentManager() | ||||
|                 .findFragmentByTag(MEDIA_DETAIL_PAGER_FRAGMENT_TAG); | ||||
|             contributionsListFragment = (ContributionsListFragment) getChildFragmentManager() | ||||
|                 .findFragmentByTag(CONTRIBUTION_LIST_FRAGMENT_TAG); | ||||
|             shouldShowMediaDetailsFragment = savedInstanceState.getBoolean("mediaDetailsVisible"); | ||||
|         } | ||||
| 
 | ||||
|         initFragments(); | ||||
|         if (!isUserProfile) { | ||||
|             upDateUploadCount(); | ||||
|         } | ||||
|         if (shouldShowMediaDetailsFragment) { | ||||
|             showMediaDetailPagerFragment(); | ||||
|         } else { | ||||
|             if (mediaDetailPagerFragment != null) { | ||||
|                 removeFragment(mediaDetailPagerFragment); | ||||
|             } | ||||
|             showContributionsListFragment(); | ||||
|         } | ||||
| 
 | ||||
|         if (!ConfigUtils.isBetaFlavour() && sessionManager.isUserLoggedIn() | ||||
|             && sessionManager.getCurrentAccount() != null && !isUserProfile) { | ||||
|             setUploadCount(); | ||||
|         } | ||||
|         setHasOptionsMenu(true); | ||||
|         return binding.getRoot(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Initialise the campaign object for WML | ||||
|      */ | ||||
|     private void initWLMCampaign() { | ||||
|         wlmCampaign = new Campaign(getString(R.string.wlm_campaign_title), | ||||
|             getString(R.string.wlm_campaign_description), Utils.getWLMStartDate().toString(), | ||||
|             Utils.getWLMEndDate().toString(), WLM_URL, true); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onCreateOptionsMenu(@NonNull final Menu menu, | ||||
|         @NonNull final MenuInflater inflater) { | ||||
| 
 | ||||
|         // Removing contributions menu items for ProfileActivity | ||||
|         if (getActivity() instanceof ProfileActivity) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         inflater.inflate(R.menu.contribution_activity_notification_menu, menu); | ||||
| 
 | ||||
|         MenuItem notificationsMenuItem = menu.findItem(R.id.notifications); | ||||
|         final View notification = notificationsMenuItem.getActionView(); | ||||
|         notificationCount = notification.findViewById(R.id.notification_count_badge); | ||||
|         MenuItem uploadMenuItem = menu.findItem(R.id.upload_tab); | ||||
|         final View uploadMenuItemActionView = uploadMenuItem.getActionView(); | ||||
|         pendingUploadsCountTextView = uploadMenuItemActionView.findViewById( | ||||
|             R.id.pending_uploads_count_badge); | ||||
|         uploadsErrorTextView = uploadMenuItemActionView.findViewById( | ||||
|             R.id.uploads_error_count_badge); | ||||
|         pendingUploadsImageView = uploadMenuItemActionView.findViewById( | ||||
|             R.id.pending_uploads_image_view); | ||||
|         if (pendingUploadsImageView != null) { | ||||
|             pendingUploadsImageView.setOnClickListener(view -> { | ||||
|                 startActivity(new Intent(getContext(), UploadProgressActivity.class)); | ||||
|             }); | ||||
|         } | ||||
|         if (pendingUploadsCountTextView != null) { | ||||
|             pendingUploadsCountTextView.setOnClickListener(view -> { | ||||
|                 startActivity(new Intent(getContext(), UploadProgressActivity.class)); | ||||
|             }); | ||||
|         } | ||||
|         if (uploadsErrorTextView != null) { | ||||
|             uploadsErrorTextView.setOnClickListener(view -> { | ||||
|                 startActivity(new Intent(getContext(), UploadProgressActivity.class)); | ||||
|             }); | ||||
|         } | ||||
|         notification.setOnClickListener(view -> { | ||||
|             NotificationActivity.Companion.startYourself(getContext(), "unread"); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     @SuppressLint("CheckResult") | ||||
|     public void setNotificationCount() { | ||||
|         compositeDisposable.add(notificationController.getNotifications(false) | ||||
|             .subscribeOn(Schedulers.io()) | ||||
|             .observeOn(AndroidSchedulers.mainThread()) | ||||
|             .subscribe(this::initNotificationViews, | ||||
|                 throwable -> Timber.e(throwable, "Error occurred while loading notifications"))); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Temporarily disabled, see issue [https://github.com/commons-app/apps-android-commons/issues/5847] | ||||
|      * Sets the visibility of the upload icon based on the number of failed and pending | ||||
|      * contributions. | ||||
|      */ | ||||
| //    public void setUploadIconVisibility() { | ||||
| //        contributionController.getFailedAndPendingContributions(); | ||||
| //        contributionController.failedAndPendingContributionList.observe(getViewLifecycleOwner(), | ||||
| //            list -> { | ||||
| //                updateUploadIcon(list.size()); | ||||
| //            }); | ||||
| //    } | ||||
| 
 | ||||
|     /** | ||||
|      * Sets the count for the upload icon based on the number of pending and failed contributions. | ||||
|      */ | ||||
|     public void setUploadIconCount() { | ||||
|         contributionController.getPendingContributions(); | ||||
|         contributionController.pendingContributionList.observe(getViewLifecycleOwner(), | ||||
|             list -> { | ||||
|                 updatePendingIcon(list.size()); | ||||
|             }); | ||||
|         contributionController.getFailedContributions(); | ||||
|         contributionController.failedContributionList.observe(getViewLifecycleOwner(), list -> { | ||||
|             updateErrorIcon(list.size()); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     public void scrollToTop() { | ||||
|         if (contributionsListFragment != null) { | ||||
|             contributionsListFragment.scrollToTop(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private void initNotificationViews(List<Notification> notificationList) { | ||||
|         Timber.d("Number of notifications is %d", notificationList.size()); | ||||
|         if (notificationList.isEmpty()) { | ||||
|             notificationCount.setVisibility(View.GONE); | ||||
|         } else { | ||||
|             notificationCount.setVisibility(View.VISIBLE); | ||||
|             notificationCount.setText(String.valueOf(notificationList.size())); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onAttach(Context context) { | ||||
|         super.onAttach(context); | ||||
|         /* | ||||
|         - There are some operations we need auth, so we need to make sure isAuthCookieAcquired. | ||||
|         - And since we use same retained fragment doesn't want to make all network operations | ||||
|         all over again on same fragment attached to recreated activity, we do this network | ||||
|         operations on first time fragment attached to an activity. Then they will be retained | ||||
|         until fragment life time ends. | ||||
|          */ | ||||
|         if (!isFragmentAttachedBefore && getActivity() != null) { | ||||
|             isFragmentAttachedBefore = true; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Replace FrameLayout with ContributionsListFragment, user will see contributions list. Creates | ||||
|      * new one if null. | ||||
|      */ | ||||
|     private void showContributionsListFragment() { | ||||
|         // show nearby card view on contributions list is visible | ||||
|         if (binding.cardViewNearby != null && !isUserProfile) { | ||||
|             if (store.getBoolean("displayNearbyCardView", true)) { | ||||
|                 if (binding.cardViewNearby.cardViewVisibilityState | ||||
|                     == NearbyNotificationCardView.CardViewVisibilityState.READY) { | ||||
|                     binding.cardViewNearby.setVisibility(View.VISIBLE); | ||||
|                 } | ||||
|             } else { | ||||
|                 binding.cardViewNearby.setVisibility(View.GONE); | ||||
|             } | ||||
|         } | ||||
|         showFragment(contributionsListFragment, CONTRIBUTION_LIST_FRAGMENT_TAG, | ||||
|             mediaDetailPagerFragment); | ||||
|     } | ||||
| 
 | ||||
|     private void showMediaDetailPagerFragment() { | ||||
|         // hide nearby card view on media detail is visible | ||||
|         setupViewForMediaDetails(); | ||||
|         showFragment(mediaDetailPagerFragment, MEDIA_DETAIL_PAGER_FRAGMENT_TAG, | ||||
|             contributionsListFragment); | ||||
|     } | ||||
| 
 | ||||
|     private void setupViewForMediaDetails() { | ||||
|         if (binding != null) { | ||||
|             binding.campaignsView.setVisibility(View.GONE); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onBackStackChanged() { | ||||
|         fetchCampaigns(); | ||||
|     } | ||||
| 
 | ||||
|     private void initFragments() { | ||||
|         if (null == contributionsListFragment) { | ||||
|             contributionsListFragment = new ContributionsListFragment(); | ||||
|             Bundle contributionsListBundle = new Bundle(); | ||||
|             contributionsListBundle.putString(KEY_USERNAME, userName); | ||||
|             contributionsListFragment.setArguments(contributionsListBundle); | ||||
|         } | ||||
| 
 | ||||
|         if (shouldShowMediaDetailsFragment) { | ||||
|             showMediaDetailPagerFragment(); | ||||
|         } else { | ||||
|             showContributionsListFragment(); | ||||
|         } | ||||
| 
 | ||||
|         showFragment(contributionsListFragment, CONTRIBUTION_LIST_FRAGMENT_TAG, | ||||
|             mediaDetailPagerFragment); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Replaces the root frame layout with the given fragment | ||||
|      * | ||||
|      * @param fragment | ||||
|      * @param tag | ||||
|      * @param otherFragment | ||||
|      */ | ||||
|     private void showFragment(Fragment fragment, String tag, Fragment otherFragment) { | ||||
|         FragmentTransaction transaction = getChildFragmentManager().beginTransaction(); | ||||
|         if (fragment.isAdded() && otherFragment != null) { | ||||
|             transaction.hide(otherFragment); | ||||
|             transaction.show(fragment); | ||||
|             transaction.addToBackStack(tag); | ||||
|             transaction.commit(); | ||||
|             getChildFragmentManager().executePendingTransactions(); | ||||
|         } else if (fragment.isAdded() && otherFragment == null) { | ||||
|             transaction.show(fragment); | ||||
|             transaction.addToBackStack(tag); | ||||
|             transaction.commit(); | ||||
|             getChildFragmentManager().executePendingTransactions(); | ||||
|         } else if (!fragment.isAdded() && otherFragment != null) { | ||||
|             transaction.hide(otherFragment); | ||||
|             transaction.add(R.id.root_frame, fragment, tag); | ||||
|             transaction.addToBackStack(tag); | ||||
|             transaction.commit(); | ||||
|             getChildFragmentManager().executePendingTransactions(); | ||||
|         } else if (!fragment.isAdded()) { | ||||
|             transaction.replace(R.id.root_frame, fragment, tag); | ||||
|             transaction.addToBackStack(tag); | ||||
|             transaction.commit(); | ||||
|             getChildFragmentManager().executePendingTransactions(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public void removeFragment(Fragment fragment) { | ||||
|         getChildFragmentManager() | ||||
|             .beginTransaction() | ||||
|             .remove(fragment) | ||||
|             .commit(); | ||||
|         getChildFragmentManager().executePendingTransactions(); | ||||
|     } | ||||
| 
 | ||||
|     @SuppressWarnings("ConstantConditions") | ||||
|     private void setUploadCount() { | ||||
|         compositeDisposable.add(okHttpJsonApiClient | ||||
|             .getUploadCount(((MainActivity) getActivity()).sessionManager.getCurrentAccount().name) | ||||
|             .subscribeOn(Schedulers.io()) | ||||
|             .observeOn(AndroidSchedulers.mainThread()) | ||||
|             .subscribe(this::displayUploadCount, | ||||
|                 t -> Timber.e(t, "Fetching upload count failed") | ||||
|             )); | ||||
|     } | ||||
| 
 | ||||
|     private void displayUploadCount(Integer uploadCount) { | ||||
|         if (getActivity().isFinishing() | ||||
|             || getResources() == null) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         ((MainActivity) getActivity()).setNumOfUploads(uploadCount); | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onPause() { | ||||
|         super.onPause(); | ||||
|         locationManager.removeLocationListener(this); | ||||
|         locationManager.unregisterLocationManager(); | ||||
|         mSensorManager.unregisterListener(this); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onSaveInstanceState(Bundle outState) { | ||||
|         super.onSaveInstanceState(outState); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onResume() { | ||||
|         super.onResume(); | ||||
|         contributionsPresenter.onAttachView(this); | ||||
|         locationManager.addLocationListener(this); | ||||
| 
 | ||||
|         if (binding == null) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         binding.cardViewNearby.permissionRequestButton.setOnClickListener(v -> { | ||||
|             showNearbyCardPermissionRationale(); | ||||
|         }); | ||||
| 
 | ||||
|         // Notification cards should only be seen on contributions list, not in media details | ||||
|         if (mediaDetailPagerFragment == null && !isUserProfile) { | ||||
|             if (store.getBoolean("displayNearbyCardView", true)) { | ||||
|                 checkPermissionsAndShowNearbyCardView(); | ||||
| 
 | ||||
|                 // Calling nearby card to keep showing it even when user clicks on it and comes back | ||||
|                 try { | ||||
|                     updateClosestNearbyCardViewInfo(); | ||||
|                 } catch (Exception e) { | ||||
|                     Timber.e(e); | ||||
|                 } | ||||
|                 if (binding.cardViewNearby.cardViewVisibilityState | ||||
|                     == NearbyNotificationCardView.CardViewVisibilityState.READY) { | ||||
|                     binding.cardViewNearby.setVisibility(View.VISIBLE); | ||||
|                 } | ||||
| 
 | ||||
|             } else { | ||||
|                 // Hide nearby notification card view if related shared preferences is false | ||||
|                 binding.cardViewNearby.setVisibility(View.GONE); | ||||
|             } | ||||
| 
 | ||||
|             // Notification Count and Campaigns should not be set, if it is used in User Profile | ||||
|             if (!isUserProfile) { | ||||
|                 setNotificationCount(); | ||||
|                 fetchCampaigns(); | ||||
|                 // Temporarily disabled, see issue [https://github.com/commons-app/apps-android-commons/issues/5847] | ||||
|                 // setUploadIconVisibility(); | ||||
|                 setUploadIconCount(); | ||||
|             } | ||||
|         } | ||||
|         mSensorManager.registerListener(this, mLight, SensorManager.SENSOR_DELAY_UI); | ||||
|     } | ||||
| 
 | ||||
|     private void checkPermissionsAndShowNearbyCardView() { | ||||
|         if (PermissionUtils.hasPermission(getActivity(), | ||||
|             new String[]{Manifest.permission.ACCESS_FINE_LOCATION})) { | ||||
|             onLocationPermissionGranted(); | ||||
|         } else if (shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION) | ||||
|             && store.getBoolean("displayLocationPermissionForCardView", true) | ||||
|             && !store.getBoolean("doNotAskForLocationPermission", false) | ||||
|             && (((MainActivity) getActivity()).activeFragment == ActiveFragment.CONTRIBUTIONS)) { | ||||
|             binding.cardViewNearby.permissionType = NearbyNotificationCardView.PermissionType.ENABLE_LOCATION_PERMISSION; | ||||
|             showNearbyCardPermissionRationale(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private void requestLocationPermission() { | ||||
|         nearbyLocationPermissionLauncher.launch(new String[]{permission.ACCESS_FINE_LOCATION}); | ||||
|     } | ||||
| 
 | ||||
|     private void onLocationPermissionGranted() { | ||||
|         binding.cardViewNearby.permissionType = NearbyNotificationCardView.PermissionType.NO_PERMISSION_NEEDED; | ||||
|         locationManager.registerLocationManager(); | ||||
|     } | ||||
| 
 | ||||
|     private void showNearbyCardPermissionRationale() { | ||||
|         DialogUtil.showAlertDialog(getActivity(), | ||||
|             getString(R.string.nearby_card_permission_title), | ||||
|             getString(R.string.nearby_card_permission_explanation), | ||||
|             this::requestLocationPermission, | ||||
|             this::displayYouWontSeeNearbyMessage, | ||||
|             checkBoxView | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     private void displayYouWontSeeNearbyMessage() { | ||||
|         ViewUtil.showLongToast(getActivity(), | ||||
|             getResources().getString(R.string.unable_to_display_nearest_place)); | ||||
|         // Set to true as the user doesn't want the app to ask for location permission anymore | ||||
|         store.putBoolean("doNotAskForLocationPermission", true); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     private void updateClosestNearbyCardViewInfo() { | ||||
|         currentLatLng = locationManager.getLastLocation(); | ||||
|         compositeDisposable.add(Observable.fromCallable(() -> nearbyController | ||||
|                 .loadAttractionsFromLocation(currentLatLng, currentLatLng, true, | ||||
|                     false)) // thanks to boolean, it will only return closest result | ||||
|             .subscribeOn(Schedulers.io()) | ||||
|             .observeOn(AndroidSchedulers.mainThread()) | ||||
|             .subscribe(this::updateNearbyNotification, | ||||
|                 throwable -> { | ||||
|                     Timber.d(throwable); | ||||
|                     updateNearbyNotification(null); | ||||
|                 })); | ||||
|     } | ||||
| 
 | ||||
|     private void updateNearbyNotification( | ||||
|         @Nullable NearbyController.NearbyPlacesInfo nearbyPlacesInfo) { | ||||
|         if (nearbyPlacesInfo != null && nearbyPlacesInfo.placeList != null | ||||
|             && nearbyPlacesInfo.placeList.size() > 0) { | ||||
|             Place closestNearbyPlace = null; | ||||
|             // Find the first nearby place that has no image and exists | ||||
|             for (Place place : nearbyPlacesInfo.placeList) { | ||||
|                 if (place.pic.equals("") && place.exists) { | ||||
|                     closestNearbyPlace = place; | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             if (closestNearbyPlace == null) { | ||||
|                 binding.cardViewNearby.setVisibility(View.GONE); | ||||
|             } else { | ||||
|                 String distance = formatDistanceBetween(currentLatLng, closestNearbyPlace.location); | ||||
|                 closestNearbyPlace.setDistance(distance); | ||||
|                 direction = (float) computeBearing(currentLatLng, closestNearbyPlace.location); | ||||
|                 binding.cardViewNearby.updateContent(closestNearbyPlace); | ||||
|             } | ||||
|         } else { | ||||
|             // Means that no close nearby place is found | ||||
|             binding.cardViewNearby.setVisibility(View.GONE); | ||||
|         } | ||||
| 
 | ||||
|         // Prevent Nearby banner from appearing in Media Details, fixing bug https://github.com/commons-app/apps-android-commons/issues/4731 | ||||
|         if (mediaDetailPagerFragment != null && !contributionsListFragment.isVisible()) { | ||||
|             binding.cardViewNearby.setVisibility(View.GONE); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onDestroy() { | ||||
|         try { | ||||
|             compositeDisposable.clear(); | ||||
|             getChildFragmentManager().removeOnBackStackChangedListener(this); | ||||
|             locationManager.unregisterLocationManager(); | ||||
|             locationManager.removeLocationListener(this); | ||||
|             super.onDestroy(); | ||||
|         } catch (IllegalArgumentException | IllegalStateException exception) { | ||||
|             Timber.e(exception); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onLocationChangedSignificantly(LatLng latLng) { | ||||
|         // Will be called if location changed more than 1000 meter | ||||
|         updateClosestNearbyCardViewInfo(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onLocationChangedSlightly(LatLng latLng) { | ||||
|         /* Update closest nearby notification card onLocationChangedSlightly | ||||
|          */ | ||||
|         try { | ||||
|             updateClosestNearbyCardViewInfo(); | ||||
|         } catch (Exception e) { | ||||
|             Timber.e(e); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onLocationChangedMedium(LatLng latLng) { | ||||
|         // Update closest nearby card view if location changed more than 500 meters | ||||
|         updateClosestNearbyCardViewInfo(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onViewCreated(@NonNull View view, | ||||
|         @Nullable Bundle savedInstanceState) { | ||||
|         super.onViewCreated(view, savedInstanceState); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * As the home screen has limited space, we have choosen to show either campaigns or WLM card. | ||||
|      * The WLM Card gets the priority over monuments, so if the WLM is going on we show that instead | ||||
|      * of campaigns on the campaigns card | ||||
|      */ | ||||
|     private void fetchCampaigns() { | ||||
|         if (Utils.isMonumentsEnabled(new Date())) { | ||||
|             if (binding != null) { | ||||
|                 binding.campaignsView.setCampaign(wlmCampaign); | ||||
|                 binding.campaignsView.setVisibility(View.VISIBLE); | ||||
|             } | ||||
|         } else if (store.getBoolean(CampaignView.CAMPAIGNS_DEFAULT_PREFERENCE, true)) { | ||||
|             presenter.getCampaigns(); | ||||
|         } else { | ||||
|             if (binding != null) { | ||||
|                 binding.campaignsView.setVisibility(View.GONE); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void showMessage(String message) { | ||||
|         Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void showCampaigns(Campaign campaign) { | ||||
|         if (campaign != null && !isUserProfile) { | ||||
|             if (binding != null) { | ||||
|                 binding.campaignsView.setCampaign(campaign); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onDestroyView() { | ||||
|         super.onDestroyView(); | ||||
|         presenter.onDetachView(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void notifyDataSetChanged() { | ||||
|         if (mediaDetailPagerFragment != null) { | ||||
|             mediaDetailPagerFragment.notifyDataSetChanged(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Notify the viewpager that number of items have changed. | ||||
|      */ | ||||
|     @Override | ||||
|     public void viewPagerNotifyDataSetChanged() { | ||||
|         if (mediaDetailPagerFragment != null) { | ||||
|             mediaDetailPagerFragment.notifyDataSetChanged(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Updates the visibility and text of the pending uploads count TextView based on the given | ||||
|      * count. | ||||
|      * | ||||
|      * @param pendingCount The number of pending uploads. | ||||
|      */ | ||||
|     public void updatePendingIcon(int pendingCount) { | ||||
|         if (pendingUploadsCountTextView != null) { | ||||
|             if (pendingCount != 0) { | ||||
|                 pendingUploadsCountTextView.setVisibility(View.VISIBLE); | ||||
|                 pendingUploadsCountTextView.setText(String.valueOf(pendingCount)); | ||||
|             } else { | ||||
|                 pendingUploadsCountTextView.setVisibility(View.INVISIBLE); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Updates the visibility and text of the error uploads TextView based on the given count. | ||||
|      * | ||||
|      * @param errorCount The number of error uploads. | ||||
|      */ | ||||
|     public void updateErrorIcon(int errorCount) { | ||||
|         if (uploadsErrorTextView != null) { | ||||
|             if (errorCount != 0) { | ||||
|                 uploadsErrorTextView.setVisibility(View.VISIBLE); | ||||
|                 uploadsErrorTextView.setText(String.valueOf(errorCount)); | ||||
|             } else { | ||||
|                 uploadsErrorTextView.setVisibility(View.GONE); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Temporarily disabled, see issue [https://github.com/commons-app/apps-android-commons/issues/5847] | ||||
|      * @param count The number of pending uploads. | ||||
|      */ | ||||
| //    public void updateUploadIcon(int count) { | ||||
| //        if (pendingUploadsImageView != null) { | ||||
| //            if (count != 0) { | ||||
| //                pendingUploadsImageView.setVisibility(View.VISIBLE); | ||||
| //            } else { | ||||
| //                pendingUploadsImageView.setVisibility(View.GONE); | ||||
| //            } | ||||
| //        } | ||||
| //    } | ||||
| 
 | ||||
|     /** | ||||
|      * Replace whatever is in the current contributionsFragmentContainer view with | ||||
|      * mediaDetailPagerFragment, and preserve previous state in back stack. Called when user selects | ||||
|      * a contribution. | ||||
|      */ | ||||
|     @Override | ||||
|     public void showDetail(int position, boolean isWikipediaButtonDisplayed) { | ||||
|         if (mediaDetailPagerFragment == null || !mediaDetailPagerFragment.isVisible()) { | ||||
|             mediaDetailPagerFragment = MediaDetailPagerFragment.newInstance(false, true); | ||||
|             if (isUserProfile) { | ||||
|                 ((ProfileActivity) getActivity()).setScroll(false); | ||||
|             } | ||||
|             showMediaDetailPagerFragment(); | ||||
|         } | ||||
|         mediaDetailPagerFragment.showImage(position, isWikipediaButtonDisplayed); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public Media getMediaAtPosition(int i) { | ||||
|         return contributionsListFragment.getMediaAtPosition(i); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public int getTotalMediaCount() { | ||||
|         return contributionsListFragment.getTotalMediaCount(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public Integer getContributionStateAt(int position) { | ||||
|         return contributionsListFragment.getContributionStateAt(position); | ||||
|     } | ||||
| 
 | ||||
|     public boolean backButtonClicked() { | ||||
|         if (mediaDetailPagerFragment != null && mediaDetailPagerFragment.isVisible()) { | ||||
|             if (store.getBoolean("displayNearbyCardView", true) && !isUserProfile) { | ||||
|                 if (binding.cardViewNearby.cardViewVisibilityState | ||||
|                     == NearbyNotificationCardView.CardViewVisibilityState.READY) { | ||||
|                     binding.cardViewNearby.setVisibility(View.VISIBLE); | ||||
|                 } | ||||
|             } else { | ||||
|                 binding.cardViewNearby.setVisibility(View.GONE); | ||||
|             } | ||||
|             removeFragment(mediaDetailPagerFragment); | ||||
|             showFragment(contributionsListFragment, CONTRIBUTION_LIST_FRAGMENT_TAG, | ||||
|                 mediaDetailPagerFragment); | ||||
|             if (isUserProfile) { | ||||
|                 // Fragment is associated with ProfileActivity | ||||
|                 // Enable ParentViewPager Scroll | ||||
|                 ((ProfileActivity) getActivity()).setScroll(true); | ||||
|             } else { | ||||
|                 fetchCampaigns(); | ||||
|             } | ||||
|             if (getActivity() instanceof MainActivity) { | ||||
|                 // Fragment is associated with MainActivity | ||||
|                 ((BaseActivity) getActivity()).getSupportActionBar() | ||||
|                     .setDisplayHomeAsUpEnabled(false); | ||||
|                 ((MainActivity) getActivity()).showTabs(); | ||||
|             } | ||||
|             return true; | ||||
|         } | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     // Getter for mediaDetailPagerFragment | ||||
|     public MediaDetailPagerFragment getMediaDetailPagerFragment() { | ||||
|         return mediaDetailPagerFragment; | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * this function updates the number of contributions | ||||
|      */ | ||||
|     void upDateUploadCount() { | ||||
|         WorkManager.getInstance(getContext()) | ||||
|             .getWorkInfosForUniqueWorkLiveData(UploadWorker.class.getSimpleName()).observe( | ||||
|                 getViewLifecycleOwner(), workInfos -> { | ||||
|                     if (workInfos.size() > 0) { | ||||
|                         setUploadCount(); | ||||
|                     } | ||||
|                 }); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * Restarts the upload process for a contribution | ||||
|      * | ||||
|      * @param contribution | ||||
|      */ | ||||
|     public void restartUpload(Contribution contribution) { | ||||
|         contribution.setDateUploadStarted(Calendar.getInstance().getTime()); | ||||
|         if (contribution.getState() == Contribution.STATE_FAILED) { | ||||
|             if (contribution.getErrorInfo() == null) { | ||||
|                 contribution.setChunkInfo(null); | ||||
|                 contribution.setTransferred(0); | ||||
|             } | ||||
|             contributionsPresenter.checkDuplicateImageAndRestartContribution(contribution); | ||||
|         } else { | ||||
|             contribution.setState(Contribution.STATE_QUEUED); | ||||
|             contributionsPresenter.saveContribution(contribution); | ||||
|             Timber.d("Restarting for %s", contribution.toString()); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Retry upload when it is failed | ||||
|      * | ||||
|      * @param contribution contribution to be retried | ||||
|      */ | ||||
|     public void retryUpload(Contribution contribution) { | ||||
|         if (NetworkUtils.isInternetConnectionEstablished(getContext())) { | ||||
|             if (contribution.getState() == STATE_PAUSED) { | ||||
|                 restartUpload(contribution); | ||||
|             } else if (contribution.getState() == STATE_FAILED) { | ||||
|                 int retries = contribution.getRetries(); | ||||
|                 // TODO: Improve UX. Additional details: https://github.com/commons-app/apps-android-commons/pull/5257#discussion_r1304662562 | ||||
|                 /* Limit the number of retries for a failed upload | ||||
|                    to handle cases like invalid filename as such uploads | ||||
|                    will never be successful */ | ||||
|                 if (retries < MAX_RETRIES) { | ||||
|                     contribution.setRetries(retries + 1); | ||||
|                     Timber.d("Retried uploading %s %d times", contribution.getMedia().getFilename(), | ||||
|                         retries + 1); | ||||
|                     restartUpload(contribution); | ||||
|                 } else { | ||||
|                     // TODO: Show the exact reason for failure | ||||
|                     Toast.makeText(getContext(), | ||||
|                         R.string.retry_limit_reached, Toast.LENGTH_SHORT).show(); | ||||
|                 } | ||||
|             } else { | ||||
|                 Timber.d("Skipping re-upload for non-failed %s", contribution.toString()); | ||||
|             } | ||||
|         } else { | ||||
|             ViewUtil.showLongToast(getContext(), R.string.this_function_needs_network_connection); | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Reload media detail fragment once media is nominated | ||||
|      * | ||||
|      * @param index item position that has been nominated | ||||
|      */ | ||||
|     @Override | ||||
|     public void refreshNominatedMedia(int index) { | ||||
|         if (mediaDetailPagerFragment != null && !contributionsListFragment.isVisible()) { | ||||
|             removeFragment(mediaDetailPagerFragment); | ||||
|             mediaDetailPagerFragment = MediaDetailPagerFragment.newInstance(false, true); | ||||
|             mediaDetailPagerFragment.showImage(index); | ||||
|             showMediaDetailPagerFragment(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * When the device rotates, rotate the Nearby banner's compass arrow in tandem. | ||||
|      */ | ||||
|     @Override | ||||
|     public void onSensorChanged(SensorEvent event) { | ||||
|         float rotateDegree = Math.round(event.values[0]); | ||||
|         binding.cardViewNearby.rotateCompass(rotateDegree, direction); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onAccuracyChanged(Sensor sensor, int accuracy) { | ||||
|         // Nothing to do. | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,998 @@ | |||
| package fr.free.nrw.commons.contributions | ||||
| 
 | ||||
| import android.Manifest.permission | ||||
| import android.annotation.SuppressLint | ||||
| import android.content.Context | ||||
| import android.content.Intent | ||||
| import android.hardware.Sensor | ||||
| import android.hardware.SensorEvent | ||||
| import android.hardware.SensorEventListener | ||||
| import android.hardware.SensorManager | ||||
| import android.os.Bundle | ||||
| import android.view.LayoutInflater | ||||
| import android.view.Menu | ||||
| import android.view.MenuInflater | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import android.widget.CheckBox | ||||
| import android.widget.CompoundButton | ||||
| import android.widget.ImageView | ||||
| import android.widget.TextView | ||||
| import android.widget.Toast | ||||
| import androidx.activity.result.ActivityResultCallback | ||||
| import androidx.activity.result.contract.ActivityResultContracts | ||||
| import androidx.fragment.app.Fragment | ||||
| import androidx.fragment.app.FragmentManager | ||||
| import androidx.lifecycle.Observer | ||||
| import androidx.paging.PagedList | ||||
| import androidx.work.WorkInfo | ||||
| import androidx.work.WorkManager | ||||
| import fr.free.nrw.commons.MapController.NearbyPlacesInfo | ||||
| import fr.free.nrw.commons.Media | ||||
| import fr.free.nrw.commons.R | ||||
| import fr.free.nrw.commons.Utils | ||||
| import fr.free.nrw.commons.auth.SessionManager | ||||
| import fr.free.nrw.commons.campaigns.CampaignView | ||||
| import fr.free.nrw.commons.campaigns.CampaignsPresenter | ||||
| import fr.free.nrw.commons.campaigns.ICampaignsView | ||||
| import fr.free.nrw.commons.campaigns.models.Campaign | ||||
| import fr.free.nrw.commons.contributions.MainActivity.ActiveFragment | ||||
| import fr.free.nrw.commons.databinding.FragmentContributionsBinding | ||||
| import fr.free.nrw.commons.di.CommonsDaggerSupportFragment | ||||
| import fr.free.nrw.commons.kvstore.JsonKvStore | ||||
| import fr.free.nrw.commons.location.LatLng | ||||
| import fr.free.nrw.commons.location.LocationServiceManager | ||||
| import fr.free.nrw.commons.location.LocationUpdateListener | ||||
| import fr.free.nrw.commons.media.MediaDetailPagerFragment | ||||
| import fr.free.nrw.commons.media.MediaDetailPagerFragment.MediaDetailProvider | ||||
| import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient | ||||
| import fr.free.nrw.commons.nearby.NearbyController | ||||
| import fr.free.nrw.commons.nearby.NearbyNotificationCardView | ||||
| import fr.free.nrw.commons.nearby.Place | ||||
| import fr.free.nrw.commons.nearby.fragments.NearbyParentFragment | ||||
| import fr.free.nrw.commons.notification.NotificationActivity.Companion.startYourself | ||||
| import fr.free.nrw.commons.notification.NotificationController | ||||
| import fr.free.nrw.commons.notification.models.Notification | ||||
| import fr.free.nrw.commons.profile.ProfileActivity | ||||
| import fr.free.nrw.commons.theme.BaseActivity | ||||
| import fr.free.nrw.commons.upload.UploadProgressActivity | ||||
| import fr.free.nrw.commons.upload.worker.UploadWorker | ||||
| import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour | ||||
| import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog | ||||
| import fr.free.nrw.commons.utils.LengthUtils.computeBearing | ||||
| import fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween | ||||
| import fr.free.nrw.commons.utils.NetworkUtils.isInternetConnectionEstablished | ||||
| import fr.free.nrw.commons.utils.PermissionUtils.hasPermission | ||||
| import fr.free.nrw.commons.utils.ViewUtil.showLongToast | ||||
| import io.reactivex.Observable | ||||
| import io.reactivex.android.schedulers.AndroidSchedulers | ||||
| import io.reactivex.disposables.CompositeDisposable | ||||
| import io.reactivex.schedulers.Schedulers | ||||
| import timber.log.Timber | ||||
| import java.util.Calendar | ||||
| import java.util.Date | ||||
| import javax.inject.Inject | ||||
| import javax.inject.Named | ||||
| 
 | ||||
| class ContributionsFragment | ||||
| 
 | ||||
|     : CommonsDaggerSupportFragment(), FragmentManager.OnBackStackChangedListener, | ||||
|     LocationUpdateListener, MediaDetailProvider, SensorEventListener, ICampaignsView, | ||||
|     ContributionsContract.View, | ||||
|     ContributionsListFragment.Callback { | ||||
|     @JvmField | ||||
|     @Inject | ||||
|     @Named("default_preferences") | ||||
|     var store: JsonKvStore? = null | ||||
| 
 | ||||
|     @JvmField | ||||
|     @Inject | ||||
|     var nearbyController: NearbyController? = null | ||||
| 
 | ||||
|     @JvmField | ||||
|     @Inject | ||||
|     var okHttpJsonApiClient: OkHttpJsonApiClient? = null | ||||
| 
 | ||||
|     @JvmField | ||||
|     @Inject | ||||
|     var presenter: CampaignsPresenter? = null | ||||
| 
 | ||||
|     @JvmField | ||||
|     @Inject | ||||
|     var locationManager: LocationServiceManager? = null | ||||
| 
 | ||||
|     @JvmField | ||||
|     @Inject | ||||
|     var notificationController: NotificationController? = null | ||||
| 
 | ||||
|     @JvmField | ||||
|     @Inject | ||||
|     var contributionController: ContributionController? = null | ||||
| 
 | ||||
|     override var compositeDisposable: CompositeDisposable = CompositeDisposable() | ||||
| 
 | ||||
|     private var contributionsListFragment: ContributionsListFragment? = null | ||||
| 
 | ||||
|     // Getter for mediaDetailPagerFragment | ||||
|     var mediaDetailPagerFragment: MediaDetailPagerFragment? = null | ||||
|         private set | ||||
|     var binding: FragmentContributionsBinding? = null | ||||
| 
 | ||||
|     @JvmField | ||||
|     @Inject | ||||
|     var contributionsPresenter: ContributionsPresenter? = null | ||||
| 
 | ||||
|     @JvmField | ||||
|     @Inject | ||||
|     var sessionManager: SessionManager? = null | ||||
| 
 | ||||
|     private var currentLatLng: LatLng? = null | ||||
| 
 | ||||
|     private var isFragmentAttachedBefore = false | ||||
|     private var checkBoxView: View? = null | ||||
|     private var checkBox: CheckBox? = null | ||||
| 
 | ||||
|     var notificationCount: TextView? = null | ||||
| 
 | ||||
|     var pendingUploadsCountTextView: TextView? = null | ||||
| 
 | ||||
|     var uploadsErrorTextView: TextView? = null | ||||
| 
 | ||||
|     var pendingUploadsImageView: ImageView? = null | ||||
| 
 | ||||
|     private var wlmCampaign: Campaign? = null | ||||
| 
 | ||||
|     var userName: String? = null | ||||
|     private var isUserProfile = false | ||||
| 
 | ||||
|     private var mSensorManager: SensorManager? = null | ||||
|     private var mLight: Sensor? = null | ||||
|     private var direction = 0f | ||||
|     private val nearbyLocationPermissionLauncher = | ||||
|         registerForActivityResult<Array<String>, Map<String, Boolean>>( | ||||
|             ActivityResultContracts.RequestMultiplePermissions(), | ||||
|             object : ActivityResultCallback<Map<String, Boolean>> { | ||||
|                 override fun onActivityResult(result: Map<String, Boolean>) { | ||||
|                     var areAllGranted = true | ||||
|                     for (b in result.values) { | ||||
|                         areAllGranted = areAllGranted && b | ||||
|                     } | ||||
| 
 | ||||
|                     if (areAllGranted) { | ||||
|                         onLocationPermissionGranted() | ||||
|                     } else { | ||||
|                         if (shouldShowRequestPermissionRationale( | ||||
|                                 permission.ACCESS_FINE_LOCATION | ||||
|                             ) | ||||
|                             && store!!.getBoolean("displayLocationPermissionForCardView", true) | ||||
|                             && !store!!.getBoolean("doNotAskForLocationPermission", false) | ||||
|                             && ((activity as MainActivity).activeFragment | ||||
|                                     == ActiveFragment.CONTRIBUTIONS) | ||||
|                         ) { | ||||
|                             binding!!.cardViewNearby.permissionType = | ||||
|                                 NearbyNotificationCardView.PermissionType.ENABLE_LOCATION_PERMISSION | ||||
|                         } else { | ||||
|                             displayYouWontSeeNearbyMessage() | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             }) | ||||
| 
 | ||||
|     private var shouldShowMediaDetailsFragment = false | ||||
| 
 | ||||
|     override fun onCreate(savedInstanceState: Bundle?) { | ||||
|         super.onCreate(savedInstanceState) | ||||
|         if (arguments != null && requireArguments().getString(ProfileActivity.KEY_USERNAME) != null) { | ||||
|             userName = requireArguments().getString(ProfileActivity.KEY_USERNAME) | ||||
|             isUserProfile = true | ||||
|         } | ||||
|         mSensorManager = requireActivity().getSystemService(Context.SENSOR_SERVICE) as SensorManager | ||||
|         mLight = mSensorManager!!.getDefaultSensor(Sensor.TYPE_ORIENTATION) | ||||
|     } | ||||
| 
 | ||||
|     override fun onCreateView( | ||||
|         inflater: LayoutInflater, container: ViewGroup?, | ||||
|         savedInstanceState: Bundle? | ||||
|     ): View? { | ||||
|         binding = FragmentContributionsBinding.inflate(inflater, container, false) | ||||
| 
 | ||||
|         initWLMCampaign() | ||||
|         presenter!!.onAttachView(this) | ||||
|         contributionsPresenter!!.onAttachView(this) | ||||
|         binding!!.campaignsView.visibility = View.GONE | ||||
|         checkBoxView = View.inflate(activity, R.layout.nearby_permission_dialog, null) | ||||
|         checkBox = checkBoxView?.findViewById<View>(R.id.never_ask_again) as CheckBox | ||||
|         checkBox!!.setOnCheckedChangeListener { buttonView: CompoundButton?, isChecked: Boolean -> | ||||
|             if (isChecked) { | ||||
|                 // Do not ask for permission on activity start again | ||||
|                 store!!.putBoolean("displayLocationPermissionForCardView", false) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (savedInstanceState != null) { | ||||
|             mediaDetailPagerFragment = childFragmentManager | ||||
|                 .findFragmentByTag(MEDIA_DETAIL_PAGER_FRAGMENT_TAG) as MediaDetailPagerFragment? | ||||
|             contributionsListFragment = childFragmentManager | ||||
|                 .findFragmentByTag(CONTRIBUTION_LIST_FRAGMENT_TAG) as ContributionsListFragment? | ||||
|             shouldShowMediaDetailsFragment = savedInstanceState.getBoolean("mediaDetailsVisible") | ||||
|         } | ||||
| 
 | ||||
|         initFragments() | ||||
|         if (!isUserProfile) { | ||||
|             upDateUploadCount() | ||||
|         } | ||||
|         if (shouldShowMediaDetailsFragment) { | ||||
|             showMediaDetailPagerFragment() | ||||
|         } else { | ||||
|             if (mediaDetailPagerFragment != null) { | ||||
|                 removeFragment(mediaDetailPagerFragment!!) | ||||
|             } | ||||
|             showContributionsListFragment() | ||||
|         } | ||||
| 
 | ||||
|         if (!isBetaFlavour && sessionManager!!.isUserLoggedIn | ||||
|             && sessionManager!!.currentAccount != null && !isUserProfile | ||||
|         ) { | ||||
|             setUploadCount() | ||||
|         } | ||||
|         setHasOptionsMenu(true) | ||||
|         return binding!!.root | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Initialise the campaign object for WML | ||||
|      */ | ||||
|     private fun initWLMCampaign() { | ||||
|         wlmCampaign = Campaign( | ||||
|             getString(R.string.wlm_campaign_title), | ||||
|             getString(R.string.wlm_campaign_description), Utils.getWLMStartDate().toString(), | ||||
|             Utils.getWLMEndDate().toString(), NearbyParentFragment.WLM_URL, true | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     override fun onCreateOptionsMenu( | ||||
|         menu: Menu, | ||||
|         inflater: MenuInflater | ||||
|     ) { | ||||
|         // Removing contributions menu items for ProfileActivity | ||||
| 
 | ||||
|         if (activity is ProfileActivity) { | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
|         inflater.inflate(R.menu.contribution_activity_notification_menu, menu) | ||||
| 
 | ||||
|         val notificationsMenuItem = menu.findItem(R.id.notifications) | ||||
|         val notification = notificationsMenuItem.actionView | ||||
|         notificationCount = notification!!.findViewById(R.id.notification_count_badge) | ||||
|         val uploadMenuItem = menu.findItem(R.id.upload_tab) | ||||
|         val uploadMenuItemActionView = uploadMenuItem.actionView | ||||
|         pendingUploadsCountTextView = uploadMenuItemActionView!!.findViewById( | ||||
|             R.id.pending_uploads_count_badge | ||||
|         ) | ||||
|         uploadsErrorTextView = uploadMenuItemActionView.findViewById( | ||||
|             R.id.uploads_error_count_badge | ||||
|         ) | ||||
|         pendingUploadsImageView = uploadMenuItemActionView.findViewById( | ||||
|             R.id.pending_uploads_image_view | ||||
|         ) | ||||
|         if (pendingUploadsImageView != null) { | ||||
|             pendingUploadsImageView!!.setOnClickListener { view: View? -> | ||||
|                 startActivity( | ||||
|                     Intent( | ||||
|                         context, | ||||
|                         UploadProgressActivity::class.java | ||||
|                     ) | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|         if (pendingUploadsCountTextView != null) { | ||||
|             pendingUploadsCountTextView!!.setOnClickListener { view: View? -> | ||||
|                 startActivity( | ||||
|                     Intent( | ||||
|                         context, | ||||
|                         UploadProgressActivity::class.java | ||||
|                     ) | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|         if (uploadsErrorTextView != null) { | ||||
|             uploadsErrorTextView!!.setOnClickListener { view: View? -> | ||||
|                 startActivity( | ||||
|                     Intent( | ||||
|                         context, | ||||
|                         UploadProgressActivity::class.java | ||||
|                     ) | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|         notification.setOnClickListener { view: View? -> | ||||
|             startYourself( | ||||
|                 context, "unread" | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @SuppressLint("CheckResult") | ||||
|     fun setNotificationCount() { | ||||
|         compositeDisposable.add( | ||||
|             notificationController!!.getNotifications(false) | ||||
|                 .subscribeOn(Schedulers.io()) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe( | ||||
|                     { notificationList: List<Notification> -> | ||||
|                         this.initNotificationViews( | ||||
|                             notificationList | ||||
|                         ) | ||||
|                     }, | ||||
|                     { throwable: Throwable? -> | ||||
|                         Timber.e( | ||||
|                             throwable, | ||||
|                             "Error occurred while loading notifications" | ||||
|                         ) | ||||
|                     }) | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Temporarily disabled, see issue [https://github.com/commons-app/apps-android-commons/issues/5847] | ||||
|      * Sets the visibility of the upload icon based on the number of failed and pending | ||||
|      * contributions. | ||||
|      */ | ||||
|     //    public void setUploadIconVisibility() { | ||||
|     //        contributionController.getFailedAndPendingContributions(); | ||||
|     //        contributionController.failedAndPendingContributionList.observe(getViewLifecycleOwner(), | ||||
|     //            list -> { | ||||
|     //                updateUploadIcon(list.size()); | ||||
|     //            }); | ||||
|     //    } | ||||
|     /** | ||||
|      * Sets the count for the upload icon based on the number of pending and failed contributions. | ||||
|      */ | ||||
|     fun setUploadIconCount() { | ||||
|         contributionController!!.pendingContributions | ||||
|         contributionController!!.pendingContributionList!!.observe( | ||||
|             viewLifecycleOwner, | ||||
|             Observer<PagedList<Contribution>> { list: PagedList<Contribution> -> | ||||
|                 updatePendingIcon(list.size) | ||||
|             }) | ||||
|         contributionController!!.failedContributions | ||||
|         contributionController!!.failedContributionList!!.observe( | ||||
|             viewLifecycleOwner, | ||||
|             Observer<PagedList<Contribution>> { list: PagedList<Contribution> -> | ||||
|                 updateErrorIcon(list.size) | ||||
|             }) | ||||
|     } | ||||
| 
 | ||||
|     fun scrollToTop() { | ||||
|         if (contributionsListFragment != null) { | ||||
|             contributionsListFragment!!.scrollToTop() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun initNotificationViews(notificationList: List<Notification>) { | ||||
|         Timber.d("Number of notifications is %d", notificationList.size) | ||||
|         if (notificationList.isEmpty()) { | ||||
|             notificationCount!!.visibility = View.GONE | ||||
|         } else { | ||||
|             notificationCount!!.visibility = View.VISIBLE | ||||
|             notificationCount!!.text = notificationList.size.toString() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun onAttach(context: Context) { | ||||
|         super.onAttach(context) | ||||
|         /* | ||||
|         - There are some operations we need auth, so we need to make sure isAuthCookieAcquired. | ||||
|         - And since we use same retained fragment doesn't want to make all network operations | ||||
|         all over again on same fragment attached to recreated activity, we do this network | ||||
|         operations on first time fragment attached to an activity. Then they will be retained | ||||
|         until fragment life time ends. | ||||
|          */ | ||||
|         if (!isFragmentAttachedBefore && activity != null) { | ||||
|             isFragmentAttachedBefore = true | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Replace FrameLayout with ContributionsListFragment, user will see contributions list. Creates | ||||
|      * new one if null. | ||||
|      */ | ||||
|     private fun showContributionsListFragment() { | ||||
|         // show nearby card view on contributions list is visible | ||||
|         if (binding!!.cardViewNearby != null && !isUserProfile) { | ||||
|             if (store!!.getBoolean("displayNearbyCardView", true)) { | ||||
|                 if (binding!!.cardViewNearby.cardViewVisibilityState | ||||
|                     == NearbyNotificationCardView.CardViewVisibilityState.READY | ||||
|                 ) { | ||||
|                     binding!!.cardViewNearby.visibility = View.VISIBLE | ||||
|                 } | ||||
|             } else { | ||||
|                 binding!!.cardViewNearby.visibility = View.GONE | ||||
|             } | ||||
|         } | ||||
|         showFragment( | ||||
|             contributionsListFragment!!, CONTRIBUTION_LIST_FRAGMENT_TAG, | ||||
|             mediaDetailPagerFragment | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     private fun showMediaDetailPagerFragment() { | ||||
|         // hide nearby card view on media detail is visible | ||||
|         setupViewForMediaDetails() | ||||
|         showFragment( | ||||
|             mediaDetailPagerFragment!!, MEDIA_DETAIL_PAGER_FRAGMENT_TAG, | ||||
|             contributionsListFragment | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     private fun setupViewForMediaDetails() { | ||||
|         if (binding != null) { | ||||
|             binding!!.campaignsView.visibility = View.GONE | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun onBackStackChanged() { | ||||
|         fetchCampaigns() | ||||
|     } | ||||
| 
 | ||||
|     private fun initFragments() { | ||||
|         if (null == contributionsListFragment) { | ||||
|             contributionsListFragment = ContributionsListFragment() | ||||
|             val contributionsListBundle = Bundle() | ||||
|             contributionsListBundle.putString(ProfileActivity.KEY_USERNAME, userName) | ||||
|             contributionsListFragment!!.arguments = contributionsListBundle | ||||
|         } | ||||
| 
 | ||||
|         if (shouldShowMediaDetailsFragment) { | ||||
|             showMediaDetailPagerFragment() | ||||
|         } else { | ||||
|             showContributionsListFragment() | ||||
|         } | ||||
| 
 | ||||
|         showFragment( | ||||
|             contributionsListFragment!!, CONTRIBUTION_LIST_FRAGMENT_TAG, | ||||
|             mediaDetailPagerFragment | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Replaces the root frame layout with the given fragment | ||||
|      * | ||||
|      * @param fragment | ||||
|      * @param tag | ||||
|      * @param otherFragment | ||||
|      */ | ||||
|     private fun showFragment(fragment: Fragment, tag: String, otherFragment: Fragment?) { | ||||
|         val transaction = childFragmentManager.beginTransaction() | ||||
|         if (fragment.isAdded && otherFragment != null) { | ||||
|             transaction.hide(otherFragment) | ||||
|             transaction.show(fragment) | ||||
|             transaction.addToBackStack(tag) | ||||
|             transaction.commit() | ||||
|             childFragmentManager.executePendingTransactions() | ||||
|         } else if (fragment.isAdded && otherFragment == null) { | ||||
|             transaction.show(fragment) | ||||
|             transaction.addToBackStack(tag) | ||||
|             transaction.commit() | ||||
|             childFragmentManager.executePendingTransactions() | ||||
|         } else if (!fragment.isAdded && otherFragment != null) { | ||||
|             transaction.hide(otherFragment) | ||||
|             transaction.add(R.id.root_frame, fragment, tag) | ||||
|             transaction.addToBackStack(tag) | ||||
|             transaction.commit() | ||||
|             childFragmentManager.executePendingTransactions() | ||||
|         } else if (!fragment.isAdded) { | ||||
|             transaction.replace(R.id.root_frame, fragment, tag) | ||||
|             transaction.addToBackStack(tag) | ||||
|             transaction.commit() | ||||
|             childFragmentManager.executePendingTransactions() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fun removeFragment(fragment: Fragment) { | ||||
|         childFragmentManager | ||||
|             .beginTransaction() | ||||
|             .remove(fragment) | ||||
|             .commit() | ||||
|         childFragmentManager.executePendingTransactions() | ||||
|     } | ||||
| 
 | ||||
|     private fun setUploadCount() { | ||||
|         okHttpJsonApiClient | ||||
|             ?.getUploadCount((activity as MainActivity).sessionManager?.currentAccount!!.name) | ||||
|             ?.subscribeOn(Schedulers.io()) | ||||
|             ?.observeOn(AndroidSchedulers.mainThread())?.let { | ||||
|                 compositeDisposable.add( | ||||
|                 it | ||||
|                     .subscribe( | ||||
|                         { uploadCount: Int -> this.displayUploadCount(uploadCount) }, | ||||
|                         { t: Throwable? -> Timber.e(t, "Fetching upload count failed") } | ||||
|                     )) | ||||
|             } | ||||
|     } | ||||
| 
 | ||||
|     private fun displayUploadCount(uploadCount: Int) { | ||||
|         if (requireActivity().isFinishing | ||||
|             || resources == null | ||||
|         ) { | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
|         (activity as MainActivity).setNumOfUploads(uploadCount) | ||||
|     } | ||||
| 
 | ||||
|     override fun onPause() { | ||||
|         super.onPause() | ||||
|         locationManager!!.removeLocationListener(this) | ||||
|         locationManager!!.unregisterLocationManager() | ||||
|         mSensorManager!!.unregisterListener(this) | ||||
|     } | ||||
| 
 | ||||
|     override fun onSaveInstanceState(outState: Bundle) { | ||||
|         super.onSaveInstanceState(outState) | ||||
|     } | ||||
| 
 | ||||
|     override fun onResume() { | ||||
|         super.onResume() | ||||
|         contributionsPresenter!!.onAttachView(this) | ||||
|         locationManager!!.addLocationListener(this) | ||||
| 
 | ||||
|         if (binding == null) { | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
|         binding!!.cardViewNearby.permissionRequestButton.setOnClickListener { v: View? -> | ||||
|             showNearbyCardPermissionRationale() | ||||
|         } | ||||
| 
 | ||||
|         // Notification cards should only be seen on contributions list, not in media details | ||||
|         if (mediaDetailPagerFragment == null && !isUserProfile) { | ||||
|             if (store!!.getBoolean("displayNearbyCardView", true)) { | ||||
|                 checkPermissionsAndShowNearbyCardView() | ||||
| 
 | ||||
|                 // Calling nearby card to keep showing it even when user clicks on it and comes back | ||||
|                 try { | ||||
|                     updateClosestNearbyCardViewInfo() | ||||
|                 } catch (e: Exception) { | ||||
|                     Timber.e(e) | ||||
|                 } | ||||
|                 if (binding!!.cardViewNearby.cardViewVisibilityState | ||||
|                     == NearbyNotificationCardView.CardViewVisibilityState.READY | ||||
|                 ) { | ||||
|                     binding!!.cardViewNearby.visibility = View.VISIBLE | ||||
|                 } | ||||
|             } else { | ||||
|                 // Hide nearby notification card view if related shared preferences is false | ||||
|                 binding!!.cardViewNearby.visibility = View.GONE | ||||
|             } | ||||
| 
 | ||||
|             // Notification Count and Campaigns should not be set, if it is used in User Profile | ||||
|             if (!isUserProfile) { | ||||
|                 setNotificationCount() | ||||
|                 fetchCampaigns() | ||||
|                 // Temporarily disabled, see issue [https://github.com/commons-app/apps-android-commons/issues/5847] | ||||
|                 // setUploadIconVisibility(); | ||||
|                 setUploadIconCount() | ||||
|             } | ||||
|         } | ||||
|         mSensorManager!!.registerListener(this, mLight, SensorManager.SENSOR_DELAY_UI) | ||||
|     } | ||||
| 
 | ||||
|     private fun checkPermissionsAndShowNearbyCardView() { | ||||
|         if (hasPermission( | ||||
|                 requireActivity(), | ||||
|                 arrayOf(permission.ACCESS_FINE_LOCATION) | ||||
|             ) | ||||
|         ) { | ||||
|             onLocationPermissionGranted() | ||||
|         } else if (shouldShowRequestPermissionRationale(permission.ACCESS_FINE_LOCATION) | ||||
|             && store!!.getBoolean("displayLocationPermissionForCardView", true) | ||||
|             && !store!!.getBoolean("doNotAskForLocationPermission", false) | ||||
|             && ((activity as MainActivity).activeFragment == ActiveFragment.CONTRIBUTIONS) | ||||
|         ) { | ||||
|             binding!!.cardViewNearby.permissionType = | ||||
|                 NearbyNotificationCardView.PermissionType.ENABLE_LOCATION_PERMISSION | ||||
|             showNearbyCardPermissionRationale() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun requestLocationPermission() { | ||||
|         nearbyLocationPermissionLauncher.launch(arrayOf(permission.ACCESS_FINE_LOCATION)) | ||||
|     } | ||||
| 
 | ||||
|     private fun onLocationPermissionGranted() { | ||||
|         binding!!.cardViewNearby.permissionType = | ||||
|             NearbyNotificationCardView.PermissionType.NO_PERMISSION_NEEDED | ||||
|         locationManager!!.registerLocationManager() | ||||
|     } | ||||
| 
 | ||||
|     private fun showNearbyCardPermissionRationale() { | ||||
|         showAlertDialog( | ||||
|             requireActivity(), | ||||
|             getString(R.string.nearby_card_permission_title), | ||||
|             getString(R.string.nearby_card_permission_explanation), | ||||
|             { this.requestLocationPermission() }, | ||||
|             { this.displayYouWontSeeNearbyMessage() }, | ||||
|             checkBoxView | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     private fun displayYouWontSeeNearbyMessage() { | ||||
|         showLongToast( | ||||
|             requireActivity(), | ||||
|             resources.getString(R.string.unable_to_display_nearest_place) | ||||
|         ) | ||||
|         // Set to true as the user doesn't want the app to ask for location permission anymore | ||||
|         store!!.putBoolean("doNotAskForLocationPermission", true) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     private fun updateClosestNearbyCardViewInfo() { | ||||
|         currentLatLng = locationManager!!.getLastLocation() | ||||
|         compositeDisposable.add(Observable.fromCallable { | ||||
|             nearbyController?.loadAttractionsFromLocation( | ||||
|                     currentLatLng, currentLatLng, true, | ||||
|                     false | ||||
|                 ) | ||||
|         } // thanks to boolean, it will only return closest result | ||||
|             .subscribeOn(Schedulers.io()) | ||||
|             .observeOn(AndroidSchedulers.mainThread()) | ||||
|             .subscribe( | ||||
|                 { nearbyPlacesInfo: NearbyPlacesInfo? -> | ||||
|                     this.updateNearbyNotification( | ||||
|                         nearbyPlacesInfo | ||||
|                     ) | ||||
|                 }, | ||||
|                 { throwable: Throwable? -> | ||||
|                     Timber.d(throwable) | ||||
|                     updateNearbyNotification(null) | ||||
|                 }) | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     private fun updateNearbyNotification( | ||||
|         nearbyPlacesInfo: NearbyPlacesInfo? | ||||
|     ) { | ||||
|         if (nearbyPlacesInfo?.placeList != null && nearbyPlacesInfo.placeList.size > 0) { | ||||
|             var closestNearbyPlace: Place? = null | ||||
|             // Find the first nearby place that has no image and exists | ||||
|             for (place in nearbyPlacesInfo.placeList) { | ||||
|                 if (place.pic == "" && place.exists) { | ||||
|                     closestNearbyPlace = place | ||||
|                     break | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             if (closestNearbyPlace == null) { | ||||
|                 binding!!.cardViewNearby.visibility = View.GONE | ||||
|             } else { | ||||
|                 val distance = formatDistanceBetween(currentLatLng, closestNearbyPlace.location) | ||||
|                 closestNearbyPlace.setDistance(distance) | ||||
|                 direction = computeBearing(currentLatLng!!, closestNearbyPlace.location).toFloat() | ||||
|                 binding!!.cardViewNearby.updateContent(closestNearbyPlace) | ||||
|             } | ||||
|         } else { | ||||
|             // Means that no close nearby place is found | ||||
|             binding!!.cardViewNearby.visibility = View.GONE | ||||
|         } | ||||
| 
 | ||||
|         // Prevent Nearby banner from appearing in Media Details, fixing bug https://github.com/commons-app/apps-android-commons/issues/4731 | ||||
|         if (mediaDetailPagerFragment != null && !contributionsListFragment!!.isVisible) { | ||||
|             binding!!.cardViewNearby.visibility = View.GONE | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun onDestroy() { | ||||
|         try { | ||||
|             compositeDisposable.clear() | ||||
|             childFragmentManager.removeOnBackStackChangedListener(this) | ||||
|             locationManager!!.unregisterLocationManager() | ||||
|             locationManager!!.removeLocationListener(this) | ||||
|             super.onDestroy() | ||||
|         } catch (exception: IllegalArgumentException) { | ||||
|             Timber.e(exception) | ||||
|         } catch (exception: IllegalStateException) { | ||||
|             Timber.e(exception) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun onLocationChangedSignificantly(latLng: LatLng) { | ||||
|         // Will be called if location changed more than 1000 meter | ||||
|         updateClosestNearbyCardViewInfo() | ||||
|     } | ||||
| 
 | ||||
|     override fun onLocationChangedSlightly(latLng: LatLng) { | ||||
|         /* Update closest nearby notification card onLocationChangedSlightly | ||||
|          */ | ||||
|         try { | ||||
|             updateClosestNearbyCardViewInfo() | ||||
|         } catch (e: Exception) { | ||||
|             Timber.e(e) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun onLocationChangedMedium(latLng: LatLng) { | ||||
|         // Update closest nearby card view if location changed more than 500 meters | ||||
|         updateClosestNearbyCardViewInfo() | ||||
|     } | ||||
| 
 | ||||
|     override fun onViewCreated( | ||||
|         view: View, | ||||
|         savedInstanceState: Bundle? | ||||
|     ) { | ||||
|         super.onViewCreated(view, savedInstanceState) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * As the home screen has limited space, we have choosen to show either campaigns or WLM card. | ||||
|      * The WLM Card gets the priority over monuments, so if the WLM is going on we show that instead | ||||
|      * of campaigns on the campaigns card | ||||
|      */ | ||||
|     private fun fetchCampaigns() { | ||||
|         if (Utils.isMonumentsEnabled(Date())) { | ||||
|             if (binding != null) { | ||||
|                 binding!!.campaignsView.setCampaign(wlmCampaign) | ||||
|                 binding!!.campaignsView.visibility = View.VISIBLE | ||||
|             } | ||||
|         } else if (store!!.getBoolean(CampaignView.CAMPAIGNS_DEFAULT_PREFERENCE, true)) { | ||||
|             presenter!!.getCampaigns() | ||||
|         } else { | ||||
|             if (binding != null) { | ||||
|                 binding!!.campaignsView.visibility = View.GONE | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun showMessage(message: String) { | ||||
|         Toast.makeText(context, message, Toast.LENGTH_SHORT).show() | ||||
|     } | ||||
| 
 | ||||
|     override fun showCampaigns(campaign: Campaign?) { | ||||
|         if (campaign != null && !isUserProfile) { | ||||
|             if (binding != null) { | ||||
|                 binding!!.campaignsView.setCampaign(campaign) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun onDestroyView() { | ||||
|         super.onDestroyView() | ||||
|         presenter!!.onDetachView() | ||||
|     } | ||||
| 
 | ||||
|     override fun notifyDataSetChanged() { | ||||
|         if (mediaDetailPagerFragment != null) { | ||||
|             mediaDetailPagerFragment!!.notifyDataSetChanged() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Notify the viewpager that number of items have changed. | ||||
|      */ | ||||
|     override fun viewPagerNotifyDataSetChanged() { | ||||
|         if (mediaDetailPagerFragment != null) { | ||||
|             mediaDetailPagerFragment!!.notifyDataSetChanged() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Updates the visibility and text of the pending uploads count TextView based on the given | ||||
|      * count. | ||||
|      * | ||||
|      * @param pendingCount The number of pending uploads. | ||||
|      */ | ||||
|     fun updatePendingIcon(pendingCount: Int) { | ||||
|         if (pendingUploadsCountTextView != null) { | ||||
|             if (pendingCount != 0) { | ||||
|                 pendingUploadsCountTextView!!.visibility = View.VISIBLE | ||||
|                 pendingUploadsCountTextView!!.text = pendingCount.toString() | ||||
|             } else { | ||||
|                 pendingUploadsCountTextView!!.visibility = View.INVISIBLE | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Updates the visibility and text of the error uploads TextView based on the given count. | ||||
|      * | ||||
|      * @param errorCount The number of error uploads. | ||||
|      */ | ||||
|     fun updateErrorIcon(errorCount: Int) { | ||||
|         if (uploadsErrorTextView != null) { | ||||
|             if (errorCount != 0) { | ||||
|                 uploadsErrorTextView!!.visibility = View.VISIBLE | ||||
|                 uploadsErrorTextView!!.text = errorCount.toString() | ||||
|             } else { | ||||
|                 uploadsErrorTextView!!.visibility = View.GONE | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Temporarily disabled, see issue [https://github.com/commons-app/apps-android-commons/issues/5847] | ||||
|      * @param count The number of pending uploads. | ||||
|      */ | ||||
|     //    public void updateUploadIcon(int count) { | ||||
|     //        if (pendingUploadsImageView != null) { | ||||
|     //            if (count != 0) { | ||||
|     //                pendingUploadsImageView.setVisibility(View.VISIBLE); | ||||
|     //            } else { | ||||
|     //                pendingUploadsImageView.setVisibility(View.GONE); | ||||
|     //            } | ||||
|     //        } | ||||
|     //    } | ||||
|     /** | ||||
|      * Replace whatever is in the current contributionsFragmentContainer view with | ||||
|      * mediaDetailPagerFragment, and preserve previous state in back stack. Called when user selects | ||||
|      * a contribution. | ||||
|      */ | ||||
|     override fun showDetail(position: Int, isWikipediaButtonDisplayed: Boolean) { | ||||
|         if (mediaDetailPagerFragment == null || !mediaDetailPagerFragment!!.isVisible) { | ||||
|             mediaDetailPagerFragment = MediaDetailPagerFragment.newInstance(false, true) | ||||
|             if (isUserProfile) { | ||||
|                 (activity as ProfileActivity).setScroll(false) | ||||
|             } | ||||
|             showMediaDetailPagerFragment() | ||||
|         } | ||||
|         mediaDetailPagerFragment!!.showImage(position, isWikipediaButtonDisplayed) | ||||
|     } | ||||
| 
 | ||||
|     override fun getMediaAtPosition(i: Int): Media? { | ||||
|         return contributionsListFragment!!.getMediaAtPosition(i) | ||||
|     } | ||||
| 
 | ||||
|     override fun getTotalMediaCount(): Int { | ||||
|         return contributionsListFragment!!.totalMediaCount | ||||
|     } | ||||
| 
 | ||||
|     override fun getContributionStateAt(position: Int): Int { | ||||
|         return contributionsListFragment!!.getContributionStateAt(position) | ||||
|     } | ||||
| 
 | ||||
|     fun backButtonClicked(): Boolean { | ||||
|         if (mediaDetailPagerFragment != null && mediaDetailPagerFragment!!.isVisible) { | ||||
|             if (store!!.getBoolean("displayNearbyCardView", true) && !isUserProfile) { | ||||
|                 if (binding!!.cardViewNearby.cardViewVisibilityState | ||||
|                     == NearbyNotificationCardView.CardViewVisibilityState.READY | ||||
|                 ) { | ||||
|                     binding!!.cardViewNearby.visibility = View.VISIBLE | ||||
|                 } | ||||
|             } else { | ||||
|                 binding!!.cardViewNearby.visibility = View.GONE | ||||
|             } | ||||
|             removeFragment(mediaDetailPagerFragment!!) | ||||
|             showFragment( | ||||
|                 contributionsListFragment!!, CONTRIBUTION_LIST_FRAGMENT_TAG, | ||||
|                 mediaDetailPagerFragment | ||||
|             ) | ||||
|             if (isUserProfile) { | ||||
|                 // Fragment is associated with ProfileActivity | ||||
|                 // Enable ParentViewPager Scroll | ||||
|                 (activity as ProfileActivity).setScroll(true) | ||||
|             } else { | ||||
|                 fetchCampaigns() | ||||
|             } | ||||
|             if (activity is MainActivity) { | ||||
|                 // Fragment is associated with MainActivity | ||||
|                 (activity as BaseActivity).supportActionBar | ||||
|                     ?.setDisplayHomeAsUpEnabled(false) | ||||
|                 (activity as MainActivity).showTabs() | ||||
|             } | ||||
|             return true | ||||
|         } | ||||
|         return false | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * this function updates the number of contributions | ||||
|      */ | ||||
|     fun upDateUploadCount() { | ||||
|         WorkManager.getInstance(context) | ||||
|             .getWorkInfosForUniqueWorkLiveData(UploadWorker::class.java.simpleName).observe( | ||||
|                 viewLifecycleOwner | ||||
|             ) { workInfos: List<WorkInfo?> -> | ||||
|                 if (workInfos.size > 0) { | ||||
|                     setUploadCount() | ||||
|                 } | ||||
|             } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * Restarts the upload process for a contribution | ||||
|      * | ||||
|      * @param contribution | ||||
|      */ | ||||
|     fun restartUpload(contribution: Contribution) { | ||||
|         contribution.dateUploadStarted = Calendar.getInstance().time | ||||
|         if (contribution.state == Contribution.STATE_FAILED) { | ||||
|             if (contribution.errorInfo == null) { | ||||
|                 contribution.chunkInfo = null | ||||
|                 contribution.transferred = 0 | ||||
|             } | ||||
|             contributionsPresenter!!.checkDuplicateImageAndRestartContribution(contribution) | ||||
|         } else { | ||||
|             contribution.state = Contribution.STATE_QUEUED | ||||
|             contributionsPresenter!!.saveContribution(contribution) | ||||
|             Timber.d("Restarting for %s", contribution.toString()) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Retry upload when it is failed | ||||
|      * | ||||
|      * @param contribution contribution to be retried | ||||
|      */ | ||||
|     fun retryUpload(contribution: Contribution) { | ||||
|         if (isInternetConnectionEstablished(context)) { | ||||
|             if (contribution.state == Contribution.STATE_PAUSED) { | ||||
|                 restartUpload(contribution) | ||||
|             } else if (contribution.state == Contribution.STATE_FAILED) { | ||||
|                 val retries = contribution.retries | ||||
|                 // TODO: Improve UX. Additional details: https://github.com/commons-app/apps-android-commons/pull/5257#discussion_r1304662562 | ||||
|                 /* Limit the number of retries for a failed upload | ||||
|                    to handle cases like invalid filename as such uploads | ||||
|                    will never be successful */ | ||||
|                 if (retries < MAX_RETRIES) { | ||||
|                     contribution.retries = retries + 1 | ||||
|                     Timber.d( | ||||
|                         "Retried uploading %s %d times", contribution.media.filename, | ||||
|                         retries + 1 | ||||
|                     ) | ||||
|                     restartUpload(contribution) | ||||
|                 } else { | ||||
|                     // TODO: Show the exact reason for failure | ||||
|                     Toast.makeText( | ||||
|                         context, | ||||
|                         R.string.retry_limit_reached, Toast.LENGTH_SHORT | ||||
|                     ).show() | ||||
|                 } | ||||
|             } else { | ||||
|                 Timber.d("Skipping re-upload for non-failed %s", contribution.toString()) | ||||
|             } | ||||
|         } else { | ||||
|             showLongToast(context, R.string.this_function_needs_network_connection) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Reload media detail fragment once media is nominated | ||||
|      * | ||||
|      * @param index item position that has been nominated | ||||
|      */ | ||||
|     override fun refreshNominatedMedia(index: Int) { | ||||
|         if (mediaDetailPagerFragment != null && !contributionsListFragment!!.isVisible) { | ||||
|             removeFragment(mediaDetailPagerFragment!!) | ||||
|             mediaDetailPagerFragment = MediaDetailPagerFragment.newInstance(false, true) | ||||
|             mediaDetailPagerFragment?.showImage(index) | ||||
|             showMediaDetailPagerFragment() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * When the device rotates, rotate the Nearby banner's compass arrow in tandem. | ||||
|      */ | ||||
|     override fun onSensorChanged(event: SensorEvent) { | ||||
|         val rotateDegree = Math.round(event.values[0]).toFloat() | ||||
|         binding!!.cardViewNearby.rotateCompass(rotateDegree, direction) | ||||
|     } | ||||
| 
 | ||||
|     override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) { | ||||
|         // Nothing to do. | ||||
|     } | ||||
| 
 | ||||
|     companion object { | ||||
|         private const val CONTRIBUTION_LIST_FRAGMENT_TAG = "ContributionListFragmentTag" | ||||
|         const val MEDIA_DETAIL_PAGER_FRAGMENT_TAG: String = "MediaDetailFragmentTag" | ||||
|         private const val MAX_RETRIES = 10 | ||||
| 
 | ||||
|         @JvmStatic | ||||
|         fun newInstance(): ContributionsFragment { | ||||
|             val fragment = ContributionsFragment() | ||||
|             fragment.retainInstance = true | ||||
|             return fragment | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -1,77 +0,0 @@ | |||
|     package fr.free.nrw.commons.contributions; | ||||
| 
 | ||||
| import android.view.LayoutInflater; | ||||
| import android.view.ViewGroup; | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.paging.PagedListAdapter; | ||||
| import androidx.recyclerview.widget.DiffUtil; | ||||
| import fr.free.nrw.commons.R; | ||||
| import fr.free.nrw.commons.media.MediaClient; | ||||
| 
 | ||||
|     /** | ||||
|  * Represents The View Adapter for the List of Contributions | ||||
|  */ | ||||
| public class ContributionsListAdapter extends | ||||
|     PagedListAdapter<Contribution, ContributionViewHolder> { | ||||
| 
 | ||||
|     private final Callback callback; | ||||
|     private final MediaClient mediaClient; | ||||
| 
 | ||||
|     ContributionsListAdapter(final Callback callback, | ||||
|         final MediaClient mediaClient) { | ||||
|         super(DIFF_CALLBACK); | ||||
|         this.callback = callback; | ||||
|         this.mediaClient = mediaClient; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Uses DiffUtil to calculate the changes in the list | ||||
|      * It has methods that check ID and the content of the items to determine if its a new item | ||||
|      */ | ||||
|     private static final DiffUtil.ItemCallback<Contribution> DIFF_CALLBACK = | ||||
|         new DiffUtil.ItemCallback<Contribution>() { | ||||
|             @Override | ||||
|             public boolean areItemsTheSame(final Contribution oldContribution, final Contribution newContribution) { | ||||
|                 return oldContribution.getPageId().equals(newContribution.getPageId()); | ||||
|             } | ||||
| 
 | ||||
|             @Override | ||||
|             public boolean areContentsTheSame(final Contribution oldContribution, final Contribution newContribution) { | ||||
|                 return oldContribution.equals(newContribution); | ||||
|             } | ||||
|         }; | ||||
| 
 | ||||
|     /** | ||||
|      * Initializes the view holder with contribution data | ||||
|      */ | ||||
|     @Override | ||||
|     public void onBindViewHolder(@NonNull ContributionViewHolder holder, int position) { | ||||
|         holder.init(position, getItem(position)); | ||||
|     } | ||||
| 
 | ||||
|     Contribution getContributionForPosition(final int position) { | ||||
|         return getItem(position); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Creates the new View Holder which will be used to display items(contributions) using the | ||||
|      * onBindViewHolder(viewHolder,position) | ||||
|      */ | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public ContributionViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, | ||||
|         final int viewType) { | ||||
|         final ContributionViewHolder viewHolder = new ContributionViewHolder( | ||||
|             LayoutInflater.from(parent.getContext()) | ||||
|                 .inflate(R.layout.layout_contribution, parent, false), | ||||
|             callback, mediaClient); | ||||
|         return viewHolder; | ||||
|     } | ||||
| 
 | ||||
|     public interface Callback { | ||||
| 
 | ||||
|         void openMediaDetail(int contribution, boolean isWikipediaPageExists); | ||||
| 
 | ||||
|         void addImageToWikipedia(Contribution contribution); | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,72 @@ | |||
| package fr.free.nrw.commons.contributions | ||||
| 
 | ||||
| import android.view.LayoutInflater | ||||
| import android.view.ViewGroup | ||||
| import androidx.paging.PagedListAdapter | ||||
| import androidx.recyclerview.widget.DiffUtil | ||||
| import fr.free.nrw.commons.R | ||||
| import fr.free.nrw.commons.media.MediaClient | ||||
| 
 | ||||
| /** | ||||
|  * Represents The View Adapter for the List of Contributions | ||||
|  */ | ||||
| class ContributionsListAdapter internal constructor( | ||||
|     private val callback: Callback, | ||||
|     private val mediaClient: MediaClient | ||||
| ) : PagedListAdapter<Contribution, ContributionViewHolder>(DIFF_CALLBACK) { | ||||
|     /** | ||||
|      * Initializes the view holder with contribution data | ||||
|      */ | ||||
|     override fun onBindViewHolder(holder: ContributionViewHolder, position: Int) { | ||||
|         holder.init(position, getItem(position)) | ||||
|     } | ||||
| 
 | ||||
|     fun getContributionForPosition(position: Int): Contribution? { | ||||
|         return getItem(position) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Creates the new View Holder which will be used to display items(contributions) using the | ||||
|      * onBindViewHolder(viewHolder,position) | ||||
|      */ | ||||
|     override fun onCreateViewHolder( | ||||
|         parent: ViewGroup, | ||||
|         viewType: Int | ||||
|     ): ContributionViewHolder { | ||||
|         val viewHolder = ContributionViewHolder( | ||||
|             LayoutInflater.from(parent.context) | ||||
|                 .inflate(R.layout.layout_contribution, parent, false), | ||||
|             callback, mediaClient | ||||
|         ) | ||||
|         return viewHolder | ||||
|     } | ||||
| 
 | ||||
|     interface Callback { | ||||
|         fun openMediaDetail(contribution: Int, isWikipediaPageExists: Boolean) | ||||
| 
 | ||||
|         fun addImageToWikipedia(contribution: Contribution?) | ||||
|     } | ||||
| 
 | ||||
|     companion object { | ||||
|         /** | ||||
|          * Uses DiffUtil to calculate the changes in the list | ||||
|          * It has methods that check ID and the content of the items to determine if its a new item | ||||
|          */ | ||||
|         private val DIFF_CALLBACK: DiffUtil.ItemCallback<Contribution> = | ||||
|             object : DiffUtil.ItemCallback<Contribution>() { | ||||
|                 override fun areItemsTheSame( | ||||
|                     oldContribution: Contribution, | ||||
|                     newContribution: Contribution | ||||
|                 ): Boolean { | ||||
|                     return oldContribution.pageId == newContribution.pageId | ||||
|                 } | ||||
| 
 | ||||
|                 override fun areContentsTheSame( | ||||
|                     oldContribution: Contribution, | ||||
|                     newContribution: Contribution | ||||
|                 ): Boolean { | ||||
|                     return oldContribution == newContribution | ||||
|                 } | ||||
|             } | ||||
|     } | ||||
| } | ||||
|  | @ -1,25 +0,0 @@ | |||
| package fr.free.nrw.commons.contributions; | ||||
| 
 | ||||
| import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; | ||||
| import fr.free.nrw.commons.BasePresenter; | ||||
| 
 | ||||
| /** | ||||
|  * The contract for Contributions list View & Presenter | ||||
|  */ | ||||
| public class ContributionsListContract { | ||||
| 
 | ||||
|     public interface View { | ||||
| 
 | ||||
|         void showWelcomeTip(boolean numberOfUploads); | ||||
| 
 | ||||
|         void showProgress(boolean shouldShow); | ||||
| 
 | ||||
|         void showNoContributionsUI(boolean shouldShow); | ||||
|     } | ||||
| 
 | ||||
|     public interface UserActionListener extends BasePresenter<View> { | ||||
| 
 | ||||
|         void refreshList(SwipeRefreshLayout swipeRefreshLayout); | ||||
| 
 | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,21 @@ | |||
| package fr.free.nrw.commons.contributions | ||||
| 
 | ||||
| import androidx.swiperefreshlayout.widget.SwipeRefreshLayout | ||||
| import fr.free.nrw.commons.BasePresenter | ||||
| 
 | ||||
| /** | ||||
|  * The contract for Contributions list View & Presenter | ||||
|  */ | ||||
| class ContributionsListContract { | ||||
|     interface View { | ||||
|         fun showWelcomeTip(numberOfUploads: Boolean) | ||||
| 
 | ||||
|         fun showProgress(shouldShow: Boolean) | ||||
| 
 | ||||
|         fun showNoContributionsUI(shouldShow: Boolean) | ||||
|     } | ||||
| 
 | ||||
|     interface UserActionListener : BasePresenter<View?> { | ||||
|         fun refreshList(swipeRefreshLayout: SwipeRefreshLayout?) | ||||
|     } | ||||
| } | ||||
|  | @ -1,534 +0,0 @@ | |||
| package fr.free.nrw.commons.contributions; | ||||
| 
 | ||||
| import static android.view.View.GONE; | ||||
| import static android.view.View.VISIBLE; | ||||
| import static fr.free.nrw.commons.di.NetworkingModule.NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE; | ||||
| 
 | ||||
| import android.Manifest.permission; | ||||
| import android.content.Context; | ||||
| import android.content.Intent; | ||||
| import android.content.res.Configuration; | ||||
| import android.net.Uri; | ||||
| import android.os.Bundle; | ||||
| import android.os.Parcelable; | ||||
| import android.view.LayoutInflater; | ||||
| import android.view.MotionEvent; | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| import android.view.animation.Animation; | ||||
| import android.view.animation.AnimationUtils; | ||||
| import android.widget.LinearLayout; | ||||
| import androidx.activity.result.ActivityResultCallback; | ||||
| import androidx.activity.result.ActivityResultLauncher; | ||||
| import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions; | ||||
| import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult; | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.annotation.VisibleForTesting; | ||||
| import androidx.fragment.app.FragmentManager; | ||||
| import androidx.recyclerview.widget.GridLayoutManager; | ||||
| import androidx.recyclerview.widget.RecyclerView; | ||||
| import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver; | ||||
| import androidx.recyclerview.widget.RecyclerView.ItemAnimator; | ||||
| import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener; | ||||
| import androidx.recyclerview.widget.SimpleItemAnimator; | ||||
| import fr.free.nrw.commons.Media; | ||||
| import fr.free.nrw.commons.R; | ||||
| import fr.free.nrw.commons.Utils; | ||||
| import fr.free.nrw.commons.auth.SessionManager; | ||||
| import fr.free.nrw.commons.contributions.ContributionsListAdapter.Callback; | ||||
| import fr.free.nrw.commons.databinding.FragmentContributionsListBinding; | ||||
| import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; | ||||
| import fr.free.nrw.commons.media.MediaClient; | ||||
| import fr.free.nrw.commons.profile.ProfileActivity; | ||||
| import fr.free.nrw.commons.utils.DialogUtil; | ||||
| import fr.free.nrw.commons.utils.SystemThemeUtils; | ||||
| import fr.free.nrw.commons.utils.ViewUtil; | ||||
| import java.util.Map; | ||||
| import java.util.Objects; | ||||
| import javax.inject.Inject; | ||||
| import javax.inject.Named; | ||||
| import org.apache.commons.lang3.StringUtils; | ||||
| import fr.free.nrw.commons.wikidata.model.WikiSite; | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * Created by root on 01.06.2018. | ||||
|  */ | ||||
| 
 | ||||
| public class ContributionsListFragment extends CommonsDaggerSupportFragment implements | ||||
|     ContributionsListContract.View, Callback, | ||||
|     WikipediaInstructionsDialogFragment.Callback { | ||||
| 
 | ||||
|     private static final String RV_STATE = "rv_scroll_state"; | ||||
| 
 | ||||
|     @Inject | ||||
|     SystemThemeUtils systemThemeUtils; | ||||
|     @Inject | ||||
|     ContributionController controller; | ||||
|     @Inject | ||||
|     MediaClient mediaClient; | ||||
|     @Named(NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE) | ||||
|     @Inject | ||||
|     WikiSite languageWikipediaSite; | ||||
|     @Inject | ||||
|     ContributionsListPresenter contributionsListPresenter; | ||||
|     @Inject | ||||
|     SessionManager sessionManager; | ||||
| 
 | ||||
|     private FragmentContributionsListBinding binding; | ||||
|     private Animation fab_close; | ||||
|     private Animation fab_open; | ||||
|     private Animation rotate_forward; | ||||
|     private Animation rotate_backward; | ||||
|     private boolean isFabOpen; | ||||
|     @VisibleForTesting | ||||
|     protected RecyclerView rvContributionsList; | ||||
| 
 | ||||
|     @VisibleForTesting | ||||
|     protected ContributionsListAdapter adapter; | ||||
| 
 | ||||
|     @Nullable | ||||
|     @VisibleForTesting | ||||
|     protected Callback callback; | ||||
| 
 | ||||
|     private final int SPAN_COUNT_LANDSCAPE = 3; | ||||
|     private final int SPAN_COUNT_PORTRAIT = 1; | ||||
| 
 | ||||
|     private int contributionsSize; | ||||
|     private String userName; | ||||
| 
 | ||||
|     private final ActivityResultLauncher<Intent> galleryPickLauncherForResult = | ||||
|         registerForActivityResult(new StartActivityForResult(), | ||||
|         result -> { | ||||
|             controller.handleActivityResultWithCallback(requireActivity(), callbacks -> { | ||||
|                 controller.onPictureReturnedFromGallery(result, requireActivity(), callbacks); | ||||
|             }); | ||||
|         }); | ||||
| 
 | ||||
|     private final ActivityResultLauncher<Intent> customSelectorLauncherForResult = | ||||
|         registerForActivityResult(new StartActivityForResult(), | ||||
|         result -> { | ||||
|             controller.handleActivityResultWithCallback(requireActivity(), callbacks -> { | ||||
|                 controller.onPictureReturnedFromCustomSelector(result, requireActivity(), callbacks); | ||||
|             }); | ||||
|         }); | ||||
| 
 | ||||
|     private final ActivityResultLauncher<Intent> cameraPickLauncherForResult = | ||||
|         registerForActivityResult(new StartActivityForResult(), | ||||
|         result -> { | ||||
|             controller.handleActivityResultWithCallback(requireActivity(), callbacks -> { | ||||
|                 controller.onPictureReturnedFromCamera(result, requireActivity(), callbacks); | ||||
|             }); | ||||
|         }); | ||||
| 
 | ||||
|     private ActivityResultLauncher<String[]> inAppCameraLocationPermissionLauncher = registerForActivityResult( | ||||
|         new RequestMultiplePermissions(), | ||||
|         new ActivityResultCallback<Map<String, Boolean>>() { | ||||
|             @Override | ||||
|             public void onActivityResult(Map<String, Boolean> result) { | ||||
|                 boolean areAllGranted = true; | ||||
|                 for (final boolean b : result.values()) { | ||||
|                     areAllGranted = areAllGranted && b; | ||||
|                 } | ||||
| 
 | ||||
|                 if (areAllGranted) { | ||||
|                     controller.locationPermissionCallback.onLocationPermissionGranted(); | ||||
|                 } else { | ||||
|                     if (shouldShowRequestPermissionRationale(permission.ACCESS_FINE_LOCATION)) { | ||||
|                         controller.handleShowRationaleFlowCameraLocation(getActivity(), | ||||
|                             inAppCameraLocationPermissionLauncher, cameraPickLauncherForResult); | ||||
|                     } else { | ||||
|                         controller.locationPermissionCallback.onLocationPermissionDenied( | ||||
|                             getActivity().getString( | ||||
|                                 R.string.in_app_camera_location_permission_denied)); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
| 
 | ||||
|     @Override | ||||
|     public void onCreate( | ||||
|         @Nullable @org.jetbrains.annotations.Nullable final Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         //Now that we are allowing this fragment to be started for | ||||
|         // any userName- we expect it to be passed as an argument | ||||
|         if (getArguments() != null) { | ||||
|             userName = getArguments().getString(ProfileActivity.KEY_USERNAME); | ||||
|         } | ||||
| 
 | ||||
|         if (StringUtils.isEmpty(userName)) { | ||||
|             userName = sessionManager.getUserName(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public View onCreateView( | ||||
|         final LayoutInflater inflater, @Nullable final ViewGroup container, | ||||
|         @Nullable final Bundle savedInstanceState) { | ||||
|         binding = FragmentContributionsListBinding.inflate( | ||||
|             inflater, container, false | ||||
|         ); | ||||
|         rvContributionsList = binding.contributionsList; | ||||
| 
 | ||||
|         contributionsListPresenter.onAttachView(this); | ||||
|         binding.fabCustomGallery.setOnClickListener(v -> launchCustomSelector()); | ||||
|         binding.fabCustomGallery.setOnLongClickListener(view -> { | ||||
|             ViewUtil.showShortToast(getContext(), R.string.custom_selector_title); | ||||
|             return true; | ||||
|         }); | ||||
| 
 | ||||
|         if (Objects.equals(sessionManager.getUserName(), userName)) { | ||||
|             binding.tvContributionsOfUser.setVisibility(GONE); | ||||
|             binding.fabLayout.setVisibility(VISIBLE); | ||||
|         } else { | ||||
|             binding.tvContributionsOfUser.setVisibility(VISIBLE); | ||||
|             binding.tvContributionsOfUser.setText( | ||||
|                 getString(R.string.contributions_of_user, userName)); | ||||
|             binding.fabLayout.setVisibility(GONE); | ||||
|         } | ||||
| 
 | ||||
|         initAdapter(); | ||||
| 
 | ||||
|         // pull down to refresh only enabled for self user. | ||||
|         if(Objects.equals(sessionManager.getUserName(), userName)){ | ||||
|             binding.swipeRefreshLayout.setOnRefreshListener(() -> { | ||||
|                 contributionsListPresenter.refreshList(binding.swipeRefreshLayout); | ||||
|             }); | ||||
|         } else { | ||||
|             binding.swipeRefreshLayout.setEnabled(false); | ||||
|         } | ||||
| 
 | ||||
|         return binding.getRoot(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onDestroyView() { | ||||
|         binding = null; | ||||
|         super.onDestroyView(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onAttach(Context context) { | ||||
|         super.onAttach(context); | ||||
|         if (getParentFragment() != null && getParentFragment() instanceof ContributionsFragment) { | ||||
|             callback = ((ContributionsFragment) getParentFragment()); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onDetach() { | ||||
|         super.onDetach(); | ||||
|         callback = null;//To avoid possible memory leak | ||||
|     } | ||||
| 
 | ||||
|     private void initAdapter() { | ||||
|         adapter = new ContributionsListAdapter(this, mediaClient); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onViewCreated(final View view, @Nullable final Bundle savedInstanceState) { | ||||
|         super.onViewCreated(view, savedInstanceState); | ||||
|         initRecyclerView(); | ||||
|         initializeAnimations(); | ||||
|         setListeners(); | ||||
|     } | ||||
| 
 | ||||
|     private void initRecyclerView() { | ||||
|         final GridLayoutManager layoutManager = new GridLayoutManager(getContext(), | ||||
|             getSpanCount(getResources().getConfiguration().orientation)); | ||||
|         rvContributionsList.setLayoutManager(layoutManager); | ||||
| 
 | ||||
|         //Setting flicker animation of recycler view to false. | ||||
|         final ItemAnimator animator = rvContributionsList.getItemAnimator(); | ||||
|         if (animator instanceof SimpleItemAnimator) { | ||||
|             ((SimpleItemAnimator) animator).setSupportsChangeAnimations(false); | ||||
|         } | ||||
| 
 | ||||
|         contributionsListPresenter.setup(userName, | ||||
|             Objects.equals(sessionManager.getUserName(), userName)); | ||||
|         contributionsListPresenter.contributionList.observe(getViewLifecycleOwner(), list -> { | ||||
|             contributionsSize = list.size(); | ||||
|             adapter.submitList(list); | ||||
|             if (callback != null) { | ||||
|                 callback.notifyDataSetChanged(); | ||||
|             } | ||||
|         }); | ||||
|         rvContributionsList.setAdapter(adapter); | ||||
|         adapter.registerAdapterDataObserver(new AdapterDataObserver() { | ||||
|             @Override | ||||
|             public void onItemRangeInserted(int positionStart, int itemCount) { | ||||
|                 super.onItemRangeInserted(positionStart, itemCount); | ||||
|                 contributionsSize = adapter.getItemCount(); | ||||
|                 if (callback != null) { | ||||
|                     callback.notifyDataSetChanged(); | ||||
|                 } | ||||
|                 if (itemCount > 0 && positionStart == 0) { | ||||
|                     if (adapter.getContributionForPosition(positionStart) != null) { | ||||
|                         rvContributionsList | ||||
|                             .scrollToPosition(0);//Newly upload items are always added to the top | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             /** | ||||
|              * Called whenever items in the list have changed | ||||
|              * Calls viewPagerNotifyDataSetChanged() that will notify the viewpager | ||||
|              */ | ||||
|             @Override | ||||
|             public void onItemRangeChanged(final int positionStart, final int itemCount) { | ||||
|                 super.onItemRangeChanged(positionStart, itemCount); | ||||
|                 if (callback != null) { | ||||
|                     callback.viewPagerNotifyDataSetChanged(); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         //Fab close on touch outside (Scrolling or taping on item triggers this action). | ||||
|         rvContributionsList.addOnItemTouchListener(new OnItemTouchListener() { | ||||
| 
 | ||||
|             /** | ||||
|              * Silently observe and/or take over touch events sent to the RecyclerView before | ||||
|              * they are handled by either the RecyclerView itself or its child views. | ||||
|              */ | ||||
|             @Override | ||||
|             public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) { | ||||
|                 if (e.getAction() == MotionEvent.ACTION_DOWN) { | ||||
|                     if (isFabOpen) { | ||||
|                         animateFAB(isFabOpen); | ||||
|                     } | ||||
|                 } | ||||
|                 return false; | ||||
|             } | ||||
| 
 | ||||
|             /** | ||||
|              * Process a touch event as part of a gesture that was claimed by returning true | ||||
|              * from a previous call to {@link #onInterceptTouchEvent}. | ||||
|              * | ||||
|              * @param rv | ||||
|              * @param e  MotionEvent describing the touch event. All coordinates are in the | ||||
|              *           RecyclerView's coordinate system. | ||||
|              */ | ||||
|             @Override | ||||
|             public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) { | ||||
|                 //required abstract method DO NOT DELETE | ||||
|             } | ||||
| 
 | ||||
|             /** | ||||
|              * Called when a child of RecyclerView does not want RecyclerView and its ancestors | ||||
|              * to intercept touch events with {@link ViewGroup#onInterceptTouchEvent(MotionEvent)}. | ||||
|              * | ||||
|              * @param disallowIntercept True if the child does not want the parent to intercept | ||||
|              *                          touch events. | ||||
|              */ | ||||
|             @Override | ||||
|             public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { | ||||
|                 //required abstract method DO NOT DELETE | ||||
|             } | ||||
| 
 | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     private int getSpanCount(final int orientation) { | ||||
|         return orientation == Configuration.ORIENTATION_LANDSCAPE ? | ||||
|             SPAN_COUNT_LANDSCAPE : SPAN_COUNT_PORTRAIT; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onConfigurationChanged(final Configuration newConfig) { | ||||
|         super.onConfigurationChanged(newConfig); | ||||
|         // check orientation | ||||
|         binding.fabLayout.setOrientation( | ||||
|             newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE ? | ||||
|                 LinearLayout.HORIZONTAL : LinearLayout.VERTICAL); | ||||
|         rvContributionsList | ||||
|             .setLayoutManager( | ||||
|                 new GridLayoutManager(getContext(), getSpanCount(newConfig.orientation))); | ||||
|     } | ||||
| 
 | ||||
|     private void initializeAnimations() { | ||||
|         fab_open = AnimationUtils.loadAnimation(getActivity(), R.anim.fab_open); | ||||
|         fab_close = AnimationUtils.loadAnimation(getActivity(), R.anim.fab_close); | ||||
|         rotate_forward = AnimationUtils.loadAnimation(getActivity(), R.anim.rotate_forward); | ||||
|         rotate_backward = AnimationUtils.loadAnimation(getActivity(), R.anim.rotate_backward); | ||||
|     } | ||||
| 
 | ||||
|     private void setListeners() { | ||||
|         binding.fabPlus.setOnClickListener(view -> animateFAB(isFabOpen)); | ||||
|         binding.fabCamera.setOnClickListener(view -> { | ||||
|             controller.initiateCameraPick(getActivity(), inAppCameraLocationPermissionLauncher, cameraPickLauncherForResult); | ||||
|             animateFAB(isFabOpen); | ||||
|         }); | ||||
|         binding.fabCamera.setOnLongClickListener(view -> { | ||||
|             ViewUtil.showShortToast(getContext(), R.string.add_contribution_from_camera); | ||||
|             return true; | ||||
|         }); | ||||
|         binding.fabGallery.setOnClickListener(view -> { | ||||
|             controller.initiateGalleryPick(getActivity(), galleryPickLauncherForResult, true); | ||||
|             animateFAB(isFabOpen); | ||||
|         }); | ||||
|         binding.fabGallery.setOnLongClickListener(view -> { | ||||
|             ViewUtil.showShortToast(getContext(), R.string.menu_from_gallery); | ||||
|             return true; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Launch Custom Selector. | ||||
|      */ | ||||
|     protected void launchCustomSelector() { | ||||
|         controller.initiateCustomGalleryPickWithPermission(getActivity(), customSelectorLauncherForResult); | ||||
|         animateFAB(isFabOpen); | ||||
|     } | ||||
| 
 | ||||
|     public void scrollToTop() { | ||||
|         rvContributionsList.smoothScrollToPosition(0); | ||||
|     } | ||||
| 
 | ||||
|     private void animateFAB(final boolean isFabOpen) { | ||||
|         this.isFabOpen = !isFabOpen; | ||||
|         if (binding.fabPlus.isShown()) { | ||||
|             if (isFabOpen) { | ||||
|                 binding.fabPlus.startAnimation(rotate_backward); | ||||
|                 binding.fabCamera.startAnimation(fab_close); | ||||
|                 binding.fabGallery.startAnimation(fab_close); | ||||
|                 binding.fabCustomGallery.startAnimation(fab_close); | ||||
|                 binding.fabCamera.hide(); | ||||
|                 binding.fabGallery.hide(); | ||||
|                 binding.fabCustomGallery.hide(); | ||||
|             } else { | ||||
|                 binding.fabPlus.startAnimation(rotate_forward); | ||||
|                 binding.fabCamera.startAnimation(fab_open); | ||||
|                 binding.fabGallery.startAnimation(fab_open); | ||||
|                 binding.fabCustomGallery.startAnimation(fab_open); | ||||
|                 binding.fabCamera.show(); | ||||
|                 binding.fabGallery.show(); | ||||
|                 binding.fabCustomGallery.show(); | ||||
|             } | ||||
|             this.isFabOpen = !isFabOpen; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Shows welcome message if user has no contributions yet i.e. new user. | ||||
|      */ | ||||
|     @Override | ||||
|     public void showWelcomeTip(final boolean shouldShow) { | ||||
|         binding.noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Responsible to set progress bar invisible and visible | ||||
|      * | ||||
|      * @param shouldShow True when contributions list should be hidden. | ||||
|      */ | ||||
|     @Override | ||||
|     public void showProgress(final boolean shouldShow) { | ||||
|         binding.loadingContributionsProgressBar.setVisibility(shouldShow ? VISIBLE : GONE); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void showNoContributionsUI(final boolean shouldShow) { | ||||
|         binding.noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onSaveInstanceState(@NonNull Bundle outState) { | ||||
|         super.onSaveInstanceState(outState); | ||||
|         final GridLayoutManager layoutManager = (GridLayoutManager) rvContributionsList | ||||
|             .getLayoutManager(); | ||||
|         outState.putParcelable(RV_STATE, layoutManager.onSaveInstanceState()); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onViewStateRestored(@Nullable Bundle savedInstanceState) { | ||||
|         super.onViewStateRestored(savedInstanceState); | ||||
|         if (null != savedInstanceState) { | ||||
|             final Parcelable savedRecyclerLayoutState = savedInstanceState.getParcelable(RV_STATE); | ||||
|             rvContributionsList.getLayoutManager().onRestoreInstanceState(savedRecyclerLayoutState); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void openMediaDetail(final int position, boolean isWikipediaButtonDisplayed) { | ||||
|         if (null != callback) {//Just being safe, ideally they won't be called when detached | ||||
|             callback.showDetail(position, isWikipediaButtonDisplayed); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Handle callback for wikipedia icon clicked | ||||
|      * | ||||
|      * @param contribution | ||||
|      */ | ||||
|     @Override | ||||
|     public void addImageToWikipedia(Contribution contribution) { | ||||
|         DialogUtil.showAlertDialog(getActivity(), | ||||
|             getString(R.string.add_picture_to_wikipedia_article_title), | ||||
|             getString(R.string.add_picture_to_wikipedia_article_desc), | ||||
|             () -> { | ||||
|                 showAddImageToWikipediaInstructions(contribution); | ||||
|             }, () -> { | ||||
|                 // do nothing | ||||
|             }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Display confirmation dialog with instructions when the user tries to add image to wikipedia | ||||
|      * | ||||
|      * @param contribution | ||||
|      */ | ||||
|     private void showAddImageToWikipediaInstructions(Contribution contribution) { | ||||
|         FragmentManager fragmentManager = getFragmentManager(); | ||||
|         WikipediaInstructionsDialogFragment fragment = WikipediaInstructionsDialogFragment | ||||
|             .newInstance(contribution); | ||||
|         fragment.setCallback(this::onConfirmClicked); | ||||
|         fragment.show(fragmentManager, "WikimediaFragment"); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     public Media getMediaAtPosition(final int i) { | ||||
|         if (adapter.getContributionForPosition(i) != null) { | ||||
|             return adapter.getContributionForPosition(i).getMedia(); | ||||
|         } | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     public int getTotalMediaCount() { | ||||
|         return contributionsSize; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Open the editor for the language Wikipedia | ||||
|      * | ||||
|      * @param contribution | ||||
|      */ | ||||
|     @Override | ||||
|     public void onConfirmClicked(@Nullable Contribution contribution, boolean copyWikicode) { | ||||
|         if (copyWikicode) { | ||||
|             String wikicode = contribution.getMedia().getWikiCode(); | ||||
|             Utils.copy("wikicode", wikicode, getContext()); | ||||
|         } | ||||
| 
 | ||||
|         final String url = | ||||
|             languageWikipediaSite.mobileUrl() + "/wiki/" + contribution.getWikidataPlace() | ||||
|                 .getWikipediaPageTitle(); | ||||
|         Utils.handleWebUrl(getContext(), Uri.parse(url)); | ||||
|     } | ||||
| 
 | ||||
|     public Integer getContributionStateAt(int position) { | ||||
|         return adapter.getContributionForPosition(position).getState(); | ||||
|     } | ||||
| 
 | ||||
|     public interface Callback { | ||||
| 
 | ||||
|         void notifyDataSetChanged(); | ||||
| 
 | ||||
|         void showDetail(int position, boolean isWikipediaButtonDisplayed); | ||||
| 
 | ||||
|         // Notify the viewpager that number of items have changed. | ||||
|         void viewPagerNotifyDataSetChanged(); | ||||
| 
 | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,551 @@ | |||
| package fr.free.nrw.commons.contributions | ||||
| 
 | ||||
| import android.Manifest.permission | ||||
| import android.annotation.SuppressLint | ||||
| import android.content.Context | ||||
| import android.content.Intent | ||||
| import android.content.res.Configuration | ||||
| import android.net.Uri | ||||
| import android.os.Bundle | ||||
| import android.os.Parcelable | ||||
| import android.view.LayoutInflater | ||||
| import android.view.MotionEvent | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import android.view.animation.Animation | ||||
| import android.view.animation.AnimationUtils | ||||
| import android.widget.LinearLayout | ||||
| import androidx.activity.result.ActivityResult | ||||
| import androidx.activity.result.ActivityResultLauncher | ||||
| import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions | ||||
| import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult | ||||
| import androidx.annotation.VisibleForTesting | ||||
| import androidx.paging.PagedList | ||||
| import androidx.recyclerview.widget.GridLayoutManager | ||||
| import androidx.recyclerview.widget.RecyclerView | ||||
| import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver | ||||
| import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener | ||||
| import androidx.recyclerview.widget.SimpleItemAnimator | ||||
| import fr.free.nrw.commons.Media | ||||
| import fr.free.nrw.commons.R | ||||
| import fr.free.nrw.commons.Utils | ||||
| import fr.free.nrw.commons.auth.SessionManager | ||||
| import fr.free.nrw.commons.contributions.WikipediaInstructionsDialogFragment.Companion.newInstance | ||||
| import fr.free.nrw.commons.databinding.FragmentContributionsListBinding | ||||
| import fr.free.nrw.commons.di.CommonsDaggerSupportFragment | ||||
| import fr.free.nrw.commons.di.NetworkingModule | ||||
| import fr.free.nrw.commons.filepicker.FilePicker | ||||
| import fr.free.nrw.commons.media.MediaClient | ||||
| import fr.free.nrw.commons.profile.ProfileActivity | ||||
| import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog | ||||
| import fr.free.nrw.commons.utils.SystemThemeUtils | ||||
| import fr.free.nrw.commons.utils.ViewUtil.showShortToast | ||||
| import fr.free.nrw.commons.wikidata.model.WikiSite | ||||
| import org.apache.commons.lang3.StringUtils | ||||
| import javax.inject.Inject | ||||
| import javax.inject.Named | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * Created by root on 01.06.2018. | ||||
|  */ | ||||
| class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsListContract.View, | ||||
|     ContributionsListAdapter.Callback, WikipediaInstructionsDialogFragment.Callback { | ||||
|     @JvmField | ||||
|     @Inject | ||||
|     var systemThemeUtils: SystemThemeUtils? = null | ||||
| 
 | ||||
|     @JvmField | ||||
|     @Inject | ||||
|     var controller: ContributionController? = null | ||||
| 
 | ||||
|     @JvmField | ||||
|     @Inject | ||||
|     var mediaClient: MediaClient? = null | ||||
| 
 | ||||
|     @JvmField | ||||
|     @Named(NetworkingModule.NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE) | ||||
|     @Inject | ||||
|     var languageWikipediaSite: WikiSite? = null | ||||
| 
 | ||||
|     @JvmField | ||||
|     @Inject | ||||
|     var contributionsListPresenter: ContributionsListPresenter? = null | ||||
| 
 | ||||
|     @JvmField | ||||
|     @Inject | ||||
|     var sessionManager: SessionManager? = null | ||||
| 
 | ||||
|     private var binding: FragmentContributionsListBinding? = null | ||||
|     private var fab_close: Animation? = null | ||||
|     private var fab_open: Animation? = null | ||||
|     private var rotate_forward: Animation? = null | ||||
|     private var rotate_backward: Animation? = null | ||||
|     private var isFabOpen = false | ||||
| 
 | ||||
|     private lateinit var inAppCameraLocationPermissionLauncher: ActivityResultLauncher<Array<String>> | ||||
| 
 | ||||
|     @VisibleForTesting | ||||
|     var rvContributionsList: RecyclerView? = null | ||||
| 
 | ||||
|     @VisibleForTesting | ||||
|     var adapter: ContributionsListAdapter? = null | ||||
| 
 | ||||
|     @VisibleForTesting | ||||
|     var callback: Callback? = null | ||||
| 
 | ||||
|     private val SPAN_COUNT_LANDSCAPE = 3 | ||||
|     private val SPAN_COUNT_PORTRAIT = 1 | ||||
| 
 | ||||
|     private var contributionsSize = 0 | ||||
|     private var userName: String? = null | ||||
| 
 | ||||
|     private val galleryPickLauncherForResult = registerForActivityResult<Intent, ActivityResult>( | ||||
|         StartActivityForResult() | ||||
|     ) { result: ActivityResult? -> | ||||
|         controller!!.handleActivityResultWithCallback(requireActivity() | ||||
|         ) { callbacks: FilePicker.Callbacks? -> | ||||
|             controller!!.onPictureReturnedFromGallery( | ||||
|                 result!!, requireActivity(), callbacks!! | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private val customSelectorLauncherForResult = registerForActivityResult<Intent, ActivityResult>( | ||||
|         StartActivityForResult() | ||||
|     ) { result: ActivityResult? -> | ||||
|         controller!!.handleActivityResultWithCallback(requireActivity() | ||||
|         ) { callbacks: FilePicker.Callbacks? -> | ||||
|             controller!!.onPictureReturnedFromCustomSelector( | ||||
|                 result!!, requireActivity(), callbacks!! | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private val cameraPickLauncherForResult = registerForActivityResult<Intent, ActivityResult>( | ||||
|         StartActivityForResult() | ||||
|     ) { result: ActivityResult? -> | ||||
|         controller!!.handleActivityResultWithCallback(requireActivity() | ||||
|         ) { callbacks: FilePicker.Callbacks? -> | ||||
|             controller!!.onPictureReturnedFromCamera( | ||||
|                 result!!, requireActivity(), callbacks!! | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @SuppressLint("NewApi") | ||||
|     override fun onCreate( | ||||
|         savedInstanceState: Bundle? | ||||
|     ) { | ||||
|         super.onCreate(savedInstanceState) | ||||
|         //Now that we are allowing this fragment to be started for | ||||
|         // any userName- we expect it to be passed as an argument | ||||
|         if (arguments != null) { | ||||
|             userName = requireArguments().getString(ProfileActivity.KEY_USERNAME) | ||||
|         } | ||||
| 
 | ||||
|         if (StringUtils.isEmpty(userName)) { | ||||
|             userName = sessionManager!!.userName | ||||
|         } | ||||
|         inAppCameraLocationPermissionLauncher = | ||||
|         registerForActivityResult(RequestMultiplePermissions()) { result -> | ||||
|             val areAllGranted = result.values.all { it } | ||||
| 
 | ||||
|             if (areAllGranted) { | ||||
|                 controller?.locationPermissionCallback?.onLocationPermissionGranted() | ||||
|             } else { | ||||
|                 activity?.let { currentActivity -> | ||||
|                     if (currentActivity.shouldShowRequestPermissionRationale(permission.ACCESS_FINE_LOCATION)) { | ||||
|                         controller?.handleShowRationaleFlowCameraLocation( | ||||
|                             currentActivity, | ||||
|                             inAppCameraLocationPermissionLauncher, // Pass launcher | ||||
|                             cameraPickLauncherForResult | ||||
|                         ) | ||||
|                     } else { | ||||
|                         controller?.locationPermissionCallback?.onLocationPermissionDenied( | ||||
|                             currentActivity.getString(R.string.in_app_camera_location_permission_denied) | ||||
|                         ) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun onCreateView( | ||||
|         inflater: LayoutInflater, container: ViewGroup?, | ||||
|         savedInstanceState: Bundle? | ||||
|     ): View? { | ||||
|         binding = FragmentContributionsListBinding.inflate( | ||||
|             inflater, container, false | ||||
|         ) | ||||
|         rvContributionsList = binding!!.contributionsList | ||||
| 
 | ||||
|         contributionsListPresenter!!.onAttachView(this) | ||||
|         binding!!.fabCustomGallery.setOnClickListener { v: View? -> launchCustomSelector() } | ||||
|         binding!!.fabCustomGallery.setOnLongClickListener { view: View? -> | ||||
|             showShortToast(context, fr.free.nrw.commons.R.string.custom_selector_title) | ||||
|             true | ||||
|         } | ||||
| 
 | ||||
|         if (sessionManager!!.userName == userName) { | ||||
|             binding!!.tvContributionsOfUser.visibility = View.GONE | ||||
|             binding!!.fabLayout.visibility = View.VISIBLE | ||||
|         } else { | ||||
|             binding!!.tvContributionsOfUser.visibility = View.VISIBLE | ||||
|             binding!!.tvContributionsOfUser.text = | ||||
|                 getString(fr.free.nrw.commons.R.string.contributions_of_user, userName) | ||||
|             binding!!.fabLayout.visibility = View.GONE | ||||
|         } | ||||
| 
 | ||||
|         initAdapter() | ||||
| 
 | ||||
|         // pull down to refresh only enabled for self user. | ||||
|         if (sessionManager!!.userName == userName) { | ||||
|             binding!!.swipeRefreshLayout.setOnRefreshListener { | ||||
|                 contributionsListPresenter!!.refreshList( | ||||
|                     binding!!.swipeRefreshLayout | ||||
|                 ) | ||||
|             } | ||||
|         } else { | ||||
|             binding!!.swipeRefreshLayout.isEnabled = false | ||||
|         } | ||||
| 
 | ||||
|         return binding!!.root | ||||
|     } | ||||
| 
 | ||||
|     override fun onDestroyView() { | ||||
|         binding = null | ||||
|         super.onDestroyView() | ||||
|     } | ||||
| 
 | ||||
|     override fun onAttach(context: Context) { | ||||
|         super.onAttach(context) | ||||
|         if (parentFragment != null && parentFragment is ContributionsFragment) { | ||||
|             callback = (parentFragment as ContributionsFragment) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun onDetach() { | ||||
|         super.onDetach() | ||||
|         callback = null //To avoid possible memory leak | ||||
|     } | ||||
| 
 | ||||
|     private fun initAdapter() { | ||||
|         adapter = ContributionsListAdapter(this, mediaClient!!) | ||||
|     } | ||||
| 
 | ||||
|     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||
|         super.onViewCreated(view, savedInstanceState) | ||||
|         initRecyclerView() | ||||
|         initializeAnimations() | ||||
|         setListeners() | ||||
|     } | ||||
| 
 | ||||
|     private fun initRecyclerView() { | ||||
|         val layoutManager = GridLayoutManager( | ||||
|             context, | ||||
|             getSpanCount(resources.configuration.orientation) | ||||
|         ) | ||||
|         rvContributionsList!!.layoutManager = layoutManager | ||||
| 
 | ||||
|         //Setting flicker animation of recycler view to false. | ||||
|         val animator = rvContributionsList!!.itemAnimator | ||||
|         if (animator is SimpleItemAnimator) { | ||||
|             animator.supportsChangeAnimations = false | ||||
|         } | ||||
| 
 | ||||
|         contributionsListPresenter!!.setup( | ||||
|             userName, | ||||
|             sessionManager!!.userName == userName | ||||
|         ) | ||||
|         contributionsListPresenter!!.contributionList?.observe( | ||||
|             viewLifecycleOwner | ||||
|         ) { list: PagedList<Contribution>? -> | ||||
|             if (list != null) { | ||||
|                 contributionsSize = list.size | ||||
|             } | ||||
|             adapter!!.submitList(list) | ||||
|             if (callback != null) { | ||||
|                 callback!!.notifyDataSetChanged() | ||||
|             } | ||||
|         } | ||||
|         rvContributionsList!!.adapter = adapter | ||||
|         adapter!!.registerAdapterDataObserver(object : AdapterDataObserver() { | ||||
|             override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { | ||||
|                 super.onItemRangeInserted(positionStart, itemCount) | ||||
|                 contributionsSize = adapter!!.itemCount | ||||
|                 if (callback != null) { | ||||
|                     callback!!.notifyDataSetChanged() | ||||
|                 } | ||||
|                 if (itemCount > 0 && positionStart == 0) { | ||||
|                     if (adapter!!.getContributionForPosition(positionStart) != null) { | ||||
|                         rvContributionsList!! | ||||
|                             .scrollToPosition(0) //Newly upload items are always added to the top | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             /** | ||||
|              * Called whenever items in the list have changed | ||||
|              * Calls viewPagerNotifyDataSetChanged() that will notify the viewpager | ||||
|              */ | ||||
|             override fun onItemRangeChanged(positionStart: Int, itemCount: Int) { | ||||
|                 super.onItemRangeChanged(positionStart, itemCount) | ||||
|                 if (callback != null) { | ||||
|                     callback!!.viewPagerNotifyDataSetChanged() | ||||
|                 } | ||||
|             } | ||||
|         }) | ||||
| 
 | ||||
|         //Fab close on touch outside (Scrolling or taping on item triggers this action). | ||||
|         rvContributionsList!!.addOnItemTouchListener(object : OnItemTouchListener { | ||||
|             /** | ||||
|              * Silently observe and/or take over touch events sent to the RecyclerView before | ||||
|              * they are handled by either the RecyclerView itself or its child views. | ||||
|              */ | ||||
|             override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean { | ||||
|                 if (e.action == MotionEvent.ACTION_DOWN) { | ||||
|                     if (isFabOpen) { | ||||
|                         animateFAB(isFabOpen) | ||||
|                     } | ||||
|                 } | ||||
|                 return false | ||||
|             } | ||||
| 
 | ||||
|             /** | ||||
|              * Process a touch event as part of a gesture that was claimed by returning true | ||||
|              * from a previous call to [.onInterceptTouchEvent]. | ||||
|              * | ||||
|              * @param rv | ||||
|              * @param e  MotionEvent describing the touch event. All coordinates are in the | ||||
|              * RecyclerView's coordinate system. | ||||
|              */ | ||||
|             override fun onTouchEvent(rv: RecyclerView, e: MotionEvent) { | ||||
|                 //required abstract method DO NOT DELETE | ||||
|             } | ||||
| 
 | ||||
|             /** | ||||
|              * Called when a child of RecyclerView does not want RecyclerView and its ancestors | ||||
|              * to intercept touch events with [ViewGroup.onInterceptTouchEvent]. | ||||
|              * | ||||
|              * @param disallowIntercept True if the child does not want the parent to intercept | ||||
|              * touch events. | ||||
|              */ | ||||
|             override fun onRequestDisallowInterceptTouchEvent(disallowIntercept: Boolean) { | ||||
|                 //required abstract method DO NOT DELETE | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     private fun getSpanCount(orientation: Int): Int { | ||||
|         return if (orientation == Configuration.ORIENTATION_LANDSCAPE) SPAN_COUNT_LANDSCAPE else SPAN_COUNT_PORTRAIT | ||||
|     } | ||||
| 
 | ||||
|     override fun onConfigurationChanged(newConfig: Configuration) { | ||||
|         super.onConfigurationChanged(newConfig) | ||||
|         // check orientation | ||||
|         binding!!.fabLayout.orientation = | ||||
|             if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) LinearLayout.HORIZONTAL else LinearLayout.VERTICAL | ||||
|         rvContributionsList | ||||
|             ?.setLayoutManager( | ||||
|                 GridLayoutManager(context, getSpanCount(newConfig.orientation)) | ||||
|             ) | ||||
|     } | ||||
| 
 | ||||
|     private fun initializeAnimations() { | ||||
|         fab_open = AnimationUtils.loadAnimation(activity, fr.free.nrw.commons.R.anim.fab_open) | ||||
|         fab_close = AnimationUtils.loadAnimation(activity, fr.free.nrw.commons.R.anim.fab_close) | ||||
|         rotate_forward = AnimationUtils.loadAnimation(activity, fr.free.nrw.commons.R.anim.rotate_forward) | ||||
|         rotate_backward = AnimationUtils.loadAnimation(activity, fr.free.nrw.commons.R.anim.rotate_backward) | ||||
|     } | ||||
| 
 | ||||
|     private fun setListeners() { | ||||
|         binding!!.fabPlus.setOnClickListener { view: View? -> animateFAB(isFabOpen) } | ||||
|         binding!!.fabCamera.setOnClickListener { view: View? -> | ||||
|             controller!!.initiateCameraPick( | ||||
|                 requireActivity(), | ||||
|                 inAppCameraLocationPermissionLauncher, | ||||
|                 cameraPickLauncherForResult | ||||
|             ) | ||||
|             animateFAB(isFabOpen) | ||||
|         } | ||||
|         binding!!.fabCamera.setOnLongClickListener { view: View? -> | ||||
|             showShortToast( | ||||
|                 context, | ||||
|                 fr.free.nrw.commons.R.string.add_contribution_from_camera | ||||
|             ) | ||||
|             true | ||||
|         } | ||||
|         binding!!.fabGallery.setOnClickListener { view: View? -> | ||||
|             controller!!.initiateGalleryPick(requireActivity(), galleryPickLauncherForResult, true) | ||||
|             animateFAB(isFabOpen) | ||||
|         } | ||||
|         binding!!.fabGallery.setOnLongClickListener { view: View? -> | ||||
|             showShortToast(context, fr.free.nrw.commons.R.string.menu_from_gallery) | ||||
|             true | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Launch Custom Selector. | ||||
|      */ | ||||
|     protected fun launchCustomSelector() { | ||||
|         controller!!.initiateCustomGalleryPickWithPermission( | ||||
|             requireActivity(), | ||||
|             customSelectorLauncherForResult | ||||
|         ) | ||||
|         animateFAB(isFabOpen) | ||||
|     } | ||||
| 
 | ||||
|     fun scrollToTop() { | ||||
|         rvContributionsList!!.smoothScrollToPosition(0) | ||||
|     } | ||||
| 
 | ||||
|     private fun animateFAB(isFabOpen: Boolean) { | ||||
|         this.isFabOpen = !isFabOpen | ||||
|         if (binding!!.fabPlus.isShown) { | ||||
|             if (isFabOpen) { | ||||
|                 binding!!.fabPlus.startAnimation(rotate_backward) | ||||
|                 binding!!.fabCamera.startAnimation(fab_close) | ||||
|                 binding!!.fabGallery.startAnimation(fab_close) | ||||
|                 binding!!.fabCustomGallery.startAnimation(fab_close) | ||||
|                 binding!!.fabCamera.hide() | ||||
|                 binding!!.fabGallery.hide() | ||||
|                 binding!!.fabCustomGallery.hide() | ||||
|             } else { | ||||
|                 binding!!.fabPlus.startAnimation(rotate_forward) | ||||
|                 binding!!.fabCamera.startAnimation(fab_open) | ||||
|                 binding!!.fabGallery.startAnimation(fab_open) | ||||
|                 binding!!.fabCustomGallery.startAnimation(fab_open) | ||||
|                 binding!!.fabCamera.show() | ||||
|                 binding!!.fabGallery.show() | ||||
|                 binding!!.fabCustomGallery.show() | ||||
|             } | ||||
|             this.isFabOpen = !isFabOpen | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Shows welcome message if user has no contributions yet i.e. new user. | ||||
|      */ | ||||
|     override fun showWelcomeTip(shouldShow: Boolean) { | ||||
|         binding!!.noContributionsYet.visibility = | ||||
|             if (shouldShow) View.VISIBLE else View.GONE | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Responsible to set progress bar invisible and visible | ||||
|      * | ||||
|      * @param shouldShow True when contributions list should be hidden. | ||||
|      */ | ||||
|     override fun showProgress(shouldShow: Boolean) { | ||||
|         binding!!.loadingContributionsProgressBar.visibility = | ||||
|             if (shouldShow) View.VISIBLE else View.GONE | ||||
|     } | ||||
| 
 | ||||
|     override fun showNoContributionsUI(shouldShow: Boolean) { | ||||
|         binding!!.noContributionsYet.visibility = | ||||
|             if (shouldShow) View.VISIBLE else View.GONE | ||||
|     } | ||||
| 
 | ||||
|     override fun onSaveInstanceState(outState: Bundle) { | ||||
|         super.onSaveInstanceState(outState) | ||||
|         val layoutManager = rvContributionsList | ||||
|             ?.getLayoutManager() as GridLayoutManager? | ||||
|         outState.putParcelable(RV_STATE, layoutManager!!.onSaveInstanceState()) | ||||
|     } | ||||
| 
 | ||||
|     override fun onViewStateRestored(savedInstanceState: Bundle?) { | ||||
|         super.onViewStateRestored(savedInstanceState) | ||||
|         if (null != savedInstanceState) { | ||||
|             val savedRecyclerLayoutState = savedInstanceState.getParcelable<Parcelable>(RV_STATE) | ||||
|             rvContributionsList!!.layoutManager!!.onRestoreInstanceState(savedRecyclerLayoutState) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun openMediaDetail(position: Int, isWikipediaButtonDisplayed: Boolean) { | ||||
|         if (null != callback) { //Just being safe, ideally they won't be called when detached | ||||
|             callback!!.showDetail(position, isWikipediaButtonDisplayed) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Handle callback for wikipedia icon clicked | ||||
|      * | ||||
|      * @param contribution | ||||
|      */ | ||||
|     override fun addImageToWikipedia(contribution: Contribution?) { | ||||
|         showAlertDialog( | ||||
|             requireActivity(), | ||||
|             getString(fr.free.nrw.commons.R.string.add_picture_to_wikipedia_article_title), | ||||
|             getString(fr.free.nrw.commons.R.string.add_picture_to_wikipedia_article_desc), | ||||
|             { | ||||
|                 if (contribution != null) { | ||||
|                     showAddImageToWikipediaInstructions(contribution) | ||||
|                 } | ||||
|             }, {}) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Display confirmation dialog with instructions when the user tries to add image to wikipedia | ||||
|      * | ||||
|      * @param contribution | ||||
|      */ | ||||
|     private fun showAddImageToWikipediaInstructions(contribution: Contribution) { | ||||
|         val fragmentManager = fragmentManager | ||||
|         val fragment = newInstance(contribution) | ||||
|         fragment.callback = | ||||
|             WikipediaInstructionsDialogFragment.Callback { contribution: Contribution?, copyWikicode: Boolean -> | ||||
|                 this.onConfirmClicked( | ||||
|                     contribution, | ||||
|                     copyWikicode | ||||
|                 ) | ||||
|             } | ||||
|         fragment.show(fragmentManager!!, "WikimediaFragment") | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     fun getMediaAtPosition(i: Int): Media? { | ||||
|         if (adapter!!.getContributionForPosition(i) != null) { | ||||
|             return adapter!!.getContributionForPosition(i)!!.media | ||||
|         } | ||||
|         return null | ||||
|     } | ||||
| 
 | ||||
|     val totalMediaCount: Int | ||||
|         get() = contributionsSize | ||||
| 
 | ||||
|     /** | ||||
|      * Open the editor for the language Wikipedia | ||||
|      * | ||||
|      * @param contribution | ||||
|      */ | ||||
|     override fun onConfirmClicked(contribution: Contribution?, copyWikicode: Boolean) { | ||||
|         if (copyWikicode) { | ||||
|             val wikicode = contribution!!.media.wikiCode | ||||
|             Utils.copy("wikicode", wikicode, context) | ||||
|         } | ||||
| 
 | ||||
|         val url = | ||||
|             languageWikipediaSite!!.mobileUrl() + "/wiki/" + (contribution!!.wikidataPlace | ||||
|                 ?.getWikipediaPageTitle()) | ||||
|         Utils.handleWebUrl(context, Uri.parse(url)) | ||||
|     } | ||||
| 
 | ||||
|     fun getContributionStateAt(position: Int): Int { | ||||
|         return adapter!!.getContributionForPosition(position)!!.state | ||||
|     } | ||||
| 
 | ||||
|      interface Callback { | ||||
|         fun notifyDataSetChanged() | ||||
| 
 | ||||
|         fun showDetail(position: Int, isWikipediaButtonDisplayed: Boolean) | ||||
| 
 | ||||
|         // Notify the viewpager that number of items have changed. | ||||
|         fun viewPagerNotifyDataSetChanged() | ||||
|     } | ||||
| 
 | ||||
|     companion object { | ||||
|         private const val RV_STATE = "rv_scroll_state" | ||||
|     } | ||||
| } | ||||
|  | @ -1,112 +0,0 @@ | |||
| package fr.free.nrw.commons.contributions; | ||||
| 
 | ||||
| import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.lifecycle.LiveData; | ||||
| import androidx.paging.DataSource; | ||||
| import androidx.paging.DataSource.Factory; | ||||
| import androidx.paging.LivePagedListBuilder; | ||||
| import androidx.paging.PagedList; | ||||
| import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; | ||||
| import fr.free.nrw.commons.contributions.ContributionsListContract.UserActionListener; | ||||
| import io.reactivex.Scheduler; | ||||
| import io.reactivex.disposables.CompositeDisposable; | ||||
| import java.util.Collections; | ||||
| import javax.inject.Inject; | ||||
| import javax.inject.Named; | ||||
| import kotlin.Unit; | ||||
| import kotlin.jvm.functions.Function0; | ||||
| 
 | ||||
| /** | ||||
|  * The presenter class for Contributions | ||||
|  */ | ||||
| public class ContributionsListPresenter implements UserActionListener { | ||||
| 
 | ||||
|     private final ContributionBoundaryCallback contributionBoundaryCallback; | ||||
|     private final ContributionsRepository repository; | ||||
|     private final Scheduler ioThreadScheduler; | ||||
| 
 | ||||
|     private final CompositeDisposable compositeDisposable; | ||||
|     private final ContributionsRemoteDataSource contributionsRemoteDataSource; | ||||
| 
 | ||||
|     LiveData<PagedList<Contribution>> contributionList; | ||||
| 
 | ||||
|     @Inject | ||||
|     ContributionsListPresenter( | ||||
|         final ContributionBoundaryCallback contributionBoundaryCallback, | ||||
|         final ContributionsRemoteDataSource contributionsRemoteDataSource, | ||||
|         final ContributionsRepository repository, | ||||
|         @Named(IO_THREAD) final Scheduler ioThreadScheduler) { | ||||
|         this.contributionBoundaryCallback = contributionBoundaryCallback; | ||||
|         this.repository = repository; | ||||
|         this.ioThreadScheduler = ioThreadScheduler; | ||||
|         this.contributionsRemoteDataSource = contributionsRemoteDataSource; | ||||
|         compositeDisposable = new CompositeDisposable(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onAttachView(final ContributionsListContract.View view) { | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Setup the paged list. This method sets the configuration for paged list and ties it up with | ||||
|      * the live data object. This method can be tweaked to update the lazy loading behavior of the | ||||
|      * contributions list | ||||
|      */ | ||||
|     void setup(String userName, boolean isSelf) { | ||||
|         final PagedList.Config pagedListConfig = | ||||
|             (new PagedList.Config.Builder()) | ||||
|                 .setPrefetchDistance(50) | ||||
|                 .setPageSize(10).build(); | ||||
|         Factory<Integer, Contribution> factory; | ||||
|         boolean shouldSetBoundaryCallback; | ||||
|         if (!isSelf) { | ||||
|             //We don't want to persist contributions for other user's, therefore | ||||
|             // creating a new DataSource for them | ||||
|             contributionsRemoteDataSource.setUserName(userName); | ||||
|             factory = new Factory<Integer, Contribution>() { | ||||
|                 @NonNull | ||||
|                 @Override | ||||
|                 public DataSource<Integer, Contribution> create() { | ||||
|                     return contributionsRemoteDataSource; | ||||
|                 } | ||||
|             }; | ||||
|             shouldSetBoundaryCallback = false; | ||||
|         } else { | ||||
|             contributionBoundaryCallback.setUserName(userName); | ||||
|             shouldSetBoundaryCallback = true; | ||||
|             factory = repository.fetchContributionsWithStates( | ||||
|                 Collections.singletonList(Contribution.STATE_COMPLETED)); | ||||
|         } | ||||
| 
 | ||||
|         LivePagedListBuilder livePagedListBuilder = new LivePagedListBuilder(factory, | ||||
|             pagedListConfig); | ||||
|         if (shouldSetBoundaryCallback) { | ||||
|             livePagedListBuilder.setBoundaryCallback(contributionBoundaryCallback); | ||||
|         } | ||||
| 
 | ||||
|         contributionList = livePagedListBuilder.build(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onDetachView() { | ||||
|         compositeDisposable.clear(); | ||||
|         contributionsRemoteDataSource.dispose(); | ||||
|         contributionBoundaryCallback.dispose(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * It is used to refresh list. | ||||
|      * | ||||
|      * @param swipeRefreshLayout used to stop refresh animation when | ||||
|      * refresh finishes. | ||||
|      */ | ||||
|     @Override | ||||
|     public void refreshList(final SwipeRefreshLayout swipeRefreshLayout) { | ||||
|         contributionBoundaryCallback.refreshList(() -> { | ||||
|             swipeRefreshLayout.setRefreshing(false); | ||||
|             return Unit.INSTANCE; | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,91 @@ | |||
| package fr.free.nrw.commons.contributions | ||||
| 
 | ||||
| import androidx.lifecycle.LiveData | ||||
| import androidx.paging.DataSource | ||||
| import androidx.paging.LivePagedListBuilder | ||||
| import androidx.paging.PagedList | ||||
| import androidx.swiperefreshlayout.widget.SwipeRefreshLayout | ||||
| import fr.free.nrw.commons.di.CommonsApplicationModule | ||||
| import io.reactivex.Scheduler | ||||
| import io.reactivex.disposables.CompositeDisposable | ||||
| import javax.inject.Inject | ||||
| import javax.inject.Named | ||||
| 
 | ||||
| /** | ||||
|  * The presenter class for Contributions | ||||
|  */ | ||||
| class ContributionsListPresenter @Inject internal constructor( | ||||
|     private val contributionBoundaryCallback: ContributionBoundaryCallback, | ||||
|     private val contributionsRemoteDataSource: ContributionsRemoteDataSource, | ||||
|     private val repository: ContributionsRepository, | ||||
|     @param:Named(CommonsApplicationModule.IO_THREAD) private val ioThreadScheduler: Scheduler | ||||
| ) : ContributionsListContract.UserActionListener { | ||||
|     private val compositeDisposable = CompositeDisposable() | ||||
| 
 | ||||
|     var contributionList: LiveData<PagedList<Contribution>>? = null | ||||
| 
 | ||||
|     override fun onAttachView(view: ContributionsListContract.View) { | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Setup the paged list. This method sets the configuration for paged list and ties it up with | ||||
|      * the live data object. This method can be tweaked to update the lazy loading behavior of the | ||||
|      * contributions list | ||||
|      */ | ||||
|     fun setup(userName: String?, isSelf: Boolean) { | ||||
|         val pagedListConfig = | ||||
|             (PagedList.Config.Builder()) | ||||
|                 .setPrefetchDistance(50) | ||||
|                 .setPageSize(10).build() | ||||
|         val factory: DataSource.Factory<Int, Contribution> | ||||
|         val shouldSetBoundaryCallback: Boolean | ||||
|         if (!isSelf) { | ||||
|             //We don't want to persist contributions for other user's, therefore | ||||
|             // creating a new DataSource for them | ||||
|             contributionsRemoteDataSource.userName = userName | ||||
|             factory = object : DataSource.Factory<Int, Contribution>() { | ||||
|                 override fun create(): DataSource<Int, Contribution> { | ||||
|                     return contributionsRemoteDataSource | ||||
|                 } | ||||
|             } | ||||
|             shouldSetBoundaryCallback = false | ||||
|         } else { | ||||
|             contributionBoundaryCallback.userName = userName | ||||
|             shouldSetBoundaryCallback = true | ||||
|             factory = repository.fetchContributionsWithStates( | ||||
|                 listOf(Contribution.STATE_COMPLETED) | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         val livePagedListBuilder: LivePagedListBuilder<Int, Contribution> = LivePagedListBuilder( | ||||
|             factory, | ||||
|             pagedListConfig | ||||
|         ) | ||||
|         if (shouldSetBoundaryCallback) { | ||||
|             livePagedListBuilder.setBoundaryCallback(contributionBoundaryCallback) | ||||
|         } | ||||
| 
 | ||||
|         contributionList = livePagedListBuilder.build() | ||||
|     } | ||||
| 
 | ||||
|     override fun onDetachView() { | ||||
|         compositeDisposable.clear() | ||||
|         contributionsRemoteDataSource.dispose() | ||||
|         contributionBoundaryCallback.dispose() | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * It is used to refresh list. | ||||
|      * | ||||
|      * @param swipeRefreshLayout used to stop refresh animation when | ||||
|      * refresh finishes. | ||||
|      */ | ||||
|     override fun refreshList(swipeRefreshLayout: SwipeRefreshLayout?) { | ||||
|         contributionBoundaryCallback.refreshList { | ||||
|             if (swipeRefreshLayout != null) { | ||||
|                 swipeRefreshLayout.isRefreshing = false | ||||
|             } | ||||
|             Unit | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -1,131 +0,0 @@ | |||
| package fr.free.nrw.commons.contributions; | ||||
| 
 | ||||
| import androidx.paging.DataSource.Factory; | ||||
| import fr.free.nrw.commons.kvstore.JsonKvStore; | ||||
| import io.reactivex.Completable; | ||||
| import io.reactivex.Single; | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
| import javax.inject.Inject; | ||||
| import javax.inject.Named; | ||||
| 
 | ||||
| /** | ||||
|  * The LocalDataSource class for Contributions | ||||
|  */ | ||||
| class ContributionsLocalDataSource { | ||||
| 
 | ||||
|     private final ContributionDao contributionDao; | ||||
|     private final JsonKvStore defaultKVStore; | ||||
| 
 | ||||
|     @Inject | ||||
|     public ContributionsLocalDataSource( | ||||
|         @Named("default_preferences") final JsonKvStore defaultKVStore, | ||||
|         final ContributionDao contributionDao) { | ||||
|         this.defaultKVStore = defaultKVStore; | ||||
|         this.contributionDao = contributionDao; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetch default number of contributions to be show, based on user preferences | ||||
|      */ | ||||
|     public String getString(final String key) { | ||||
|         return defaultKVStore.getString(key); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetch default number of contributions to be show, based on user preferences | ||||
|      */ | ||||
|     public long getLong(final String key) { | ||||
|         return defaultKVStore.getLong(key); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get contribution object from cursor | ||||
|      * | ||||
|      * @param uri | ||||
|      * @return | ||||
|      */ | ||||
|     public Contribution getContributionWithFileName(final String uri) { | ||||
|         final List<Contribution> contributionWithUri = contributionDao.getContributionWithTitle( | ||||
|             uri); | ||||
|         if (!contributionWithUri.isEmpty()) { | ||||
|             return contributionWithUri.get(0); | ||||
|         } | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Remove a contribution from the contributions table | ||||
|      * | ||||
|      * @param contribution | ||||
|      * @return | ||||
|      */ | ||||
|     public Completable deleteContribution(final Contribution contribution) { | ||||
|         return contributionDao.delete(contribution); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Deletes contributions with specific states. | ||||
|      * | ||||
|      * @param states The states of the contributions to delete. | ||||
|      * @return A Completable indicating the result of the operation. | ||||
|      */ | ||||
|     public Completable deleteContributionsWithStates(List<Integer> states) { | ||||
|         return contributionDao.deleteContributionsWithStates(states); | ||||
|     } | ||||
| 
 | ||||
|     public Factory<Integer, Contribution> getContributions() { | ||||
|         return contributionDao.fetchContributions(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetches contributions with specific states. | ||||
|      * | ||||
|      * @param states The states of the contributions to fetch. | ||||
|      * @return A DataSource factory for paginated contributions with the specified states. | ||||
|      */ | ||||
|     public Factory<Integer, Contribution> getContributionsWithStates(List<Integer> states) { | ||||
|         return contributionDao.getContributions(states); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetches contributions with specific states sorted by the date the upload started. | ||||
|      * | ||||
|      * @param states The states of the contributions to fetch. | ||||
|      * @return A DataSource factory for paginated contributions with the specified states sorted by | ||||
|      * date upload started. | ||||
|      */ | ||||
|     public Factory<Integer, Contribution> getContributionsWithStatesSortedByDateUploadStarted( | ||||
|         List<Integer> states) { | ||||
|         return contributionDao.getContributionsSortedByDateUploadStarted(states); | ||||
|     } | ||||
| 
 | ||||
|     public Single<List<Long>> saveContributions(final List<Contribution> contributions) { | ||||
|         final List<Contribution> contributionList = new ArrayList<>(); | ||||
|         for (final Contribution contribution : contributions) { | ||||
|             final Contribution oldContribution = contributionDao.getContribution( | ||||
|                 contribution.getPageId()); | ||||
|             if (oldContribution != null) { | ||||
|                 contribution.setWikidataPlace(oldContribution.getWikidataPlace()); | ||||
|             } | ||||
|             contributionList.add(contribution); | ||||
|         } | ||||
|         return contributionDao.save(contributionList); | ||||
|     } | ||||
| 
 | ||||
|     public Completable saveContributions(Contribution contribution) { | ||||
|         return contributionDao.save(contribution); | ||||
|     } | ||||
| 
 | ||||
|     public void set(final String key, final long value) { | ||||
|         defaultKVStore.putLong(key, value); | ||||
|     } | ||||
| 
 | ||||
|     public Completable updateContribution(final Contribution contribution) { | ||||
|         return contributionDao.update(contribution); | ||||
|     } | ||||
| 
 | ||||
|     public Completable updateContributionsWithStates(List<Integer> states, int newState) { | ||||
|         return contributionDao.updateContributionsWithStates(states, newState); | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,121 @@ | |||
| package fr.free.nrw.commons.contributions | ||||
| 
 | ||||
| import androidx.paging.DataSource | ||||
| import fr.free.nrw.commons.kvstore.JsonKvStore | ||||
| import io.reactivex.Completable | ||||
| import io.reactivex.Single | ||||
| import javax.inject.Inject | ||||
| import javax.inject.Named | ||||
| 
 | ||||
| /** | ||||
|  * The LocalDataSource class for Contributions | ||||
|  */ | ||||
| class ContributionsLocalDataSource @Inject constructor( | ||||
|     @param:Named("default_preferences") private val defaultKVStore: JsonKvStore, | ||||
|     private val contributionDao: ContributionDao | ||||
| ) { | ||||
|     /** | ||||
|      * Fetch default number of contributions to be show, based on user preferences | ||||
|      */ | ||||
|     fun getString(key: String): String? { | ||||
|         return defaultKVStore.getString(key) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetch default number of contributions to be show, based on user preferences | ||||
|      */ | ||||
|     fun getLong(key: String): Long { | ||||
|         return defaultKVStore.getLong(key) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get contribution object from cursor | ||||
|      * | ||||
|      * @param uri | ||||
|      * @return | ||||
|      */ | ||||
|     fun getContributionWithFileName(uri: String): Contribution { | ||||
|         val contributionWithUri = contributionDao.getContributionWithTitle(uri) | ||||
|         if (contributionWithUri.isNotEmpty()) { | ||||
|             return contributionWithUri[0] | ||||
|         } | ||||
|         throw IllegalArgumentException("Contribution not found for URI: $uri") | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Remove a contribution from the contributions table | ||||
|      * | ||||
|      * @param contribution | ||||
|      * @return | ||||
|      */ | ||||
|     fun deleteContribution(contribution: Contribution): Completable { | ||||
|         return contributionDao.delete(contribution) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Deletes contributions with specific states. | ||||
|      * | ||||
|      * @param states The states of the contributions to delete. | ||||
|      * @return A Completable indicating the result of the operation. | ||||
|      */ | ||||
|     fun deleteContributionsWithStates(states: List<Int>): Completable { | ||||
|         return contributionDao.deleteContributionsWithStates(states) | ||||
|     } | ||||
| 
 | ||||
|     fun getContributions(): DataSource.Factory<Int, Contribution> { | ||||
|         return contributionDao.fetchContributions() | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetches contributions with specific states. | ||||
|      * | ||||
|      * @param states The states of the contributions to fetch. | ||||
|      * @return A DataSource factory for paginated contributions with the specified states. | ||||
|      */ | ||||
|     fun getContributionsWithStates(states: List<Int>): DataSource.Factory<Int, Contribution> { | ||||
|         return contributionDao.getContributions(states) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetches contributions with specific states sorted by the date the upload started. | ||||
|      * | ||||
|      * @param states The states of the contributions to fetch. | ||||
|      * @return A DataSource factory for paginated contributions with the specified states sorted by | ||||
|      * date upload started. | ||||
|      */ | ||||
|     fun getContributionsWithStatesSortedByDateUploadStarted( | ||||
|         states: List<Int> | ||||
|     ): DataSource.Factory<Int, Contribution> { | ||||
|         return contributionDao.getContributionsSortedByDateUploadStarted(states) | ||||
|     } | ||||
| 
 | ||||
|     fun saveContributions(contributions: List<Contribution>): Single<List<Long>> { | ||||
|         val contributionList: MutableList<Contribution> = ArrayList() | ||||
|         for (contribution in contributions) { | ||||
|             val oldContribution = contributionDao.getContribution( | ||||
|                 contribution.pageId | ||||
|             ) | ||||
|             if (oldContribution != null) { | ||||
|                 contribution.wikidataPlace = oldContribution.wikidataPlace | ||||
|             } | ||||
|             contributionList.add(contribution) | ||||
|         } | ||||
|         return contributionDao.save(contributionList) | ||||
|     } | ||||
| 
 | ||||
|     fun saveContributions(contribution: Contribution): Completable { | ||||
|         return contributionDao.save(contribution) | ||||
|     } | ||||
| 
 | ||||
|     fun set(key: String, value: Long) { | ||||
|         defaultKVStore.putLong(key, value) | ||||
|     } | ||||
| 
 | ||||
|     fun updateContribution(contribution: Contribution): Completable { | ||||
|         return contributionDao.update(contribution) | ||||
|     } | ||||
| 
 | ||||
|     fun updateContributionsWithStates(states: List<Int>, newState: Int): Completable { | ||||
|         return contributionDao.updateContributionsWithStates(states, newState) | ||||
|     } | ||||
| } | ||||
|  | @ -1,15 +0,0 @@ | |||
| package fr.free.nrw.commons.contributions; | ||||
| 
 | ||||
| import dagger.Binds; | ||||
| import dagger.Module; | ||||
| 
 | ||||
| /** | ||||
|  * The Dagger Module for contributions related presenters and (some other objects maybe in future) | ||||
|  */ | ||||
| @Module | ||||
| public abstract class ContributionsModule { | ||||
| 
 | ||||
|     @Binds | ||||
|     public abstract ContributionsContract.UserActionListener bindsContibutionsPresenter( | ||||
|             ContributionsPresenter presenter); | ||||
| } | ||||
|  | @ -0,0 +1,16 @@ | |||
| package fr.free.nrw.commons.contributions | ||||
| 
 | ||||
| import dagger.Binds | ||||
| import dagger.Module | ||||
| 
 | ||||
| /** | ||||
|  * The Dagger Module for contributions-related presenters and other dependencies | ||||
|  */ | ||||
| @Module | ||||
| abstract class ContributionsModule { | ||||
| 
 | ||||
|     @Binds | ||||
|     abstract fun bindsContributionsPresenter( | ||||
|         presenter: ContributionsPresenter? | ||||
|     ): ContributionsContract.UserActionListener? | ||||
| } | ||||
|  | @ -1,97 +0,0 @@ | |||
| package fr.free.nrw.commons.contributions; | ||||
| 
 | ||||
| import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD; | ||||
| import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK; | ||||
| 
 | ||||
| import androidx.work.ExistingWorkPolicy; | ||||
| import fr.free.nrw.commons.MediaDataExtractor; | ||||
| import fr.free.nrw.commons.contributions.ContributionsContract.UserActionListener; | ||||
| import fr.free.nrw.commons.di.CommonsApplicationModule; | ||||
| import fr.free.nrw.commons.repository.UploadRepository; | ||||
| import fr.free.nrw.commons.upload.worker.WorkRequestHelper; | ||||
| import io.reactivex.Scheduler; | ||||
| import io.reactivex.disposables.CompositeDisposable; | ||||
| import javax.inject.Inject; | ||||
| import javax.inject.Named; | ||||
| import timber.log.Timber; | ||||
| 
 | ||||
| /** | ||||
|  * The presenter class for Contributions | ||||
|  */ | ||||
| public class ContributionsPresenter implements UserActionListener { | ||||
| 
 | ||||
|     private final ContributionsRepository contributionsRepository; | ||||
|     private final UploadRepository uploadRepository; | ||||
|     private final Scheduler ioThreadScheduler; | ||||
|     private CompositeDisposable compositeDisposable; | ||||
|     private ContributionsContract.View view; | ||||
| 
 | ||||
|     @Inject | ||||
|     MediaDataExtractor mediaDataExtractor; | ||||
| 
 | ||||
|     @Inject | ||||
|     ContributionsPresenter(ContributionsRepository repository, | ||||
|         UploadRepository uploadRepository, | ||||
|         @Named(IO_THREAD) Scheduler ioThreadScheduler) { | ||||
|         this.contributionsRepository = repository; | ||||
|         this.uploadRepository = uploadRepository; | ||||
|         this.ioThreadScheduler = ioThreadScheduler; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onAttachView(ContributionsContract.View view) { | ||||
|         this.view = view; | ||||
|         compositeDisposable = new CompositeDisposable(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onDetachView() { | ||||
|         this.view = null; | ||||
|         compositeDisposable.clear(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public Contribution getContributionsWithTitle(String title) { | ||||
|         return contributionsRepository.getContributionWithFileName(title); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Checks if a contribution is a duplicate and restarts the contribution process if it is not. | ||||
|      * | ||||
|      * @param contribution The contribution to check and potentially restart. | ||||
|      */ | ||||
|     public void checkDuplicateImageAndRestartContribution(Contribution contribution) { | ||||
|         compositeDisposable.add(uploadRepository | ||||
|             .checkDuplicateImage( | ||||
|                 contribution.getContentUri(), | ||||
|                 contribution.getLocalUri() | ||||
|             ) | ||||
|             .subscribeOn(ioThreadScheduler) | ||||
|             .subscribe(imageCheckResult -> { | ||||
|                 if (imageCheckResult == IMAGE_OK) { | ||||
|                     contribution.setState(Contribution.STATE_QUEUED); | ||||
|                     saveContribution(contribution); | ||||
|                 } else { | ||||
|                     Timber.e("Contribution already exists"); | ||||
|                     compositeDisposable.add(contributionsRepository | ||||
|                         .deleteContributionFromDB(contribution) | ||||
|                         .subscribeOn(ioThreadScheduler) | ||||
|                         .subscribe()); | ||||
|                 } | ||||
|             })); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Update the contribution's state in the databse, upon completion, trigger the workmanager to | ||||
|      * process this contribution | ||||
|      * | ||||
|      * @param contribution | ||||
|      */ | ||||
|     public void saveContribution(Contribution contribution) { | ||||
|         compositeDisposable.add(contributionsRepository | ||||
|             .save(contribution) | ||||
|             .subscribeOn(ioThreadScheduler) | ||||
|             .subscribe(() -> WorkRequestHelper.Companion.makeOneTimeWorkRequest( | ||||
|                 view.getContext().getApplicationContext(), ExistingWorkPolicy.KEEP))); | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,88 @@ | |||
| package fr.free.nrw.commons.contributions | ||||
| 
 | ||||
| import androidx.work.ExistingWorkPolicy | ||||
| import fr.free.nrw.commons.MediaDataExtractor | ||||
| import fr.free.nrw.commons.di.CommonsApplicationModule | ||||
| import fr.free.nrw.commons.repository.UploadRepository | ||||
| import fr.free.nrw.commons.upload.worker.WorkRequestHelper.Companion.makeOneTimeWorkRequest | ||||
| import fr.free.nrw.commons.utils.ImageUtils | ||||
| import io.reactivex.Scheduler | ||||
| import io.reactivex.disposables.CompositeDisposable | ||||
| import timber.log.Timber | ||||
| import javax.inject.Inject | ||||
| import javax.inject.Named | ||||
| 
 | ||||
| /** | ||||
|  * The presenter class for Contributions | ||||
|  */ | ||||
| class ContributionsPresenter @Inject internal constructor( | ||||
|     private val contributionsRepository: ContributionsRepository, | ||||
|     private val uploadRepository: UploadRepository, | ||||
|     @param:Named(CommonsApplicationModule.IO_THREAD) private val ioThreadScheduler: Scheduler | ||||
| ) : ContributionsContract.UserActionListener { | ||||
|     private var compositeDisposable: CompositeDisposable? = null | ||||
|     private var view: ContributionsContract.View? = null | ||||
| 
 | ||||
|     @JvmField | ||||
|     @Inject | ||||
|     var mediaDataExtractor: MediaDataExtractor? = null | ||||
| 
 | ||||
|     override fun onAttachView(view: ContributionsContract.View) { | ||||
|         this.view = view | ||||
|         compositeDisposable = CompositeDisposable() | ||||
|     } | ||||
| 
 | ||||
|     override fun onDetachView() { | ||||
|         this.view = null | ||||
|         compositeDisposable!!.clear() | ||||
|     } | ||||
| 
 | ||||
|     override fun getContributionsWithTitle(title: String): Contribution { | ||||
|         return contributionsRepository.getContributionWithFileName(title) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Checks if a contribution is a duplicate and restarts the contribution process if it is not. | ||||
|      * | ||||
|      * @param contribution The contribution to check and potentially restart. | ||||
|      */ | ||||
|     fun checkDuplicateImageAndRestartContribution(contribution: Contribution) { | ||||
|         compositeDisposable!!.add( | ||||
|             uploadRepository | ||||
|                 .checkDuplicateImage( | ||||
|                     contribution.contentUri, | ||||
|                     contribution.localUri) | ||||
|                 .subscribeOn(ioThreadScheduler) | ||||
|                 .subscribe { imageCheckResult: Int -> | ||||
|                     if (imageCheckResult == ImageUtils.IMAGE_OK) { | ||||
|                         contribution.state = Contribution.STATE_QUEUED | ||||
|                         saveContribution(contribution) | ||||
|                     } else { | ||||
|                         Timber.e("Contribution already exists") | ||||
|                         compositeDisposable!!.add( | ||||
|                             contributionsRepository | ||||
|                                 .deleteContributionFromDB(contribution) | ||||
|                                 .subscribeOn(ioThreadScheduler) | ||||
|                                 .subscribe() | ||||
|                         ) | ||||
|                     } | ||||
|                 }) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Update the contribution's state in the databse, upon completion, trigger the workmanager to | ||||
|      * process this contribution | ||||
|      * | ||||
|      * @param contribution | ||||
|      */ | ||||
|     fun saveContribution(contribution: Contribution) { | ||||
|         compositeDisposable!!.add(contributionsRepository | ||||
|             .save(contribution) | ||||
|             .subscribeOn(ioThreadScheduler) | ||||
|             .subscribe { | ||||
|                 makeOneTimeWorkRequest( | ||||
|                     view!!.getContext().applicationContext, ExistingWorkPolicy.KEEP | ||||
|                 ) | ||||
|             }) | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,28 @@ | |||
| package fr.free.nrw.commons.contributions | ||||
| 
 | ||||
| import dagger.Module | ||||
| import dagger.Provides | ||||
| import fr.free.nrw.commons.kvstore.JsonKvStore | ||||
| import fr.free.nrw.commons.wikidata.model.WikiSite | ||||
| import javax.inject.Named | ||||
| 
 | ||||
| /** | ||||
|  * The Dagger Module for contributions-related providers | ||||
|  */ | ||||
| @Module | ||||
| class ContributionsProvidesModule { | ||||
| 
 | ||||
|     @Provides | ||||
|     fun providesApplicationKvStore( | ||||
|         @Named("default_preferences") kvStore: JsonKvStore | ||||
|     ): JsonKvStore { | ||||
|         return kvStore | ||||
|     } | ||||
| 
 | ||||
|     @Provides | ||||
|     fun providesLanguageWikipediaSite( | ||||
|         @Named("language-wikipedia-wikisite") languageWikipediaSite: WikiSite | ||||
|     ): WikiSite { | ||||
|         return languageWikipediaSite | ||||
|     } | ||||
| } | ||||
|  | @ -1,112 +0,0 @@ | |||
| package fr.free.nrw.commons.contributions; | ||||
| 
 | ||||
| import androidx.paging.DataSource.Factory; | ||||
| import io.reactivex.Completable; | ||||
| import java.util.List; | ||||
| 
 | ||||
| import javax.inject.Inject; | ||||
| 
 | ||||
| import io.reactivex.Single; | ||||
| 
 | ||||
| /** | ||||
|  * The repository class for contributions | ||||
|  */ | ||||
| public class ContributionsRepository { | ||||
| 
 | ||||
|     private ContributionsLocalDataSource localDataSource; | ||||
| 
 | ||||
|     @Inject | ||||
|     public ContributionsRepository(ContributionsLocalDataSource localDataSource) { | ||||
|         this.localDataSource = localDataSource; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetch default number of contributions to be show, based on user preferences | ||||
|      */ | ||||
|     public String getString(String key) { | ||||
|         return localDataSource.getString(key); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Deletes a failed upload from DB | ||||
|      * | ||||
|      * @param contribution | ||||
|      * @return | ||||
|      */ | ||||
|     public Completable deleteContributionFromDB(Contribution contribution) { | ||||
|         return localDataSource.deleteContribution(contribution); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Deletes contributions from the database with specific states. | ||||
|      * | ||||
|      * @param states The states of the contributions to delete. | ||||
|      * @return A Completable indicating the result of the operation. | ||||
|      */ | ||||
|     public Completable deleteContributionsFromDBWithStates(List<Integer> states) { | ||||
|         return localDataSource.deleteContributionsWithStates(states); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get contribution object with title | ||||
|      * | ||||
|      * @param fileName | ||||
|      * @return | ||||
|      */ | ||||
|     public Contribution getContributionWithFileName(String fileName) { | ||||
|         return localDataSource.getContributionWithFileName(fileName); | ||||
|     } | ||||
| 
 | ||||
|     public Factory<Integer, Contribution> fetchContributions() { | ||||
|         return localDataSource.getContributions(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetches contributions with specific states. | ||||
|      * | ||||
|      * @param states The states of the contributions to fetch. | ||||
|      * @return A DataSource factory for paginated contributions with the specified states. | ||||
|      */ | ||||
|     public Factory<Integer, Contribution> fetchContributionsWithStates(List<Integer> states) { | ||||
|         return localDataSource.getContributionsWithStates(states); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetches contributions with specific states sorted by the date the upload started. | ||||
|      * | ||||
|      * @param states The states of the contributions to fetch. | ||||
|      * @return A DataSource factory for paginated contributions with the specified states sorted by | ||||
|      * date upload started. | ||||
|      */ | ||||
|     public Factory<Integer, Contribution> fetchContributionsWithStatesSortedByDateUploadStarted( | ||||
|         List<Integer> states) { | ||||
|         return localDataSource.getContributionsWithStatesSortedByDateUploadStarted(states); | ||||
|     } | ||||
| 
 | ||||
|     public Single<List<Long>> save(List<Contribution> contributions) { | ||||
|         return localDataSource.saveContributions(contributions); | ||||
|     } | ||||
| 
 | ||||
|     public Completable save(Contribution contributions) { | ||||
|         return localDataSource.saveContributions(contributions); | ||||
|     } | ||||
| 
 | ||||
|     public void set(String key, long value) { | ||||
|         localDataSource.set(key, value); | ||||
|     } | ||||
| 
 | ||||
|     public Completable updateContribution(Contribution contribution) { | ||||
|         return localDataSource.updateContribution(contribution); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Updates the state of contributions with specific states. | ||||
|      * | ||||
|      * @param states   The current states of the contributions to update. | ||||
|      * @param newState The new state to set. | ||||
|      * @return A Completable indicating the result of the operation. | ||||
|      */ | ||||
|     public Completable updateContributionsWithStates(List<Integer> states, int newState) { | ||||
|         return localDataSource.updateContributionsWithStates(states, newState); | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,102 @@ | |||
| package fr.free.nrw.commons.contributions | ||||
| 
 | ||||
| import androidx.paging.DataSource | ||||
| import io.reactivex.Completable | ||||
| import io.reactivex.Single | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| /** | ||||
|  * The repository class for contributions | ||||
|  */ | ||||
| class ContributionsRepository @Inject constructor(private val localDataSource: ContributionsLocalDataSource) { | ||||
|     /** | ||||
|      * Fetch default number of contributions to be show, based on user preferences | ||||
|      */ | ||||
|     fun getString(key: String): String? { | ||||
|         return localDataSource.getString(key) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Deletes a failed upload from DB | ||||
|      * | ||||
|      * @param contribution | ||||
|      * @return | ||||
|      */ | ||||
|     fun deleteContributionFromDB(contribution: Contribution): Completable { | ||||
|         return localDataSource.deleteContribution(contribution) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Deletes contributions from the database with specific states. | ||||
|      * | ||||
|      * @param states The states of the contributions to delete. | ||||
|      * @return A Completable indicating the result of the operation. | ||||
|      */ | ||||
|     fun deleteContributionsFromDBWithStates(states: List<Int>): Completable { | ||||
|         return localDataSource.deleteContributionsWithStates(states) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get contribution object with title | ||||
|      * | ||||
|      * @param fileName | ||||
|      * @return | ||||
|      */ | ||||
|     fun getContributionWithFileName(fileName: String): Contribution { | ||||
|         return localDataSource.getContributionWithFileName(fileName) | ||||
|     } | ||||
| 
 | ||||
|     fun fetchContributions(): DataSource.Factory<Int, Contribution> { | ||||
|         return localDataSource.getContributions() | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetches contributions with specific states. | ||||
|      * | ||||
|      * @param states The states of the contributions to fetch. | ||||
|      * @return A DataSource factory for paginated contributions with the specified states. | ||||
|      */ | ||||
|     fun fetchContributionsWithStates(states: List<Int>): DataSource.Factory<Int, Contribution> { | ||||
|         return localDataSource.getContributionsWithStates(states) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetches contributions with specific states sorted by the date the upload started. | ||||
|      * | ||||
|      * @param states The states of the contributions to fetch. | ||||
|      * @return A DataSource factory for paginated contributions with the specified states sorted by | ||||
|      * date upload started. | ||||
|      */ | ||||
|     fun fetchContributionsWithStatesSortedByDateUploadStarted( | ||||
|         states: List<Int> | ||||
|     ): DataSource.Factory<Int, Contribution> { | ||||
|         return localDataSource.getContributionsWithStatesSortedByDateUploadStarted(states) | ||||
|     } | ||||
| 
 | ||||
|     fun save(contributions: List<Contribution>): Single<List<Long>> { | ||||
|         return localDataSource.saveContributions(contributions) | ||||
|     } | ||||
| 
 | ||||
|     fun save(contributions: Contribution): Completable { | ||||
|         return localDataSource.saveContributions(contributions) | ||||
|     } | ||||
| 
 | ||||
|     operator fun set(key: String, value: Long) { | ||||
|         localDataSource.set(key, value) | ||||
|     } | ||||
| 
 | ||||
|     fun updateContribution(contribution: Contribution): Completable { | ||||
|         return localDataSource.updateContribution(contribution) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Updates the state of contributions with specific states. | ||||
|      * | ||||
|      * @param states   The current states of the contributions to update. | ||||
|      * @param newState The new state to set. | ||||
|      * @return A Completable indicating the result of the operation. | ||||
|      */ | ||||
|     fun updateContributionsWithStates(states: List<Int>, newState: Int): Completable { | ||||
|         return localDataSource.updateContributionsWithStates(states, newState) | ||||
|     } | ||||
| } | ||||
|  | @ -1,550 +0,0 @@ | |||
| package fr.free.nrw.commons.contributions; | ||||
| 
 | ||||
| import android.annotation.SuppressLint; | ||||
| import android.app.Activity; | ||||
| import android.content.Context; | ||||
| import android.content.Intent; | ||||
| import android.content.SharedPreferences; | ||||
| import android.os.Bundle; | ||||
| import android.view.Menu; | ||||
| import android.view.MenuItem; | ||||
| import android.view.View; | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.fragment.app.Fragment; | ||||
| import androidx.fragment.app.FragmentManager; | ||||
| import androidx.work.ExistingWorkPolicy; | ||||
| import fr.free.nrw.commons.databinding.MainBinding; | ||||
| import fr.free.nrw.commons.R; | ||||
| import fr.free.nrw.commons.WelcomeActivity; | ||||
| import fr.free.nrw.commons.auth.SessionManager; | ||||
| 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.MediaDetailPagerFragment; | ||||
| import fr.free.nrw.commons.navtab.MoreBottomSheetFragment; | ||||
| import fr.free.nrw.commons.navtab.MoreBottomSheetLoggedOutFragment; | ||||
| import fr.free.nrw.commons.navtab.NavTab; | ||||
| import fr.free.nrw.commons.navtab.NavTabLayout; | ||||
| import fr.free.nrw.commons.navtab.NavTabLoggedOut; | ||||
| 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.NotificationController; | ||||
| import fr.free.nrw.commons.quiz.QuizChecker; | ||||
| import fr.free.nrw.commons.settings.SettingsFragment; | ||||
| import fr.free.nrw.commons.theme.BaseActivity; | ||||
| import fr.free.nrw.commons.upload.UploadProgressActivity; | ||||
| 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.schedulers.Schedulers; | ||||
| import java.util.Calendar; | ||||
| import java.util.Collections; | ||||
| import java.util.List; | ||||
| import javax.inject.Inject; | ||||
| import javax.inject.Named; | ||||
| import timber.log.Timber; | ||||
| 
 | ||||
| public class MainActivity extends BaseActivity | ||||
|     implements FragmentManager.OnBackStackChangedListener { | ||||
| 
 | ||||
|     @Inject | ||||
|     SessionManager sessionManager; | ||||
|     @Inject | ||||
|     ContributionController controller; | ||||
|     @Inject | ||||
|     ContributionDao contributionDao; | ||||
| 
 | ||||
|     private ContributionsFragment contributionsFragment; | ||||
|     private NearbyParentFragment nearbyParentFragment; | ||||
|     private ExploreFragment exploreFragment; | ||||
|     private BookmarkFragment bookmarkFragment; | ||||
|     public ActiveFragment activeFragment; | ||||
|     private MediaDetailPagerFragment mediaDetailPagerFragment; | ||||
|     private NavTabLayout.OnNavigationItemSelectedListener navListener; | ||||
| 
 | ||||
|     @Inject | ||||
|     public LocationServiceManager locationManager; | ||||
|     @Inject | ||||
|     NotificationController notificationController; | ||||
|     @Inject | ||||
|     QuizChecker quizChecker; | ||||
|     @Inject | ||||
|     @Named("default_preferences") | ||||
|     public | ||||
|     JsonKvStore applicationKvStore; | ||||
|     @Inject | ||||
|     ViewUtilWrapper viewUtilWrapper; | ||||
| 
 | ||||
|     public Menu menu; | ||||
| 
 | ||||
|     public MainBinding binding; | ||||
| 
 | ||||
|     NavTabLayout tabLayout; | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * Consumers should be simply using this method to use this activity. | ||||
|      * | ||||
|      * @param context A Context of the application package implementing this class. | ||||
|      */ | ||||
|     public static void startYourself(Context context) { | ||||
|         Intent intent = new Intent(context, MainActivity.class); | ||||
|         intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_SINGLE_TOP); | ||||
|         context.startActivity(intent); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public boolean onSupportNavigateUp() { | ||||
|         if (activeFragment == ActiveFragment.CONTRIBUTIONS) { | ||||
|             if (!contributionsFragment.backButtonClicked()) { | ||||
|                 return false; | ||||
|             } | ||||
|         } else { | ||||
|             onBackPressed(); | ||||
|             showTabs(); | ||||
|         } | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onCreate(Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         binding = MainBinding.inflate(getLayoutInflater()); | ||||
|         setContentView(binding.getRoot()); | ||||
|         setSupportActionBar(binding.toolbarBinding.toolbar); | ||||
|         tabLayout = binding.fragmentMainNavTabLayout; | ||||
|         loadLocale(); | ||||
| 
 | ||||
|         binding.toolbarBinding.toolbar.setNavigationOnClickListener(view -> { | ||||
|             onSupportNavigateUp(); | ||||
|         }); | ||||
|         /* | ||||
|         "first_edit_depict" is a key for getting information about opening the depiction editor | ||||
|         screen for the first time after opening the app. | ||||
| 
 | ||||
|         Getting true by the key means the depiction editor screen is opened for the first time | ||||
|         after opening the app. | ||||
|         Getting false by the key means the depiction editor screen is not opened for the first time | ||||
|         after opening the app. | ||||
|          */ | ||||
|         applicationKvStore.putBoolean("first_edit_depict", true); | ||||
|         if (applicationKvStore.getBoolean("login_skipped") == true) { | ||||
|             setTitle(getString(R.string.navigation_item_explore)); | ||||
|             setUpLoggedOutPager(); | ||||
|         } else { | ||||
|             if (applicationKvStore.getBoolean("firstrun", true)) { | ||||
|                 applicationKvStore.putBoolean("hasAlreadyLaunchedBigMultiupload", false); | ||||
|                 applicationKvStore.putBoolean("hasAlreadyLaunchedCategoriesDialog", false); | ||||
|             } | ||||
|             if (savedInstanceState == null) { | ||||
|                 //starting a fresh fragment. | ||||
|                 // Open Last opened screen if it is Contributions or Nearby, otherwise Contributions | ||||
|                 if (applicationKvStore.getBoolean("last_opened_nearby")) { | ||||
|                     setTitle(getString(R.string.nearby_fragment)); | ||||
|                     showNearby(); | ||||
|                     loadFragment(NearbyParentFragment.newInstance(), false); | ||||
|                 } else { | ||||
|                     setTitle(getString(R.string.contributions_fragment)); | ||||
|                     loadFragment(ContributionsFragment.newInstance(), false); | ||||
|                 } | ||||
|             } | ||||
|             setUpPager(); | ||||
|             /** | ||||
|              * Ask the user for media location access just after login | ||||
|              * so that location in the EXIF metadata of the images shared by the user | ||||
|              * is retained on devices running Android 10 or above | ||||
|              */ | ||||
| //            if (VERSION.SDK_INT >= VERSION_CODES.Q) { | ||||
| //                ActivityCompat.requestPermissions(this, | ||||
| //                    new String[]{Manifest.permission.ACCESS_MEDIA_LOCATION}, 0); | ||||
| //                PermissionUtils.checkPermissionsAndPerformAction( | ||||
| //                    this, | ||||
| //                    () -> {}, | ||||
| //                    R.string.media_location_permission_denied, | ||||
| //                    R.string.add_location_manually, | ||||
| //                    permission.ACCESS_MEDIA_LOCATION); | ||||
| //            } | ||||
|             checkAndResumeStuckUploads(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public void setSelectedItemId(int id) { | ||||
|         binding.fragmentMainNavTabLayout.setSelectedItemId(id); | ||||
|     } | ||||
| 
 | ||||
|     private void setUpPager() { | ||||
|         binding.fragmentMainNavTabLayout.setOnNavigationItemSelectedListener( | ||||
|             navListener = (item) -> { | ||||
|                 if (!item.getTitle().equals(getString(R.string.more))) { | ||||
|                     // do not change title for more fragment | ||||
|                     setTitle(item.getTitle()); | ||||
|                 } | ||||
|                 // set last_opened_nearby true if item is nearby screen else set false | ||||
|                 applicationKvStore.putBoolean("last_opened_nearby", | ||||
|                     item.getTitle().equals(getString(R.string.nearby_fragment))); | ||||
|                 final Fragment fragment = NavTab.of(item.getOrder()).newInstance(); | ||||
|                 return loadFragment(fragment, true); | ||||
|             }); | ||||
|     } | ||||
| 
 | ||||
|     private void setUpLoggedOutPager() { | ||||
|         loadFragment(ExploreFragment.newInstance(), false); | ||||
|         binding.fragmentMainNavTabLayout.setOnNavigationItemSelectedListener(item -> { | ||||
|             if (!item.getTitle().equals(getString(R.string.more))) { | ||||
|                 // do not change title for more fragment | ||||
|                 setTitle(item.getTitle()); | ||||
|             } | ||||
|             Fragment fragment = NavTabLoggedOut.of(item.getOrder()).newInstance(); | ||||
|             return loadFragment(fragment, true); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     private boolean loadFragment(Fragment fragment, boolean showBottom) { | ||||
|         //showBottom so that we do not show the bottom tray again when constructing | ||||
|         //from the saved instance state. | ||||
| 
 | ||||
|         freeUpFragments(); | ||||
| 
 | ||||
|         if (fragment instanceof ContributionsFragment) { | ||||
|             if (activeFragment == ActiveFragment.CONTRIBUTIONS) { | ||||
|                 // scroll to top if already on the Contributions tab | ||||
|                 contributionsFragment.scrollToTop(); | ||||
|                 return true; | ||||
|             } | ||||
|             contributionsFragment = (ContributionsFragment) fragment; | ||||
|             activeFragment = ActiveFragment.CONTRIBUTIONS; | ||||
|         } else if (fragment instanceof NearbyParentFragment) { | ||||
|             if (activeFragment == ActiveFragment.NEARBY) { // Do nothing if same tab | ||||
|                 return true; | ||||
|             } | ||||
|             nearbyParentFragment = (NearbyParentFragment) fragment; | ||||
|             activeFragment = ActiveFragment.NEARBY; | ||||
|         } else if (fragment instanceof ExploreFragment) { | ||||
|             if (activeFragment == ActiveFragment.EXPLORE) { // Do nothing if same tab | ||||
|                 return true; | ||||
|             } | ||||
|             exploreFragment = (ExploreFragment) fragment; | ||||
|             activeFragment = ActiveFragment.EXPLORE; | ||||
|         } else if (fragment instanceof BookmarkFragment) { | ||||
|             if (activeFragment == ActiveFragment.BOOKMARK) { // Do nothing if same tab | ||||
|                 return true; | ||||
|             } | ||||
|             bookmarkFragment = (BookmarkFragment) fragment; | ||||
|             activeFragment = ActiveFragment.BOOKMARK; | ||||
|         } else if (fragment == null && showBottom) { | ||||
|             if (applicationKvStore.getBoolean("login_skipped") | ||||
|                 == true) { // If logged out, more sheet is different | ||||
|                 MoreBottomSheetLoggedOutFragment bottomSheet = new MoreBottomSheetLoggedOutFragment(); | ||||
|                 bottomSheet.show(getSupportFragmentManager(), | ||||
|                     "MoreBottomSheetLoggedOut"); | ||||
|             } else { | ||||
|                 MoreBottomSheetFragment bottomSheet = new MoreBottomSheetFragment(); | ||||
|                 bottomSheet.show(getSupportFragmentManager(), | ||||
|                     "MoreBottomSheet"); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (fragment != null) { | ||||
|             getSupportFragmentManager() | ||||
|                 .beginTransaction() | ||||
|                 .replace(R.id.fragmentContainer, fragment) | ||||
|                 .commit(); | ||||
|             return true; | ||||
|         } | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * loadFragment() overload that supports passing extras to fragments | ||||
|      **/ | ||||
|     private boolean loadFragment(Fragment fragment, boolean showBottom, Bundle args) { | ||||
|         if (fragment != null && args != null) { | ||||
|             fragment.setArguments(args); | ||||
|         } | ||||
| 
 | ||||
|         return loadFragment(fragment, showBottom); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Old implementation of loadFragment() was causing memory leaks, due to MainActivity holding | ||||
|      * references to cleared fragments. This function frees up all fragment references. | ||||
|      * <p> | ||||
|      * Called in loadFragment() before doing the actual loading. | ||||
|      **/ | ||||
|     public void freeUpFragments() { | ||||
|         // free all fragments except contributionsFragment because several tests depend on it. | ||||
|         // hence, contributionsFragment is probably still a leak | ||||
|         nearbyParentFragment = null; | ||||
|         exploreFragment = null; | ||||
|         bookmarkFragment = null; | ||||
|     } | ||||
| 
 | ||||
|     public void hideTabs() { | ||||
|         binding.fragmentMainNavTabLayout.setVisibility(View.GONE); | ||||
|     } | ||||
| 
 | ||||
|     public void showTabs() { | ||||
|         binding.fragmentMainNavTabLayout.setVisibility(View.VISIBLE); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Adds number of uploads next to tab text "Contributions" then it will look like "Contributions | ||||
|      * (NUMBER)" | ||||
|      * | ||||
|      * @param uploadCount | ||||
|      */ | ||||
|     public void setNumOfUploads(int uploadCount) { | ||||
|         if (activeFragment == ActiveFragment.CONTRIBUTIONS) { | ||||
|             setTitle(getResources().getString(R.string.contributions_fragment) + " " + ( | ||||
|                 !(uploadCount == 0) ? | ||||
|                     getResources() | ||||
|                         .getQuantityString(R.plurals.contributions_subtitle, | ||||
|                             uploadCount, uploadCount) | ||||
|                     : getString(R.string.contributions_subtitle_zero))); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Resume the uploads that got stuck because of the app being killed or the device being | ||||
|      * rebooted. | ||||
|      * <p> | ||||
|      * When the app is terminated or the device is restarted, contributions remain in the | ||||
|      * 'STATE_IN_PROGRESS' state. This status persists and doesn't change during these events. So, | ||||
|      * retrieving contributions labeled as 'STATE_IN_PROGRESS' from the database will provide the | ||||
|      * list of uploads that appear as stuck on opening the app again | ||||
|      */ | ||||
|     @SuppressLint("CheckResult") | ||||
|     private void checkAndResumeStuckUploads() { | ||||
|         List<Contribution> stuckUploads = contributionDao.getContribution( | ||||
|                 Collections.singletonList(Contribution.STATE_IN_PROGRESS)) | ||||
|             .subscribeOn(Schedulers.io()) | ||||
|             .blockingGet(); | ||||
|         Timber.d("Resuming " + stuckUploads.size() + " uploads..."); | ||||
|         if (!stuckUploads.isEmpty()) { | ||||
|             for (Contribution contribution : stuckUploads) { | ||||
|                 contribution.setState(Contribution.STATE_QUEUED); | ||||
|                 contribution.setDateUploadStarted(Calendar.getInstance().getTime()); | ||||
|                 Completable.fromAction(() -> contributionDao.saveSynchronous(contribution)) | ||||
|                     .subscribeOn(Schedulers.io()) | ||||
|                     .subscribe(); | ||||
|             } | ||||
|             WorkRequestHelper.Companion.makeOneTimeWorkRequest( | ||||
|                 this, ExistingWorkPolicy.APPEND_OR_REPLACE); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onPostCreate(@Nullable Bundle savedInstanceState) { | ||||
|         super.onPostCreate(savedInstanceState); | ||||
|         //quizChecker.initQuizCheck(this); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onSaveInstanceState(Bundle outState) { | ||||
|         super.onSaveInstanceState(outState); | ||||
|         outState.putInt("viewPagerCurrentItem", binding.pager.getCurrentItem()); | ||||
|         outState.putString("activeFragment", activeFragment.name()); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onRestoreInstanceState(Bundle savedInstanceState) { | ||||
|         super.onRestoreInstanceState(savedInstanceState); | ||||
|         String activeFragmentName = savedInstanceState.getString("activeFragment"); | ||||
|         if (activeFragmentName != null) { | ||||
|             restoreActiveFragment(activeFragmentName); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private void restoreActiveFragment(@NonNull String fragmentName) { | ||||
|         if (fragmentName.equals(ActiveFragment.CONTRIBUTIONS.name())) { | ||||
|             setTitle(getString(R.string.contributions_fragment)); | ||||
|             loadFragment(ContributionsFragment.newInstance(), false); | ||||
|         } else if (fragmentName.equals(ActiveFragment.NEARBY.name())) { | ||||
|             setTitle(getString(R.string.nearby_fragment)); | ||||
|             loadFragment(NearbyParentFragment.newInstance(), false); | ||||
|         } else if (fragmentName.equals(ActiveFragment.EXPLORE.name())) { | ||||
|             setTitle(getString(R.string.navigation_item_explore)); | ||||
|             loadFragment(ExploreFragment.newInstance(), false); | ||||
|         } else if (fragmentName.equals(ActiveFragment.BOOKMARK.name())) { | ||||
|             setTitle(getString(R.string.bookmarks)); | ||||
|             loadFragment(BookmarkFragment.newInstance(), false); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onBackPressed() { | ||||
|         if (contributionsFragment != null && activeFragment == ActiveFragment.CONTRIBUTIONS) { | ||||
|             // Means that contribution fragment is visible | ||||
|             if (!contributionsFragment.backButtonClicked()) {//If this one does not wan't to handle | ||||
|                 // the back press, let the activity do so | ||||
|                 super.onBackPressed(); | ||||
|             } | ||||
|         } else if (nearbyParentFragment != null && activeFragment == ActiveFragment.NEARBY) { | ||||
|             // Means that nearby fragment is visible | ||||
|             /* If function nearbyParentFragment.backButtonClick() returns false, it means that the bottomsheet is | ||||
|               not expanded. So if the back button is pressed, then go back to the Contributions tab */ | ||||
|             if (!nearbyParentFragment.backButtonClicked()) { | ||||
|                 getSupportFragmentManager().beginTransaction().remove(nearbyParentFragment) | ||||
|                     .commit(); | ||||
|                 setSelectedItemId(NavTab.CONTRIBUTIONS.code()); | ||||
|             } | ||||
|         } else if (exploreFragment != null && activeFragment == ActiveFragment.EXPLORE) { | ||||
|             // Means that explore fragment is visible | ||||
|             if (!exploreFragment.onBackPressed()) { | ||||
|                 if (applicationKvStore.getBoolean("login_skipped")) { | ||||
|                     super.onBackPressed(); | ||||
|                 } else { | ||||
|                     setSelectedItemId(NavTab.CONTRIBUTIONS.code()); | ||||
|                 } | ||||
|             } | ||||
|         } else if (bookmarkFragment != null && activeFragment == ActiveFragment.BOOKMARK) { | ||||
|             // Means that bookmark fragment is visible | ||||
|             bookmarkFragment.onBackPressed(); | ||||
|         } else { | ||||
|             super.onBackPressed(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onBackStackChanged() { | ||||
|         //initBackButton(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Retry all failed uploads as soon as the user returns to the app | ||||
|      */ | ||||
|     @SuppressLint("CheckResult") | ||||
|     private void retryAllFailedUploads() { | ||||
|         contributionDao. | ||||
|             getContribution(Collections.singletonList(Contribution.STATE_FAILED)) | ||||
|             .subscribeOn(Schedulers.io()) | ||||
|             .subscribe(failedUploads -> { | ||||
|                 for (Contribution contribution : failedUploads) { | ||||
|                     contributionsFragment.retryUpload(contribution); | ||||
|                 } | ||||
|             }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Handles item selection in the options menu. This method is called when a user interacts with | ||||
|      * the options menu in the Top Bar. | ||||
|      */ | ||||
|     @Override | ||||
|     public boolean onOptionsItemSelected(MenuItem item) { | ||||
|         switch (item.getItemId()) { | ||||
|             case R.id.upload_tab: | ||||
|                 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; | ||||
|             default: | ||||
|                 return super.onOptionsItemSelected(item); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public void centerMapToPlace(Place place) { | ||||
|         setSelectedItemId(NavTab.NEARBY.code()); | ||||
|         nearbyParentFragment.setNearbyParentFragmentInstanceReadyCallback( | ||||
|             new NearbyParentFragmentInstanceReadyCallback() { | ||||
|                 @Override | ||||
|                 public void onReady() { | ||||
|                     nearbyParentFragment.centerMapToPlace(place); | ||||
|                 } | ||||
|             }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Launch the Explore fragment from Nearby fragment. This method is called when a user clicks | ||||
|      * the 'Show in Explore' option in the 3-dots menu in Nearby. | ||||
|      * | ||||
|      * @param zoom      current zoom of Nearby map | ||||
|      * @param latitude  current latitude of Nearby map | ||||
|      * @param longitude current longitude of Nearby map | ||||
|      **/ | ||||
|     public void loadExploreMapFromNearby(double zoom, double latitude, double longitude) { | ||||
|         Bundle bundle = new Bundle(); | ||||
|         bundle.putDouble("prev_zoom", zoom); | ||||
|         bundle.putDouble("prev_latitude", latitude); | ||||
|         bundle.putDouble("prev_longitude", longitude); | ||||
| 
 | ||||
|         loadFragment(ExploreFragment.newInstance(), false, bundle); | ||||
|         setSelectedItemId(NavTab.EXPLORE.code()); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Launch the Nearby fragment from Explore fragment. This method is called when a user clicks | ||||
|      * the 'Show in Nearby' option in the 3-dots menu in Explore. | ||||
|      * | ||||
|      * @param zoom      current zoom of Explore map | ||||
|      * @param latitude  current latitude of Explore map | ||||
|      * @param longitude current longitude of Explore map | ||||
|      **/ | ||||
|     public void loadNearbyMapFromExplore(double zoom, double latitude, double longitude) { | ||||
|         Bundle bundle = new Bundle(); | ||||
|         bundle.putDouble("prev_zoom", zoom); | ||||
|         bundle.putDouble("prev_latitude", latitude); | ||||
|         bundle.putDouble("prev_longitude", longitude); | ||||
| 
 | ||||
|         loadFragment(NearbyParentFragment.newInstance(), false, bundle); | ||||
|         setSelectedItemId(NavTab.NEARBY.code()); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onResume() { | ||||
|         super.onResume(); | ||||
| 
 | ||||
|         if ((applicationKvStore.getBoolean("firstrun", true)) && | ||||
|             (!applicationKvStore.getBoolean("login_skipped"))) { | ||||
|             defaultKvStore.putBoolean("inAppCameraFirstRun", true); | ||||
|             WelcomeActivity.startYourself(this); | ||||
|         } | ||||
| 
 | ||||
|         retryAllFailedUploads(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onDestroy() { | ||||
|         quizChecker.cleanup(); | ||||
|         locationManager.unregisterLocationManager(); | ||||
|         // Remove ourself from hashmap to prevent memory leaks | ||||
|         locationManager = null; | ||||
|         super.onDestroy(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Public method to show nearby from the reference of this. | ||||
|      */ | ||||
|     public void showNearby() { | ||||
|         binding.fragmentMainNavTabLayout.setSelectedItemId(NavTab.NEARBY.code()); | ||||
|     } | ||||
| 
 | ||||
|     public enum ActiveFragment { | ||||
|         CONTRIBUTIONS, | ||||
|         NEARBY, | ||||
|         EXPLORE, | ||||
|         BOOKMARK, | ||||
|         MORE | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Load default language in onCreate from SharedPreferences | ||||
|      */ | ||||
|     private void loadLocale() { | ||||
|         final SharedPreferences preferences = getSharedPreferences("Settings", | ||||
|             Activity.MODE_PRIVATE); | ||||
|         final String language = preferences.getString("language", ""); | ||||
|         final SettingsFragment settingsFragment = new SettingsFragment(); | ||||
|         settingsFragment.setLocale(this, language); | ||||
|     } | ||||
| 
 | ||||
|     public NavTabLayout.OnNavigationItemSelectedListener getNavListener() { | ||||
|         return navListener; | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,567 @@ | |||
| package fr.free.nrw.commons.contributions | ||||
| 
 | ||||
| import android.annotation.SuppressLint | ||||
| import android.content.Context | ||||
| import android.content.Intent | ||||
| import android.os.Bundle | ||||
| import android.view.Menu | ||||
| import android.view.MenuItem | ||||
| import android.view.View | ||||
| import androidx.fragment.app.Fragment | ||||
| import androidx.fragment.app.FragmentManager | ||||
| import androidx.work.ExistingWorkPolicy | ||||
| import com.google.android.material.bottomnavigation.BottomNavigationView | ||||
| import fr.free.nrw.commons.R | ||||
| import fr.free.nrw.commons.WelcomeActivity | ||||
| import fr.free.nrw.commons.auth.SessionManager | ||||
| import fr.free.nrw.commons.bookmarks.BookmarkFragment | ||||
| import fr.free.nrw.commons.contributions.ContributionsFragment.Companion.newInstance | ||||
| import fr.free.nrw.commons.databinding.MainBinding | ||||
| 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.MediaDetailPagerFragment | ||||
| import fr.free.nrw.commons.navtab.MoreBottomSheetFragment | ||||
| import fr.free.nrw.commons.navtab.MoreBottomSheetLoggedOutFragment | ||||
| import fr.free.nrw.commons.navtab.NavTab | ||||
| import fr.free.nrw.commons.navtab.NavTabLayout | ||||
| import fr.free.nrw.commons.navtab.NavTabLoggedOut | ||||
| import fr.free.nrw.commons.nearby.Place | ||||
| import fr.free.nrw.commons.nearby.fragments.NearbyParentFragment | ||||
| import fr.free.nrw.commons.notification.NotificationActivity.Companion.startYourself | ||||
| import fr.free.nrw.commons.notification.NotificationController | ||||
| import fr.free.nrw.commons.quiz.QuizChecker | ||||
| import fr.free.nrw.commons.settings.SettingsFragment | ||||
| import fr.free.nrw.commons.theme.BaseActivity | ||||
| import fr.free.nrw.commons.upload.UploadProgressActivity | ||||
| import fr.free.nrw.commons.upload.worker.WorkRequestHelper.Companion.makeOneTimeWorkRequest | ||||
| import fr.free.nrw.commons.utils.ViewUtilWrapper | ||||
| import io.reactivex.Completable | ||||
| import io.reactivex.schedulers.Schedulers | ||||
| import timber.log.Timber | ||||
| import java.util.Calendar | ||||
| import javax.inject.Inject | ||||
| import javax.inject.Named | ||||
| 
 | ||||
| 
 | ||||
| class MainActivity : BaseActivity(), FragmentManager.OnBackStackChangedListener { | ||||
|     @JvmField | ||||
|     @Inject | ||||
|     var sessionManager: SessionManager? = null | ||||
| 
 | ||||
|     @JvmField | ||||
|     @Inject | ||||
|     var controller: ContributionController? = null | ||||
| 
 | ||||
|     @JvmField | ||||
|     @Inject | ||||
|     var contributionDao: ContributionDao? = null | ||||
| 
 | ||||
|     private var contributionsFragment: ContributionsFragment? = null | ||||
|     private var nearbyParentFragment: NearbyParentFragment? = null | ||||
|     private var exploreFragment: ExploreFragment? = null | ||||
|     private var bookmarkFragment: BookmarkFragment? = null | ||||
|     @JvmField | ||||
|     var activeFragment: ActiveFragment? = null | ||||
|     private val mediaDetailPagerFragment: MediaDetailPagerFragment? = null | ||||
|     var navListener: BottomNavigationView.OnNavigationItemSelectedListener? = null | ||||
|         private set | ||||
| 
 | ||||
|     @JvmField | ||||
|     @Inject | ||||
|     var locationManager: LocationServiceManager? = null | ||||
| 
 | ||||
|     @JvmField | ||||
|     @Inject | ||||
|     var notificationController: NotificationController? = null | ||||
| 
 | ||||
|     @JvmField | ||||
|     @Inject | ||||
|     var quizChecker: QuizChecker? = null | ||||
| 
 | ||||
|     @JvmField | ||||
|     @Inject | ||||
|     @Named("default_preferences") | ||||
|     var applicationKvStore: JsonKvStore? = null | ||||
| 
 | ||||
|     @JvmField | ||||
|     @Inject | ||||
|     var viewUtilWrapper: ViewUtilWrapper? = null | ||||
| 
 | ||||
|     var menu: Menu? = null | ||||
| 
 | ||||
|     @JvmField | ||||
|     var binding: MainBinding? = null | ||||
| 
 | ||||
|     var tabLayout: NavTabLayout? = null | ||||
| 
 | ||||
| 
 | ||||
|     override fun onSupportNavigateUp(): Boolean { | ||||
|         if (activeFragment == ActiveFragment.CONTRIBUTIONS) { | ||||
|             if (!contributionsFragment!!.backButtonClicked()) { | ||||
|                 return false | ||||
|             } | ||||
|         } else { | ||||
|             onBackPressed() | ||||
|             showTabs() | ||||
|         } | ||||
|         return true | ||||
|     } | ||||
| 
 | ||||
|     public override fun onCreate(savedInstanceState: Bundle?) { | ||||
|         super.onCreate(savedInstanceState) | ||||
|         binding = MainBinding.inflate(layoutInflater) | ||||
|         setContentView(binding!!.root) | ||||
|         setSupportActionBar(binding!!.toolbarBinding.toolbar) | ||||
|         tabLayout = binding!!.fragmentMainNavTabLayout | ||||
|         loadLocale() | ||||
| 
 | ||||
|         binding!!.toolbarBinding.toolbar.setNavigationOnClickListener { view: View? -> | ||||
|             onSupportNavigateUp() | ||||
|         } | ||||
|         /* | ||||
| "first_edit_depict" is a key for getting information about opening the depiction editor | ||||
| screen for the first time after opening the app. | ||||
| 
 | ||||
| Getting true by the key means the depiction editor screen is opened for the first time | ||||
| after opening the app. | ||||
| Getting false by the key means the depiction editor screen is not opened for the first time | ||||
| after opening the app. | ||||
|  */ | ||||
|         applicationKvStore!!.putBoolean("first_edit_depict", true) | ||||
|         if (applicationKvStore!!.getBoolean("login_skipped") == true) { | ||||
|             title = getString(R.string.navigation_item_explore) | ||||
|             setUpLoggedOutPager() | ||||
|         } else { | ||||
|             if (applicationKvStore!!.getBoolean("firstrun", true)) { | ||||
|                 applicationKvStore!!.putBoolean("hasAlreadyLaunchedBigMultiupload", false) | ||||
|                 applicationKvStore!!.putBoolean("hasAlreadyLaunchedCategoriesDialog", false) | ||||
|             } | ||||
|             if (savedInstanceState == null) { | ||||
|                 //starting a fresh fragment. | ||||
|                 // Open Last opened screen if it is Contributions or Nearby, otherwise Contributions | ||||
|                 if (applicationKvStore!!.getBoolean("last_opened_nearby")) { | ||||
|                     title = getString(R.string.nearby_fragment) | ||||
|                     showNearby() | ||||
|                     loadFragment(NearbyParentFragment.newInstance(), false) | ||||
|                 } else { | ||||
|                     title = getString(R.string.contributions_fragment) | ||||
|                     loadFragment(newInstance(), false) | ||||
|                 } | ||||
|             } | ||||
|             setUpPager() | ||||
|             /** | ||||
|              * Ask the user for media location access just after login | ||||
|              * so that location in the EXIF metadata of the images shared by the user | ||||
|              * is retained on devices running Android 10 or above | ||||
|              */ | ||||
| //            if (VERSION.SDK_INT >= VERSION_CODES.Q) { | ||||
| //                ActivityCompat.requestPermissions(this, | ||||
| //                    new String[]{Manifest.permission.ACCESS_MEDIA_LOCATION}, 0); | ||||
| //                PermissionUtils.checkPermissionsAndPerformAction( | ||||
| //                    this, | ||||
| //                    () -> {}, | ||||
| //                    R.string.media_location_permission_denied, | ||||
| //                    R.string.add_location_manually, | ||||
| //                    permission.ACCESS_MEDIA_LOCATION); | ||||
| //            } | ||||
|             checkAndResumeStuckUploads() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fun setSelectedItemId(id: Int) { | ||||
|         binding!!.fragmentMainNavTabLayout.selectedItemId = id | ||||
|     } | ||||
| 
 | ||||
|     private fun setUpPager() { | ||||
|         binding!!.fragmentMainNavTabLayout.setOnNavigationItemSelectedListener( | ||||
|             BottomNavigationView.OnNavigationItemSelectedListener { item: MenuItem -> | ||||
|                 if (item.title != getString(R.string.more)) { | ||||
|                     // do not change title for more fragment | ||||
|                     title = item.title | ||||
|                 } | ||||
|                 // set last_opened_nearby true if item is nearby screen else set false | ||||
|                 applicationKvStore!!.putBoolean( | ||||
|                     "last_opened_nearby", | ||||
|                     item.title == getString(R.string.nearby_fragment) | ||||
|                 ) | ||||
|                 val fragment = NavTab.of(item.order).newInstance() | ||||
|                 loadFragment(fragment, true) | ||||
|             }.also { navListener = it }) | ||||
|     } | ||||
| 
 | ||||
|     private fun setUpLoggedOutPager() { | ||||
|         loadFragment(ExploreFragment.newInstance(), false) | ||||
|         binding!!.fragmentMainNavTabLayout.setOnNavigationItemSelectedListener { item: MenuItem -> | ||||
|             if (item.title != getString(R.string.more)) { | ||||
|                 // do not change title for more fragment | ||||
|                 title = item.title | ||||
|             } | ||||
|             val fragment = | ||||
|                 NavTabLoggedOut.of(item.order).newInstance() | ||||
|             loadFragment(fragment, true) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun loadFragment(fragment: Fragment?, showBottom: Boolean): Boolean { | ||||
|         //showBottom so that we do not show the bottom tray again when constructing | ||||
|         //from the saved instance state. | ||||
| 
 | ||||
|         freeUpFragments(); | ||||
| 
 | ||||
|         if (fragment is ContributionsFragment) { | ||||
|             if (activeFragment == ActiveFragment.CONTRIBUTIONS) { | ||||
|                 // scroll to top if already on the Contributions tab | ||||
|                 contributionsFragment!!.scrollToTop() | ||||
|                 return true | ||||
|             } | ||||
|             contributionsFragment = fragment | ||||
|             activeFragment = ActiveFragment.CONTRIBUTIONS | ||||
|         } else if (fragment is NearbyParentFragment) { | ||||
|             if (activeFragment == ActiveFragment.NEARBY) { // Do nothing if same tab | ||||
|                 return true | ||||
|             } | ||||
|             nearbyParentFragment = fragment | ||||
|             activeFragment = ActiveFragment.NEARBY | ||||
|         } else if (fragment is ExploreFragment) { | ||||
|             if (activeFragment == ActiveFragment.EXPLORE) { // Do nothing if same tab | ||||
|                 return true | ||||
|             } | ||||
|             exploreFragment = fragment | ||||
|             activeFragment = ActiveFragment.EXPLORE | ||||
|         } else if (fragment is BookmarkFragment) { | ||||
|             if (activeFragment == ActiveFragment.BOOKMARK) { // Do nothing if same tab | ||||
|                 return true | ||||
|             } | ||||
|             bookmarkFragment = fragment | ||||
|             activeFragment = ActiveFragment.BOOKMARK | ||||
|         } else if (fragment == null && showBottom) { | ||||
|             if (applicationKvStore!!.getBoolean("login_skipped") | ||||
|                 == true | ||||
|             ) { // If logged out, more sheet is different | ||||
|                 val bottomSheet = MoreBottomSheetLoggedOutFragment() | ||||
|                 bottomSheet.show( | ||||
|                     supportFragmentManager, | ||||
|                     "MoreBottomSheetLoggedOut" | ||||
|                 ) | ||||
|             } else { | ||||
|                 val bottomSheet = MoreBottomSheetFragment() | ||||
|                 bottomSheet.show( | ||||
|                     supportFragmentManager, | ||||
|                     "MoreBottomSheet" | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (fragment != null) { | ||||
|             supportFragmentManager | ||||
|                 .beginTransaction() | ||||
|                 .replace(R.id.fragmentContainer, fragment) | ||||
|                 .commit() | ||||
|             return true | ||||
|         } | ||||
|         return false | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * loadFragment() overload that supports passing extras to fragments | ||||
|      */ | ||||
|     private fun loadFragment(fragment: Fragment?, showBottom: Boolean, args: Bundle?): Boolean { | ||||
|         if (fragment != null && args != null) { | ||||
|             fragment.arguments = args | ||||
|         } | ||||
| 
 | ||||
|         return loadFragment(fragment, showBottom) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Old implementation of loadFragment() was causing memory leaks, due to MainActivity holding | ||||
|      * references to cleared fragments. This function frees up all fragment references. | ||||
|      * | ||||
|      * | ||||
|      * Called in loadFragment() before doing the actual loading. | ||||
|      */ | ||||
|     fun freeUpFragments() { | ||||
|         // free all fragments except contributionsFragment because several tests depend on it. | ||||
|         // hence, contributionsFragment is probably still a leak | ||||
|         nearbyParentFragment = null | ||||
|         exploreFragment = null | ||||
|         bookmarkFragment = null | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     fun hideTabs() { | ||||
|         binding!!.fragmentMainNavTabLayout.visibility = View.GONE | ||||
|     } | ||||
| 
 | ||||
|     fun showTabs() { | ||||
|         binding!!.fragmentMainNavTabLayout.visibility = View.VISIBLE | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Adds number of uploads next to tab text "Contributions" then it will look like "Contributions | ||||
|      * (NUMBER)" | ||||
|      * | ||||
|      * @param uploadCount | ||||
|      */ | ||||
|     fun setNumOfUploads(uploadCount: Int) { | ||||
|         if (activeFragment == ActiveFragment.CONTRIBUTIONS) { | ||||
|             title = | ||||
|                 resources.getString(R.string.contributions_fragment) + " " + (if (uploadCount != 0) | ||||
|                     resources | ||||
|                         .getQuantityString( | ||||
|                             R.plurals.contributions_subtitle, | ||||
|                             uploadCount, uploadCount | ||||
|                         ) | ||||
|                 else | ||||
|                     getString(R.string.contributions_subtitle_zero)) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Resume the uploads that got stuck because of the app being killed or the device being | ||||
|      * rebooted. | ||||
|      * | ||||
|      * | ||||
|      * When the app is terminated or the device is restarted, contributions remain in the | ||||
|      * 'STATE_IN_PROGRESS' state. This status persists and doesn't change during these events. So, | ||||
|      * retrieving contributions labeled as 'STATE_IN_PROGRESS' from the database will provide the | ||||
|      * list of uploads that appear as stuck on opening the app again | ||||
|      */ | ||||
|     @SuppressLint("CheckResult") | ||||
|     private fun checkAndResumeStuckUploads() { | ||||
|         val stuckUploads = contributionDao!!.getContribution( | ||||
|             listOf(Contribution.STATE_IN_PROGRESS) | ||||
|         ) | ||||
|             .subscribeOn(Schedulers.io()) | ||||
|             .blockingGet() | ||||
|         Timber.d("Resuming " + stuckUploads.size + " uploads...") | ||||
|         if (!stuckUploads.isEmpty()) { | ||||
|             for (contribution in stuckUploads) { | ||||
|                 contribution.state = Contribution.STATE_QUEUED | ||||
|                 contribution.dateUploadStarted = Calendar.getInstance().time | ||||
|                 Completable.fromAction { contributionDao!!.saveSynchronous(contribution) } | ||||
|                     .subscribeOn(Schedulers.io()) | ||||
|                     .subscribe() | ||||
|             } | ||||
|             makeOneTimeWorkRequest( | ||||
|                 this, ExistingWorkPolicy.APPEND_OR_REPLACE | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun onPostCreate(savedInstanceState: Bundle?) { | ||||
|         super.onPostCreate(savedInstanceState) | ||||
|         //quizChecker.initQuizCheck(this); | ||||
|     } | ||||
| 
 | ||||
|     override fun onSaveInstanceState(outState: Bundle) { | ||||
|         super.onSaveInstanceState(outState) | ||||
|         outState.putInt("viewPagerCurrentItem", binding!!.pager.currentItem) | ||||
|         outState.putString("activeFragment", activeFragment!!.name) | ||||
|     } | ||||
| 
 | ||||
|     override fun onRestoreInstanceState(savedInstanceState: Bundle) { | ||||
|         super.onRestoreInstanceState(savedInstanceState) | ||||
|         val activeFragmentName = savedInstanceState.getString("activeFragment") | ||||
|         if (activeFragmentName != null) { | ||||
|             restoreActiveFragment(activeFragmentName) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun restoreActiveFragment(fragmentName: String) { | ||||
|         if (fragmentName == ActiveFragment.CONTRIBUTIONS.name) { | ||||
|             title = getString(R.string.contributions_fragment) | ||||
|             loadFragment(newInstance(), false) | ||||
|         } else if (fragmentName == ActiveFragment.NEARBY.name) { | ||||
|             title = getString(R.string.nearby_fragment) | ||||
|             loadFragment(NearbyParentFragment.newInstance(), false) | ||||
|         } else if (fragmentName == ActiveFragment.EXPLORE.name) { | ||||
|             title = getString(R.string.navigation_item_explore) | ||||
|             loadFragment(ExploreFragment.newInstance(), false) | ||||
|         } else if (fragmentName == ActiveFragment.BOOKMARK.name) { | ||||
|             title = getString(R.string.bookmarks) | ||||
|             loadFragment(BookmarkFragment.newInstance(), false) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun onBackPressed() { | ||||
|         if (contributionsFragment != null && activeFragment == ActiveFragment.CONTRIBUTIONS) { | ||||
|             // Means that contribution fragment is visible | ||||
|             if (!contributionsFragment!!.backButtonClicked()) { //If this one does not wan't to handle | ||||
|                 // the back press, let the activity do so | ||||
|                 super.onBackPressed() | ||||
|             } | ||||
|         } else if (nearbyParentFragment != null && activeFragment == ActiveFragment.NEARBY) { | ||||
|             // Means that nearby fragment is visible | ||||
|             /* If function nearbyParentFragment.backButtonClick() returns false, it means that the bottomsheet is | ||||
|               not expanded. So if the back button is pressed, then go back to the Contributions tab */ | ||||
|             if (!nearbyParentFragment!!.backButtonClicked()) { | ||||
|                 supportFragmentManager.beginTransaction().remove(nearbyParentFragment!!) | ||||
|                     .commit() | ||||
|                 setSelectedItemId(NavTab.CONTRIBUTIONS.code()) | ||||
|             } | ||||
|         } else if (exploreFragment != null && activeFragment == ActiveFragment.EXPLORE) { | ||||
|             // Means that explore fragment is visible | ||||
|             if (!exploreFragment!!.onBackPressed()) { | ||||
|                 if (applicationKvStore!!.getBoolean("login_skipped")) { | ||||
|                     super.onBackPressed() | ||||
|                 } else { | ||||
|                     setSelectedItemId(NavTab.CONTRIBUTIONS.code()) | ||||
|                 } | ||||
|             } | ||||
|         } else if (bookmarkFragment != null && activeFragment == ActiveFragment.BOOKMARK) { | ||||
|             // Means that bookmark fragment is visible | ||||
|             bookmarkFragment!!.onBackPressed() | ||||
|         } else { | ||||
|             super.onBackPressed() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun onBackStackChanged() { | ||||
|         //initBackButton(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Retry all failed uploads as soon as the user returns to the app | ||||
|      */ | ||||
|     @SuppressLint("CheckResult") | ||||
|     private fun retryAllFailedUploads() { | ||||
|         contributionDao | ||||
|             ?.getContribution(listOf(Contribution.STATE_FAILED)) | ||||
|             ?.subscribeOn(Schedulers.io()) | ||||
|             ?.subscribe { failedUploads -> | ||||
|                 failedUploads.forEach { contribution -> | ||||
|                     contributionsFragment?.retryUpload(contribution) | ||||
|                 } | ||||
|             } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Handles item selection in the options menu. This method is called when a user interacts with | ||||
|      * the options menu in the Top Bar. | ||||
|      */ | ||||
|     override fun onOptionsItemSelected(item: MenuItem): Boolean { | ||||
|         when (item.itemId) { | ||||
|             R.id.upload_tab -> { | ||||
|                 startActivity(Intent(this, UploadProgressActivity::class.java)) | ||||
|                 return true | ||||
|             } | ||||
| 
 | ||||
|             R.id.notifications -> { | ||||
|                 // Starts notification activity on click to notification icon | ||||
|                 startYourself(this, "unread") | ||||
|                 return true | ||||
|             } | ||||
| 
 | ||||
|             else -> return super.onOptionsItemSelected(item) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fun centerMapToPlace(place: Place?) { | ||||
|         setSelectedItemId(NavTab.NEARBY.code()) | ||||
|         nearbyParentFragment!!.setNearbyParentFragmentInstanceReadyCallback { | ||||
|             nearbyParentFragment!!.centerMapToPlace( | ||||
|                 place | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Launch the Explore fragment from Nearby fragment. This method is called when a user clicks | ||||
|      * the 'Show in Explore' option in the 3-dots menu in Nearby. | ||||
|      * | ||||
|      * @param zoom      current zoom of Nearby map | ||||
|      * @param latitude  current latitude of Nearby map | ||||
|      * @param longitude current longitude of Nearby map | ||||
|      */ | ||||
|     fun loadExploreMapFromNearby(zoom: Double, latitude: Double, longitude: Double) { | ||||
|         val bundle = Bundle() | ||||
|         bundle.putDouble("prev_zoom", zoom) | ||||
|         bundle.putDouble("prev_latitude", latitude) | ||||
|         bundle.putDouble("prev_longitude", longitude) | ||||
| 
 | ||||
|         loadFragment(ExploreFragment.newInstance(), false, bundle) | ||||
|         setSelectedItemId(NavTab.EXPLORE.code()) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Launch the Nearby fragment from Explore fragment. This method is called when a user clicks | ||||
|      * the 'Show in Nearby' option in the 3-dots menu in Explore. | ||||
|      * | ||||
|      * @param zoom      current zoom of Explore map | ||||
|      * @param latitude  current latitude of Explore map | ||||
|      * @param longitude current longitude of Explore map | ||||
|      */ | ||||
|     fun loadNearbyMapFromExplore(zoom: Double, latitude: Double, longitude: Double) { | ||||
|         val bundle = Bundle() | ||||
|         bundle.putDouble("prev_zoom", zoom) | ||||
|         bundle.putDouble("prev_latitude", latitude) | ||||
|         bundle.putDouble("prev_longitude", longitude) | ||||
| 
 | ||||
|         loadFragment(NearbyParentFragment.newInstance(), false, bundle) | ||||
|         setSelectedItemId(NavTab.NEARBY.code()) | ||||
|     } | ||||
| 
 | ||||
|     override fun onResume() { | ||||
|         super.onResume() | ||||
| 
 | ||||
|         if ((applicationKvStore!!.getBoolean("firstrun", true)) && | ||||
|             (!applicationKvStore!!.getBoolean("login_skipped")) | ||||
|         ) { | ||||
|             defaultKvStore.putBoolean("inAppCameraFirstRun", true) | ||||
|             WelcomeActivity.startYourself(this) | ||||
|         } | ||||
| 
 | ||||
|         retryAllFailedUploads() | ||||
|     } | ||||
| 
 | ||||
|     override fun onDestroy() { | ||||
|         quizChecker!!.cleanup() | ||||
|         locationManager!!.unregisterLocationManager() | ||||
|         // Remove ourself from hashmap to prevent memory leaks | ||||
|         locationManager = null | ||||
|         super.onDestroy() | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Public method to show nearby from the reference of this. | ||||
|      */ | ||||
|     fun showNearby() { | ||||
|         binding!!.fragmentMainNavTabLayout.selectedItemId = NavTab.NEARBY.code() | ||||
|     } | ||||
| 
 | ||||
|     enum class ActiveFragment { | ||||
|         CONTRIBUTIONS, | ||||
|         NEARBY, | ||||
|         EXPLORE, | ||||
|         BOOKMARK, | ||||
|         MORE | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Load default language in onCreate from SharedPreferences | ||||
|      */ | ||||
|     private fun loadLocale() { | ||||
|         val preferences = getSharedPreferences( | ||||
|             "Settings", | ||||
|             MODE_PRIVATE | ||||
|         ) | ||||
|         val language = preferences.getString("language", "")!! | ||||
|         val settingsFragment = SettingsFragment() | ||||
|         settingsFragment.setLocale(this, language) | ||||
|     } | ||||
| 
 | ||||
|     companion object { | ||||
|         /** | ||||
|          * Consumers should be simply using this method to use this activity. | ||||
|          * | ||||
|          * @param context A Context of the application package implementing this class. | ||||
|          */ | ||||
|         fun startYourself(context: Context) { | ||||
|             val intent = Intent(context, MainActivity::class.java) | ||||
|             intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT or Intent.FLAG_ACTIVITY_SINGLE_TOP) | ||||
|             context.startActivity(intent) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -1,126 +0,0 @@ | |||
| package fr.free.nrw.commons.contributions; | ||||
| 
 | ||||
| import android.app.NotificationChannel; | ||||
| import android.app.NotificationManager; | ||||
| import android.app.WallpaperManager; | ||||
| import android.content.Context; | ||||
| import android.graphics.Bitmap; | ||||
| import android.net.Uri; | ||||
| import android.os.Build; | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.core.app.NotificationCompat; | ||||
| import androidx.work.Worker; | ||||
| import androidx.work.WorkerParameters; | ||||
| import com.facebook.common.executors.CallerThreadExecutor; | ||||
| import com.facebook.common.references.CloseableReference; | ||||
| import com.facebook.datasource.DataSource; | ||||
| import com.facebook.drawee.backends.pipeline.Fresco; | ||||
| import com.facebook.imagepipeline.core.ImagePipeline; | ||||
| import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber; | ||||
| import com.facebook.imagepipeline.image.CloseableImage; | ||||
| import com.facebook.imagepipeline.request.ImageRequest; | ||||
| import com.facebook.imagepipeline.request.ImageRequestBuilder; | ||||
| import fr.free.nrw.commons.R; | ||||
| import timber.log.Timber; | ||||
| 
 | ||||
| public class SetWallpaperWorker extends Worker { | ||||
| 
 | ||||
|     private static final String NOTIFICATION_CHANNEL_ID = "set_wallpaper_channel"; | ||||
|     private static final int NOTIFICATION_ID = 1; | ||||
| 
 | ||||
|     public SetWallpaperWorker(@NonNull Context context, @NonNull WorkerParameters params) { | ||||
|         super(context, params); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public Result doWork() { | ||||
|         Context context = getApplicationContext(); | ||||
|         createNotificationChannel(context); | ||||
|         showProgressNotification(context); | ||||
| 
 | ||||
|         String imageUrl = getInputData().getString("imageUrl"); | ||||
|         if (imageUrl == null) { | ||||
|             return Result.failure(); | ||||
|         } | ||||
| 
 | ||||
|         ImageRequest imageRequest = ImageRequestBuilder | ||||
|             .newBuilderWithSource(Uri.parse(imageUrl)) | ||||
|             .build(); | ||||
| 
 | ||||
|         ImagePipeline imagePipeline = Fresco.getImagePipeline(); | ||||
|         final DataSource<CloseableReference<CloseableImage>> | ||||
|             dataSource = imagePipeline.fetchDecodedImage(imageRequest, context); | ||||
| 
 | ||||
|         dataSource.subscribe(new BaseBitmapDataSubscriber() { | ||||
|             @Override | ||||
|             public void onNewResultImpl(@Nullable Bitmap bitmap) { | ||||
|                 if (dataSource.isFinished() && bitmap != null) { | ||||
|                     Timber.d("Bitmap loaded from url %s", imageUrl.toString()); | ||||
|                     setWallpaper(context, Bitmap.createBitmap(bitmap)); | ||||
|                     dataSource.close(); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             @Override | ||||
|             public void onFailureImpl(DataSource dataSource) { | ||||
|                 Timber.d("Error getting bitmap from image url %s", imageUrl.toString()); | ||||
|                 showNotification(context, "Setting Wallpaper Failed", "Failed to download image."); | ||||
|                 if (dataSource != null) { | ||||
|                     dataSource.close(); | ||||
|                 } | ||||
|             } | ||||
|         }, CallerThreadExecutor.getInstance()); | ||||
| 
 | ||||
|         return Result.success(); | ||||
|     } | ||||
| 
 | ||||
|     private  void setWallpaper(Context context, Bitmap bitmap) { | ||||
|         WallpaperManager wallpaperManager = WallpaperManager.getInstance(context); | ||||
| 
 | ||||
|         try { | ||||
|             wallpaperManager.setBitmap(bitmap); | ||||
|             showNotification(context, "Wallpaper Set", "Wallpaper has been updated successfully."); | ||||
| 
 | ||||
|         } catch (Exception e) { | ||||
|             Timber.e(e, "Error setting wallpaper"); | ||||
|             showNotification(context, "Setting Wallpaper Failed", " "+e.getLocalizedMessage()); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private void showProgressNotification(Context context) { | ||||
|         NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); | ||||
|         NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID) | ||||
|             .setSmallIcon(R.drawable.commons_logo) | ||||
|             .setContentTitle("Setting Wallpaper") | ||||
|             .setContentText("Please wait...") | ||||
|             .setPriority(NotificationCompat.PRIORITY_HIGH) | ||||
|             .setOngoing(true) | ||||
|             .setProgress(0, 0, true); | ||||
|         notificationManager.notify(NOTIFICATION_ID, builder.build()); | ||||
|     } | ||||
| 
 | ||||
|     private void showNotification(Context context, String title, String content) { | ||||
|         NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); | ||||
|         NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID) | ||||
|             .setSmallIcon(R.drawable.commons_logo) | ||||
|             .setContentTitle(title) | ||||
|             .setContentText(content) | ||||
|             .setPriority(NotificationCompat.PRIORITY_HIGH) | ||||
|             .setOngoing(false); | ||||
|         notificationManager.notify(NOTIFICATION_ID, builder.build()); | ||||
|     } | ||||
| 
 | ||||
|     private void createNotificationChannel(Context context) { | ||||
|         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | ||||
|             CharSequence name = "Wallpaper Setting"; | ||||
|             String description = "Notifications for wallpaper setting progress"; | ||||
|             int importance = NotificationManager.IMPORTANCE_HIGH; | ||||
|             NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, name, importance); | ||||
|             channel.setDescription(description); | ||||
|             NotificationManager notificationManager = context.getSystemService(NotificationManager.class); | ||||
|             notificationManager.createNotificationChannel(channel); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,113 @@ | |||
| package fr.free.nrw.commons.contributions | ||||
| 
 | ||||
| import android.app.NotificationChannel | ||||
| import android.app.NotificationManager | ||||
| import android.app.WallpaperManager | ||||
| import android.content.Context | ||||
| import android.graphics.Bitmap | ||||
| import android.net.Uri | ||||
| import android.os.Build | ||||
| import androidx.core.app.NotificationCompat | ||||
| import androidx.work.Worker | ||||
| import androidx.work.WorkerParameters | ||||
| import com.facebook.common.executors.CallerThreadExecutor | ||||
| import com.facebook.common.references.CloseableReference | ||||
| import com.facebook.datasource.DataSource | ||||
| import com.facebook.drawee.backends.pipeline.Fresco | ||||
| import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber | ||||
| import com.facebook.imagepipeline.image.CloseableImage | ||||
| import com.facebook.imagepipeline.request.ImageRequestBuilder | ||||
| import fr.free.nrw.commons.R | ||||
| import timber.log.Timber | ||||
| 
 | ||||
| class SetWallpaperWorker(context: Context, params: WorkerParameters) : | ||||
|     Worker(context, params) { | ||||
|     override fun doWork(): Result { | ||||
|         val context = applicationContext | ||||
|         createNotificationChannel(context) | ||||
|         showProgressNotification(context) | ||||
| 
 | ||||
|         val imageUrl = inputData.getString("imageUrl") ?: return Result.failure() | ||||
| 
 | ||||
|         val imageRequest = ImageRequestBuilder | ||||
|             .newBuilderWithSource(Uri.parse(imageUrl)) | ||||
|             .build() | ||||
| 
 | ||||
|         val imagePipeline = Fresco.getImagePipeline() | ||||
|         val dataSource = imagePipeline.fetchDecodedImage(imageRequest, context) | ||||
| 
 | ||||
|         dataSource.subscribe(object : BaseBitmapDataSubscriber() { | ||||
|             public override fun onNewResultImpl(bitmap: Bitmap?) { | ||||
|                 if (dataSource.isFinished && bitmap != null) { | ||||
|                     Timber.d("Bitmap loaded from url %s", imageUrl.toString()) | ||||
|                     setWallpaper(context, Bitmap.createBitmap(bitmap)) | ||||
|                     dataSource.close() | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             override fun onFailureImpl(dataSource: DataSource<CloseableReference<CloseableImage>>?) { | ||||
|                 Timber.d("Error getting bitmap from image url %s", imageUrl.toString()) | ||||
|                 showNotification(context, "Setting Wallpaper Failed", "Failed to download image.") | ||||
|                 dataSource?.close() | ||||
|             } | ||||
|         }, CallerThreadExecutor.getInstance()) | ||||
| 
 | ||||
|         return Result.success() | ||||
|     } | ||||
| 
 | ||||
|     private fun setWallpaper(context: Context, bitmap: Bitmap) { | ||||
|         val wallpaperManager = WallpaperManager.getInstance(context) | ||||
| 
 | ||||
|         try { | ||||
|             wallpaperManager.setBitmap(bitmap) | ||||
|             showNotification(context, "Wallpaper Set", "Wallpaper has been updated successfully.") | ||||
|         } catch (e: Exception) { | ||||
|             Timber.e(e, "Error setting wallpaper") | ||||
|             showNotification(context, "Setting Wallpaper Failed", " " + e.localizedMessage) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun showProgressNotification(context: Context) { | ||||
|         val notificationManager = | ||||
|             context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager | ||||
|         val builder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID) | ||||
|             .setSmallIcon(R.drawable.commons_logo) | ||||
|             .setContentTitle("Setting Wallpaper") | ||||
|             .setContentText("Please wait...") | ||||
|             .setPriority(NotificationCompat.PRIORITY_HIGH) | ||||
|             .setOngoing(true) | ||||
|             .setProgress(0, 0, true) | ||||
|         notificationManager.notify(NOTIFICATION_ID, builder.build()) | ||||
|     } | ||||
| 
 | ||||
|     private fun showNotification(context: Context, title: String, content: String) { | ||||
|         val notificationManager = | ||||
|             context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager | ||||
|         val builder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID) | ||||
|             .setSmallIcon(R.drawable.commons_logo) | ||||
|             .setContentTitle(title) | ||||
|             .setContentText(content) | ||||
|             .setPriority(NotificationCompat.PRIORITY_HIGH) | ||||
|             .setOngoing(false) | ||||
|         notificationManager.notify(NOTIFICATION_ID, builder.build()) | ||||
|     } | ||||
| 
 | ||||
|     private fun createNotificationChannel(context: Context) { | ||||
|         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | ||||
|             val name: CharSequence = "Wallpaper Setting" | ||||
|             val description = "Notifications for wallpaper setting progress" | ||||
|             val importance = NotificationManager.IMPORTANCE_HIGH | ||||
|             val channel = NotificationChannel(NOTIFICATION_CHANNEL_ID, name, importance) | ||||
|             channel.description = description | ||||
|             val notificationManager = context.getSystemService( | ||||
|                 NotificationManager::class.java | ||||
|             ) | ||||
|             notificationManager.createNotificationChannel(channel) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     companion object { | ||||
|         private const val NOTIFICATION_CHANNEL_ID = "set_wallpaper_channel" | ||||
|         private const val NOTIFICATION_ID = 1 | ||||
|     } | ||||
| } | ||||
|  | @ -1,31 +0,0 @@ | |||
| package fr.free.nrw.commons.contributions; | ||||
| 
 | ||||
| import android.content.Context; | ||||
| import android.util.AttributeSet; | ||||
| import android.view.MotionEvent; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.viewpager.widget.ViewPager; | ||||
| 
 | ||||
| public class UnswipableViewPager extends ViewPager{ | ||||
|     public UnswipableViewPager(@NonNull Context context) { | ||||
|         super(context); | ||||
|     } | ||||
| 
 | ||||
|     public UnswipableViewPager(@NonNull Context context, @Nullable AttributeSet attrs) { | ||||
|         super(context, attrs); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public boolean onInterceptTouchEvent(MotionEvent event) { | ||||
|         // Unswipable | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public boolean onTouchEvent(MotionEvent event) { | ||||
|         // Unswipable | ||||
|         return false; | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,22 @@ | |||
| package fr.free.nrw.commons.contributions | ||||
| 
 | ||||
| import android.content.Context | ||||
| import android.util.AttributeSet | ||||
| import android.view.MotionEvent | ||||
| import androidx.viewpager.widget.ViewPager | ||||
| 
 | ||||
| class UnswipableViewPager : ViewPager { | ||||
|     constructor(context: Context) : super(context) | ||||
| 
 | ||||
|     constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) | ||||
| 
 | ||||
|     override fun onInterceptTouchEvent(event: MotionEvent): Boolean { | ||||
|         // Unswipable | ||||
|         return false | ||||
|     } | ||||
| 
 | ||||
|     override fun onTouchEvent(event: MotionEvent): Boolean { | ||||
|         // Unswipable | ||||
|         return false | ||||
|     } | ||||
| } | ||||
|  | @ -43,7 +43,7 @@ class WikipediaInstructionsDialogFragment : DialogFragment() { | |||
|     /** | ||||
|      * Callback for handling confirm button clicked | ||||
|      */ | ||||
|     interface Callback { | ||||
|     fun interface Callback { | ||||
|         fun onConfirmClicked( | ||||
|             contribution: Contribution?, | ||||
|             copyWikicode: Boolean, | ||||
|  |  | |||
|  | @ -9,6 +9,7 @@ import fr.free.nrw.commons.CommonsApplication | |||
| import fr.free.nrw.commons.activity.SingleWebViewActivity | ||||
| import fr.free.nrw.commons.auth.LoginActivity | ||||
| import fr.free.nrw.commons.contributions.ContributionsModule | ||||
| import fr.free.nrw.commons.contributions.ContributionsProvidesModule | ||||
| import fr.free.nrw.commons.explore.SearchModule | ||||
| import fr.free.nrw.commons.explore.categories.CategoriesModule | ||||
| import fr.free.nrw.commons.explore.depictions.DepictionModule | ||||
|  | @ -40,6 +41,7 @@ import javax.inject.Singleton | |||
|         ContentProviderBuilderModule::class, | ||||
|         UploadModule::class, | ||||
|         ContributionsModule::class, | ||||
|         ContributionsProvidesModule::class, | ||||
|         SearchModule::class, | ||||
|         DepictionModule::class, | ||||
|         CategoriesModule::class | ||||
|  |  | |||
|  | @ -15,8 +15,8 @@ abstract class CommonsDaggerSupportFragment : Fragment(), HasSupportFragmentInje | |||
|     @Inject @JvmField | ||||
|     var childFragmentInjector: DispatchingAndroidInjector<Fragment>? = null | ||||
| 
 | ||||
|     @JvmField | ||||
|     protected var compositeDisposable: CompositeDisposable = CompositeDisposable() | ||||
|     // Removed @JvmField to allow overriding | ||||
|     protected open var compositeDisposable: CompositeDisposable = CompositeDisposable() | ||||
| 
 | ||||
|     override fun onAttach(context: Context) { | ||||
|         inject() | ||||
|  | @ -63,4 +63,9 @@ abstract class CommonsDaggerSupportFragment : Fragment(), HasSupportFragmentInje | |||
| 
 | ||||
|         return getInstance(activity.applicationContext) | ||||
|     } | ||||
| 
 | ||||
|     // Ensure getContext() returns a non-null Context | ||||
|     override fun getContext(): Context { | ||||
|         return super.getContext() ?: throw IllegalStateException("Context is null") | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -467,7 +467,7 @@ public class ExploreMapFragment extends CommonsDaggerSupportFragment | |||
|             nearbyPlacesInfoObservable = presenter.loadAttractionsFromLocation(getLastMapFocus(), | ||||
|                 currentLatLng, false); | ||||
|         } | ||||
|         compositeDisposable.add(nearbyPlacesInfoObservable | ||||
|             getCompositeDisposable().add(nearbyPlacesInfoObservable | ||||
|             .subscribeOn(Schedulers.io()) | ||||
|             .observeOn(AndroidSchedulers.mainThread()) | ||||
|             .subscribe(explorePlacesInfo -> { | ||||
|  |  | |||
|  | @ -426,7 +426,7 @@ object FilePicker : Constants { | |||
|         fun onCanceled(source: ImageSource, type: Int) | ||||
|     } | ||||
| 
 | ||||
|     interface HandleActivityResult { | ||||
|     fun interface HandleActivityResult { | ||||
|         fun onHandleActivityResult(callbacks: Callbacks) | ||||
|     } | ||||
| } | ||||
|  | @ -493,7 +493,7 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C | |||
| 
 | ||||
|         val contributionsFragment: ContributionsFragment? = this.getContributionsFragmentParent() | ||||
|         if (contributionsFragment?.binding != null) { | ||||
|             contributionsFragment.binding.cardViewNearby.visibility = View.GONE | ||||
|             contributionsFragment.binding!!.cardViewNearby.visibility = View.GONE | ||||
|         } | ||||
| 
 | ||||
|         // detail provider is null when fragment is shown in review activity | ||||
|  |  | |||
|  | @ -31,8 +31,8 @@ class NavTabLayout : BottomNavigationView { | |||
| 
 | ||||
|     private fun setTabViews() { | ||||
|         val isLoginSkipped = (context as MainActivity) | ||||
|             .applicationKvStore.getBoolean("login_skipped") | ||||
|         if (isLoginSkipped) { | ||||
|             .applicationKvStore?.getBoolean("login_skipped") | ||||
|         if (isLoginSkipped == true) { | ||||
|             for (i in 0 until NavTabLoggedOut.size()) { | ||||
|                 val navTab = NavTabLoggedOut.of(i) | ||||
|                 menu.add(Menu.NONE, i, i, navTab.text()).setIcon(navTab.icon()) | ||||
|  |  | |||
|  | @ -742,7 +742,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment | |||
|     public void onPause() { | ||||
|         super.onPause(); | ||||
|         binding.map.onPause(); | ||||
|         compositeDisposable.clear(); | ||||
|         getCompositeDisposable().clear(); | ||||
|         presenter.detachView(); | ||||
|         registerUnregisterLocationListener(true); | ||||
|         try { | ||||
|  | @ -857,7 +857,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment | |||
|             0.75); | ||||
|         binding.nearbyFilterList.searchListView.setAdapter(nearbyFilterSearchRecyclerViewAdapter); | ||||
|         LayoutUtils.setLayoutHeightAlignedToWidth(1.25, binding.nearbyFilterList.getRoot()); | ||||
|         compositeDisposable.add( | ||||
|         getCompositeDisposable().add( | ||||
|             RxSearchView.queryTextChanges(binding.nearbyFilter.searchViewLayout.searchView) | ||||
|                 .takeUntil(RxView.detaches(binding.nearbyFilter.searchViewLayout.searchView)) | ||||
|                 .debounce(500, TimeUnit.MILLISECONDS) | ||||
|  | @ -1234,7 +1234,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment | |||
|      */ | ||||
|     private void emptyCache() { | ||||
|         // reload the map once the cache is cleared | ||||
|         compositeDisposable.add( | ||||
|         getCompositeDisposable().add( | ||||
|             placesRepository.clearCache() | ||||
|                 .subscribeOn(Schedulers.io()) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|  | @ -1269,7 +1269,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment | |||
|         final Observable<String> savePlacesObservable = Observable | ||||
|             .fromCallable(() -> nearbyController | ||||
|                 .getPlacesAsKML(getMapFocus())); | ||||
|         compositeDisposable.add(savePlacesObservable | ||||
|         getCompositeDisposable().add(savePlacesObservable | ||||
|             .subscribeOn(Schedulers.io()) | ||||
|             .observeOn(AndroidSchedulers.mainThread()) | ||||
|             .subscribe(kmlString -> { | ||||
|  | @ -1303,7 +1303,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment | |||
|         final Observable<String> savePlacesObservable = Observable | ||||
|             .fromCallable(() -> nearbyController | ||||
|                 .getPlacesAsGPX(getMapFocus())); | ||||
|         compositeDisposable.add(savePlacesObservable | ||||
|         getCompositeDisposable().add(savePlacesObservable | ||||
|             .subscribeOn(Schedulers.io()) | ||||
|             .observeOn(AndroidSchedulers.mainThread()) | ||||
|             .subscribe(gpxString -> { | ||||
|  | @ -1405,7 +1405,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment | |||
|         final Observable<List<Place>> getPlaceObservable = Observable | ||||
|             .fromCallable(() -> nearbyController | ||||
|                 .getPlaces(List.of(place))); | ||||
|         compositeDisposable.add(getPlaceObservable | ||||
|         getCompositeDisposable().add(getPlaceObservable | ||||
|             .subscribeOn(Schedulers.io()) | ||||
|             .observeOn(AndroidSchedulers.mainThread()) | ||||
|             .subscribe(placeList -> { | ||||
|  | @ -1449,7 +1449,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment | |||
|                     searchLatLng, | ||||
|                     false, true, Utils.isMonumentsEnabled(new Date()), customQuery)); | ||||
| 
 | ||||
|         compositeDisposable.add(nearbyPlacesInfoObservable | ||||
|         getCompositeDisposable().add(nearbyPlacesInfoObservable | ||||
|             .subscribeOn(Schedulers.io()) | ||||
|             .observeOn(AndroidSchedulers.mainThread()) | ||||
|             .subscribe(nearbyPlacesInfo -> { | ||||
|  | @ -1486,7 +1486,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment | |||
|                     searchLatLng, | ||||
|                     false, true, Utils.isMonumentsEnabled(new Date()), customQuery)); | ||||
| 
 | ||||
|         compositeDisposable.add(nearbyPlacesInfoObservable | ||||
|         getCompositeDisposable().add(nearbyPlacesInfoObservable | ||||
|             .subscribeOn(Schedulers.io()) | ||||
|             .observeOn(AndroidSchedulers.mainThread()) | ||||
|             .subscribe(nearbyPlacesInfo -> { | ||||
|  | @ -1518,7 +1518,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment | |||
|     } | ||||
| 
 | ||||
|     public void savePlaceToDatabase(Place place) { | ||||
|         compositeDisposable.add(placesRepository | ||||
|         getCompositeDisposable().add(placesRepository | ||||
|             .save(place) | ||||
|             .subscribeOn(Schedulers.io()) | ||||
|             .subscribe()); | ||||
|  | @ -1531,7 +1531,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment | |||
|     @Override | ||||
|     public void stopQuery() { | ||||
|         stopQuery = true; | ||||
|         compositeDisposable.clear(); | ||||
|         getCompositeDisposable().clear(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  |  | |||
|  | @ -204,7 +204,7 @@ class UploadRepository @Inject constructor( | |||
|      * @param filePath file to be checked | ||||
|      * @return IMAGE_DUPLICATE or IMAGE_OK | ||||
|      */ | ||||
|     fun checkDuplicateImage(originalFilePath: Uri, modifiedFilePath: Uri): Single<Int> { | ||||
|     fun checkDuplicateImage(originalFilePath: Uri?, modifiedFilePath: Uri?): Single<Int> { | ||||
|         return uploadModel.checkDuplicateImage(originalFilePath, modifiedFilePath) | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -28,8 +28,7 @@ import javax.inject.Named | |||
| 
 | ||||
| /** | ||||
|  * The presenter class for PendingUploadsFragment and FailedUploadsFragment | ||||
|  */ | ||||
| class PendingUploadsPresenter @Inject internal constructor( | ||||
|  */ class PendingUploadsPresenter @Inject internal constructor( | ||||
|     private val contributionBoundaryCallback: ContributionBoundaryCallback, | ||||
|     private val contributionsRemoteDataSource: ContributionsRemoteDataSource, | ||||
|     private val contributionsRepository: ContributionsRepository, | ||||
|  | @ -89,12 +88,16 @@ class PendingUploadsPresenter @Inject internal constructor( | |||
|      * @param context      The context in which the operation is being performed. | ||||
|      */ | ||||
|     override fun deleteUpload(contribution: Contribution?, context: Context?) { | ||||
|         compositeDisposable.add( | ||||
|         contribution?.let { | ||||
|             contributionsRepository | ||||
|                 .deleteContributionFromDB(contribution) | ||||
|                 .deleteContributionFromDB(it) | ||||
|                 .subscribeOn(ioThreadScheduler) | ||||
|                 .subscribe() | ||||
|         ) | ||||
|         }?.let { | ||||
|             compositeDisposable.add( | ||||
|                 it | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  |  | |||
|  | @ -679,7 +679,7 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C | |||
|     } | ||||
| 
 | ||||
|     private fun receiveExternalSharedItems() { | ||||
|         uploadableFiles = contributionController!!.handleExternalImagesPicked(this, intent) | ||||
|         uploadableFiles = contributionController!!.handleExternalImagesPicked(this, intent).toMutableList() | ||||
|     } | ||||
| 
 | ||||
|     private fun receiveInternalSharedItems() { | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| package fr.free.nrw.commons.upload.categories | ||||
| 
 | ||||
| import android.annotation.SuppressLint | ||||
| import android.app.Activity | ||||
| import android.app.ProgressDialog | ||||
| import android.content.Context | ||||
|  | @ -89,6 +90,7 @@ class UploadCategoriesFragment : UploadBaseFragment(), CategoriesContract.View { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @SuppressLint("StringFormatMatches") | ||||
|     private fun init() { | ||||
|         if (binding == null) { | ||||
|             return | ||||
|  | @ -372,8 +374,9 @@ class UploadCategoriesFragment : UploadBaseFragment(), CategoriesContract.View { | |||
| 
 | ||||
|             (requireActivity() as AppCompatActivity).supportActionBar?.hide() | ||||
| 
 | ||||
| 
 | ||||
|             if (parentFragment?.parentFragment?.parentFragment is ContributionsFragment) { | ||||
|                 ((parentFragment?.parentFragment?.parentFragment) as ContributionsFragment).binding.cardViewNearby.visibility = View.GONE | ||||
|                 ((parentFragment?.parentFragment?.parentFragment) as ContributionsFragment).binding?.cardViewNearby?.visibility = View.GONE | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  |  | |||
|  | @ -149,7 +149,7 @@ class UploadWorker( | |||
|                 currentNotification.build(), | ||||
|             ) | ||||
|             contribution!!.transferred = transferred | ||||
|             contributionDao.update(contribution).blockingAwait() | ||||
|             contributionDao.update(contribution!!).blockingAwait() | ||||
|         } | ||||
| 
 | ||||
|         open fun onChunkUploaded( | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Sujal
						Sujal